@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
package/lib/lambda.js ADDED
@@ -0,0 +1,992 @@
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 constants = require('./constants');
10
+ const shimmer = require('./instrumentation/shimmer');
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const querystring = require('querystring');
14
+
15
+ const { MAX_MESSAGES_PROCESSED_FOR_TRACE_CONTEXT } = require('./constants');
16
+
17
+ // https://github.com/elastic/apm/blob/main/specs/agents/tracing-instrumentation-aws-lambda.md#deriving-cold-starts
18
+ let isFirstRun = true;
19
+ let gFaasId; // Set on first invocation.
20
+
21
+ // The trigger types for which we support special handling.
22
+ // https://docs.aws.amazon.com/lambda/latest/dg/lambda-services.html
23
+ const TRIGGER_GENERIC = 1;
24
+ const TRIGGER_API_GATEWAY = 2; // This includes Lambda URLs which use the same event format.
25
+ const TRIGGER_SNS = 3;
26
+ const TRIGGER_SQS = 4;
27
+ const TRIGGER_S3_SINGLE_EVENT = 5;
28
+ const TRIGGER_ELB = 6; // Elastic Load Balancer, aka Application Load Balancer
29
+
30
+ function triggerTypeFromEvent(event) {
31
+ if (event.requestContext) {
32
+ if (event.requestContext.elb) {
33
+ return TRIGGER_ELB;
34
+ } else if (event.requestContext.requestId) {
35
+ return TRIGGER_API_GATEWAY;
36
+ }
37
+ }
38
+ if (event.Records && event.Records.length >= 1) {
39
+ const eventSource =
40
+ event.Records[0].eventSource || // S3 and SQS
41
+ event.Records[0].EventSource; // SNS
42
+ if (eventSource === 'aws:sns') {
43
+ return TRIGGER_SNS;
44
+ } else if (eventSource === 'aws:sqs') {
45
+ return TRIGGER_SQS;
46
+ } else if (eventSource === 'aws:s3' && event.Records.length === 1) {
47
+ return TRIGGER_S3_SINGLE_EVENT;
48
+ }
49
+ }
50
+ return TRIGGER_GENERIC;
51
+ }
52
+
53
+ // Gather APM metadata for this Lambda executor per
54
+ // https://github.com/elastic/apm/blob/main/specs/agents/tracing-instrumentation-aws-lambda.md#overwriting-metadata
55
+ function getMetadata(agent, cloudAccountId) {
56
+ return {
57
+ service: {
58
+ framework: {
59
+ // Passing this service.framework.name to Client#setExtraMetadata()
60
+ // ensures that it "wins" over a framework name from
61
+ // `agent.setFramework()`, because in the client `_extraMetadata`
62
+ // wins over `_conf.frameworkName`.
63
+ name: 'AWS Lambda',
64
+ },
65
+ runtime: {
66
+ name: process.env.AWS_EXECUTION_ENV,
67
+ },
68
+ node: {
69
+ configured_name: process.env.AWS_LAMBDA_LOG_STREAM_NAME,
70
+ },
71
+ },
72
+ cloud: {
73
+ provider: 'aws',
74
+ region: process.env.AWS_REGION,
75
+ service: {
76
+ name: 'lambda',
77
+ },
78
+ account: {
79
+ id: cloudAccountId,
80
+ },
81
+ },
82
+ };
83
+ }
84
+
85
+ function getFaasData(context, faasId, isColdStart, faasTriggerType, requestId) {
86
+ const faasData = {
87
+ id: faasId,
88
+ name: context.functionName,
89
+ version: context.functionVersion,
90
+ coldstart: isColdStart,
91
+ execution: context.awsRequestId,
92
+ trigger: {
93
+ type: faasTriggerType,
94
+ },
95
+ };
96
+ if (requestId) {
97
+ faasData.trigger.request_id = requestId;
98
+ }
99
+ return faasData;
100
+ }
101
+
102
+ function setGenericData(trans, event, context, faasId, isColdStart) {
103
+ trans.type = 'request';
104
+ trans.setDefaultName(context.functionName);
105
+
106
+ trans.setFaas(getFaasData(context, faasId, isColdStart, 'other'));
107
+
108
+ const cloudContext = {
109
+ origin: {
110
+ provider: 'aws',
111
+ },
112
+ };
113
+ trans.setCloudContext(cloudContext);
114
+ }
115
+
116
+ // Set transaction data for an API Gateway triggered invocation.
117
+ //
118
+ // Handle API Gateway payload format vers 1.0 (a.k.a "REST") and 2.0 ("HTTP").
119
+ // https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
120
+ function setApiGatewayData(agent, trans, event, context, faasId, isColdStart) {
121
+ const requestContext = event.requestContext;
122
+
123
+ let name;
124
+ let pseudoReq;
125
+ if (requestContext.http) {
126
+ // 2.0
127
+ if (agent._conf.usePathAsTransactionName) {
128
+ name = `${requestContext.http.method} ${requestContext.http.path}`;
129
+ } else {
130
+ // Get a routeKeyPath from the routeKey:
131
+ // GET /some/path -> /some/path
132
+ // ANY /some/path -> /some/path
133
+ // $default -> /$default
134
+ let routeKeyPath = requestContext.routeKey;
135
+ const spaceIdx = routeKeyPath.indexOf(' ');
136
+ if (spaceIdx === -1) {
137
+ routeKeyPath = '/' + routeKeyPath;
138
+ } else {
139
+ routeKeyPath = routeKeyPath.slice(spaceIdx + 1);
140
+ }
141
+ name = `${requestContext.http.method} /${requestContext.stage}${routeKeyPath}`;
142
+ }
143
+ pseudoReq = {
144
+ httpVersion: requestContext.http.protocol
145
+ ? requestContext.http.protocol.split('/')[1] // 'HTTP/1.1' -> '1.1'
146
+ : undefined,
147
+ method: requestContext.http.method,
148
+ url:
149
+ event.rawPath +
150
+ (event.rawQueryString ? '?' + event.rawQueryString : ''),
151
+ headers: event.normedHeaders || {},
152
+ socket: { remoteAddress: requestContext.http.sourceIp },
153
+ body: event.body,
154
+ };
155
+ } else {
156
+ // payload version format 1.0
157
+ if (agent._conf.usePathAsTransactionName) {
158
+ name = `${requestContext.httpMethod} ${requestContext.path}`;
159
+ } else {
160
+ name = `${requestContext.httpMethod} /${requestContext.stage}${requestContext.resourcePath}`;
161
+ }
162
+ pseudoReq = {
163
+ httpVersion: requestContext.protocol
164
+ ? requestContext.protocol.split('/')[1] // 'HTTP/1.1' -> '1.1'
165
+ : undefined,
166
+ method: requestContext.httpMethod,
167
+ url:
168
+ requestContext.path +
169
+ (event.queryStringParameters
170
+ ? '?' + querystring.encode(event.queryStringParameters)
171
+ : ''),
172
+ headers: event.normedHeaders || {},
173
+ socket: {
174
+ remoteAddress:
175
+ requestContext.identity && requestContext.identity.sourceIp,
176
+ },
177
+ // Limitation: Note that `getContextFromRequest` does *not* use this body,
178
+ // because API Gateway payload format 1.0 does not include the
179
+ // Content-Length header from the original request.
180
+ body: event.body,
181
+ };
182
+ }
183
+ trans.type = 'request';
184
+ trans.setDefaultName(name);
185
+ trans.req = pseudoReq; // Used by parsers.getContextFromRequest() for adding context to transaction and errors.
186
+
187
+ trans.setFaas(
188
+ getFaasData(context, faasId, isColdStart, 'http', requestContext.requestId),
189
+ );
190
+
191
+ const serviceContext = {
192
+ origin: {
193
+ name: requestContext.domainName,
194
+ id: requestContext.apiId,
195
+ version: event.version || '1.0',
196
+ },
197
+ };
198
+ trans.setServiceContext(serviceContext);
199
+
200
+ const originSvcName =
201
+ // `<url-id>.lambda-url.<region>.on.aws` indicates this is a Lambda URL.
202
+ requestContext.domainName &&
203
+ requestContext.domainPrefix &&
204
+ requestContext.domainName.startsWith(
205
+ requestContext.domainPrefix + '.lambda-url.',
206
+ )
207
+ ? 'lambda url'
208
+ : 'api gateway';
209
+ const cloudContext = {
210
+ origin: {
211
+ provider: 'aws',
212
+ service: {
213
+ name: originSvcName,
214
+ },
215
+ account: {
216
+ id: requestContext.accountId,
217
+ },
218
+ },
219
+ };
220
+ trans.setCloudContext(cloudContext);
221
+ }
222
+
223
+ function setTransDataFromApiGatewayResult(err, result, trans, event) {
224
+ if (err) {
225
+ trans.result = 'HTTP 5xx';
226
+ trans._setOutcomeFromHttpStatusCode(500);
227
+ } else if (result && result.statusCode) {
228
+ trans.result = 'HTTP ' + result.statusCode.toString()[0] + 'xx';
229
+ trans._setOutcomeFromHttpStatusCode(result.statusCode);
230
+ } else {
231
+ trans.result = constants.RESULT_SUCCESS;
232
+ trans._setOutcomeFromHttpStatusCode(200);
233
+ }
234
+
235
+ // This doc defines the format of API Gateway-triggered responses, from which
236
+ // we can infer `transaction.context.response` values.
237
+ // https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.response
238
+ // https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads
239
+ if (err) {
240
+ trans.res = {
241
+ statusCode: 500,
242
+ };
243
+ } else if (event.requestContext.http) {
244
+ // payload format version 2.0
245
+ if (result && result.statusCode) {
246
+ trans.res = {
247
+ statusCode: result.statusCode,
248
+ headers: result.headers,
249
+ };
250
+ } else {
251
+ trans.res = {
252
+ statusCode: 200,
253
+ headers: { 'content-type': 'application/json' },
254
+ };
255
+ }
256
+ } else {
257
+ // payload format version 1.0
258
+ if (result && result.statusCode) {
259
+ trans.res = {
260
+ statusCode: result.statusCode,
261
+ headers: result.headers,
262
+ };
263
+ }
264
+ }
265
+ }
266
+
267
+ // https://docs.aws.amazon.com/lambda/latest/dg/services-alb.html
268
+ function setElbData(agent, trans, event, context, faasId, isColdStart) {
269
+ trans.type = 'request';
270
+ let name;
271
+ if (agent._conf.usePathAsTransactionName) {
272
+ name = `${event.httpMethod} ${event.path}`;
273
+ } else {
274
+ name = `${event.httpMethod} unknown route`;
275
+ }
276
+ trans.setDefaultName(name);
277
+ trans.req = {
278
+ // Used by parsers.getContextFromRequest() for adding context to transaction and errors.
279
+ method: event.httpMethod,
280
+ url:
281
+ event.path +
282
+ (event.queryStringParameters &&
283
+ Object.keys(event.queryStringParameters) > 0
284
+ ? '?' + querystring.encode(event.queryStringParameters)
285
+ : ''),
286
+ headers: event.normedHeaders || {},
287
+ body: event.body,
288
+ bodyIsBase64Encoded: event.isBase64Encoded,
289
+ };
290
+
291
+ trans.setFaas(getFaasData(context, faasId, isColdStart, 'http'));
292
+
293
+ const targetGroupArn = event.requestContext.elb.targetGroupArn;
294
+ const arnParts = targetGroupArn.split(':');
295
+ trans.setServiceContext({
296
+ origin: {
297
+ name: arnParts[5].split('/')[1],
298
+ id: targetGroupArn,
299
+ },
300
+ });
301
+
302
+ trans.setCloudContext({
303
+ origin: {
304
+ provider: 'aws',
305
+ region: arnParts[3],
306
+ service: {
307
+ name: 'elb',
308
+ },
309
+ account: {
310
+ id: arnParts[4],
311
+ },
312
+ },
313
+ });
314
+ }
315
+
316
+ function setTransDataFromElbResult(err, result, trans) {
317
+ // ELB defaults to 502 Bad Gateway if there is an error or `result.statusCode`
318
+ // is missing or not an integer.
319
+ const validStatusCode =
320
+ result &&
321
+ result.statusCode &&
322
+ typeof result.statusCode === 'number' &&
323
+ Number.isInteger(result.statusCode)
324
+ ? result.statusCode
325
+ : null;
326
+
327
+ if (err) {
328
+ trans.result = 'HTTP 5xx';
329
+ } else if (validStatusCode) {
330
+ trans.result = 'HTTP ' + validStatusCode.toString()[0] + 'xx';
331
+ } else {
332
+ trans.result = 'HTTP 5xx';
333
+ }
334
+
335
+ // Set an appropriate pseudo `trans.res`, used by `parsers.getContextFromResponse()`.
336
+ if (err) {
337
+ trans.res = {
338
+ statusCode: 502,
339
+ };
340
+ } else {
341
+ trans.res = {
342
+ statusCode: validStatusCode || 502,
343
+ headers: result.headers,
344
+ };
345
+ }
346
+
347
+ trans._setOutcomeFromHttpStatusCode(validStatusCode || 502);
348
+ }
349
+
350
+ function setSqsData(agent, trans, event, context, faasId, isColdStart) {
351
+ const record = event && event.Records && event.Records[0];
352
+ const eventSourceARN = record.eventSourceARN ? record.eventSourceARN : '';
353
+
354
+ trans.setFaas(getFaasData(context, faasId, isColdStart, 'pubsub'));
355
+
356
+ const arnParts = eventSourceARN.split(':');
357
+ const queueName = arnParts[5];
358
+ const accountId = arnParts[4];
359
+
360
+ trans.setDefaultName(`RECEIVE ${queueName}`);
361
+ trans.type = 'messaging';
362
+
363
+ const serviceContext = {
364
+ origin: {
365
+ name: queueName,
366
+ id: eventSourceARN,
367
+ },
368
+ };
369
+ trans.setServiceContext(serviceContext);
370
+
371
+ const cloudContext = {
372
+ origin: {
373
+ provider: 'aws',
374
+ region: record.awsRegion,
375
+ service: {
376
+ name: 'sqs',
377
+ },
378
+ account: {
379
+ id: accountId,
380
+ },
381
+ },
382
+ };
383
+ trans.setCloudContext(cloudContext);
384
+
385
+ const links = spanLinksFromSqsRecords(event.Records);
386
+ trans.addLinks(links);
387
+ }
388
+
389
+ function setSnsData(agent, trans, event, context, faasId, isColdStart) {
390
+ const record = event && event.Records && event.Records[0];
391
+ const sns = record && record.Sns;
392
+
393
+ trans.setFaas(getFaasData(context, faasId, isColdStart, 'pubsub'));
394
+
395
+ const topicArn = (sns && sns.TopicArn) || '';
396
+ const arnParts = topicArn.split(':');
397
+ const topicName = arnParts[5];
398
+ const accountId = arnParts[4];
399
+ const region = arnParts[3];
400
+
401
+ trans.setDefaultName(`RECEIVE ${topicName}`);
402
+ trans.type = 'messaging';
403
+
404
+ const serviceContext = {
405
+ origin: {
406
+ name: topicName,
407
+ id: topicArn,
408
+ },
409
+ };
410
+ trans.setServiceContext(serviceContext);
411
+
412
+ const cloudContext = {
413
+ origin: {
414
+ provider: 'aws',
415
+ region,
416
+ service: {
417
+ name: 'sns',
418
+ },
419
+ account: {
420
+ id: accountId,
421
+ },
422
+ },
423
+ };
424
+ trans.setCloudContext(cloudContext);
425
+
426
+ const links = spanLinksFromSnsRecords(event.Records);
427
+ trans.addLinks(links);
428
+ }
429
+
430
+ function setS3SingleData(trans, event, context, faasId, isColdStart) {
431
+ const record = event.Records[0];
432
+
433
+ trans.setFaas(
434
+ getFaasData(
435
+ context,
436
+ faasId,
437
+ isColdStart,
438
+ 'datasource',
439
+ record.responseElements && record.responseElements['x-amz-request-id'],
440
+ ),
441
+ );
442
+
443
+ trans.setDefaultName(
444
+ `${record && record.eventName} ${
445
+ record && record.s3 && record.s3.bucket && record.s3.bucket.name
446
+ }`,
447
+ );
448
+ trans.type = 'request';
449
+
450
+ const serviceContext = {
451
+ origin: {
452
+ name: record && record.s3 && record.s3.bucket && record.s3.bucket.name,
453
+ id: record && record.s3 && record.s3.bucket && record.s3.bucket.arn,
454
+ version: record.eventVersion,
455
+ },
456
+ };
457
+ trans.setServiceContext(serviceContext);
458
+
459
+ const cloudContext = {
460
+ origin: {
461
+ provider: 'aws',
462
+ service: {
463
+ name: 's3',
464
+ },
465
+ region: record.awsRegion,
466
+ },
467
+ };
468
+ trans.setCloudContext(cloudContext);
469
+ }
470
+
471
+ function elasticApmAwsLambda(agent) {
472
+ const log = agent.logger;
473
+ const ins = agent._instrumentation;
474
+
475
+ /**
476
+ * Register this transaction with the Lambda extension, if possible. This
477
+ * function is `await`able so that the transaction is registered before
478
+ * executing the user's Lambda handler.
479
+ *
480
+ * Perf note: Using a Lambda sized to have 1 vCPU (1769MB memory), some
481
+ * rudimentary perf tests showed an average of 0.8ms for this call to the ext.
482
+ */
483
+ function registerTransaction(trans, awsRequestId) {
484
+ if (!agent._apmClient) {
485
+ return;
486
+ }
487
+ if (!agent._apmClient.lambdaShouldRegisterTransactions()) {
488
+ return;
489
+ }
490
+
491
+ // Reproduce the filtering logic from `Instrumentation.prototype.addEndedTransaction`.
492
+ if (agent._conf.contextPropagationOnly) {
493
+ return;
494
+ }
495
+ if (
496
+ !trans.sampled &&
497
+ !agent._apmClient.supportsKeepingUnsampledTransaction()
498
+ ) {
499
+ return;
500
+ }
501
+
502
+ var payload = trans.toJSON();
503
+ // If this partial transaction is used, the Lambda Extension will fill in:
504
+ // - `transaction.result` will be set to one of:
505
+ // - The "status" field from the Logs API platform `runtimeDone` message.
506
+ // https://docs.aws.amazon.com/lambda/latest/dg/runtimes-logs-api.html#runtimes-logs-api-ref-done
507
+ // Values: "success", "failure"
508
+ // - The "shutdownReason" field from the `Shutdown` event from the Extensions API.
509
+ // https://docs.aws.amazon.com/lambda/latest/dg/runtimes-extensions-api.html#runtimes-lifecycle-shutdown
510
+ // Values: "spindown", "timeout", "failure" (I think these are the values.)
511
+ // - `transaction.outcome` will be set to "failure" if the status above is
512
+ // not "success". Therefore we want a default outcome value.
513
+ // - `transaction.duration` will be estimated
514
+ delete payload.result;
515
+ delete payload.duration;
516
+
517
+ payload = agent._transactionFilters.process(payload);
518
+ if (!payload) {
519
+ log.trace(
520
+ { traceId: trans.traceId, transactionId: trans.id },
521
+ 'transaction ignored by filter',
522
+ );
523
+ return;
524
+ }
525
+
526
+ return agent._apmClient.lambdaRegisterTransaction(payload, awsRequestId);
527
+ }
528
+
529
+ function endAndFlushTransaction(
530
+ err,
531
+ result,
532
+ trans,
533
+ event,
534
+ context,
535
+ triggerType,
536
+ cb,
537
+ ) {
538
+ log.trace(
539
+ { awsRequestId: context && context.awsRequestId },
540
+ 'lambda: fn end',
541
+ );
542
+
543
+ switch (triggerType) {
544
+ case TRIGGER_API_GATEWAY:
545
+ setTransDataFromApiGatewayResult(err, result, trans, event);
546
+ break;
547
+ case TRIGGER_ELB:
548
+ setTransDataFromElbResult(err, result, trans);
549
+ break;
550
+ default:
551
+ if (err) {
552
+ trans.result = constants.RESULT_FAILURE;
553
+ trans.setOutcome(constants.OUTCOME_FAILURE);
554
+ } else {
555
+ trans.result = constants.RESULT_SUCCESS;
556
+ trans.setOutcome(constants.OUTCOME_SUCCESS);
557
+ }
558
+ break;
559
+ }
560
+
561
+ if (err) {
562
+ // Capture the error before trans.end() so it associates with the
563
+ // current trans. `skipOutcome` to avoid setting outcome on a possible
564
+ // currentSpan, because this error applies to the transaction, not any
565
+ // sub-span.
566
+ agent.captureError(err, { skipOutcome: true });
567
+ }
568
+
569
+ trans.end();
570
+
571
+ agent._flush({ lambdaEnd: true, inflightTimeout: 100 }, (flushErr) => {
572
+ if (flushErr) {
573
+ log.error(
574
+ { err: flushErr, awsRequestId: context && context.awsRequestId },
575
+ 'lambda: flush error',
576
+ );
577
+ }
578
+ log.trace(
579
+ { awsRequestId: context && context.awsRequestId },
580
+ 'lambda: wrapper end',
581
+ );
582
+ cb();
583
+ });
584
+ }
585
+
586
+ function wrapContext(runContext, trans, event, context, triggerType) {
587
+ shimmer.wrap(context, 'succeed', (origSucceed) => {
588
+ return ins.bindFunctionToRunContext(
589
+ runContext,
590
+ function wrappedSucceed(result) {
591
+ endAndFlushTransaction(
592
+ null,
593
+ result,
594
+ trans,
595
+ event,
596
+ context,
597
+ triggerType,
598
+ function () {
599
+ origSucceed(result);
600
+ },
601
+ );
602
+ },
603
+ );
604
+ });
605
+
606
+ shimmer.wrap(context, 'fail', (origFail) => {
607
+ return ins.bindFunctionToRunContext(
608
+ runContext,
609
+ function wrappedFail(err) {
610
+ endAndFlushTransaction(
611
+ err,
612
+ null,
613
+ trans,
614
+ event,
615
+ context,
616
+ triggerType,
617
+ function () {
618
+ origFail(err);
619
+ },
620
+ );
621
+ },
622
+ );
623
+ });
624
+
625
+ shimmer.wrap(context, 'done', (origDone) => {
626
+ return wrapLambdaCallback(
627
+ runContext,
628
+ trans,
629
+ event,
630
+ context,
631
+ triggerType,
632
+ origDone,
633
+ );
634
+ });
635
+ }
636
+
637
+ function wrapLambdaCallback(
638
+ runContext,
639
+ trans,
640
+ event,
641
+ context,
642
+ triggerType,
643
+ callback,
644
+ ) {
645
+ return ins.bindFunctionToRunContext(
646
+ runContext,
647
+ function wrappedLambdaCallback(err, result) {
648
+ endAndFlushTransaction(
649
+ err,
650
+ result,
651
+ trans,
652
+ event,
653
+ context,
654
+ triggerType,
655
+ () => {
656
+ callback(err, result);
657
+ },
658
+ );
659
+ },
660
+ );
661
+ }
662
+
663
+ return function wrapLambdaHandler(type, fn) {
664
+ if (typeof type === 'function') {
665
+ fn = type;
666
+ type = 'request';
667
+ }
668
+ if (!agent._conf.active) {
669
+ // Manual usage of `apm.lambda(...)` should be a no-op when not active.
670
+ return fn;
671
+ }
672
+
673
+ return async function wrappedLambdaHandler(event, context, callback) {
674
+ if (!(event && context && typeof callback === 'function')) {
675
+ // Skip instrumentation if arguments are unexpected.
676
+ // https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html
677
+ return fn.call(this, ...arguments);
678
+ }
679
+ log.trace({ awsRequestId: context.awsRequestId }, 'lambda: fn start');
680
+
681
+ const isColdStart = isFirstRun;
682
+ if (isFirstRun) {
683
+ isFirstRun = false;
684
+
685
+ // E.g. 'arn:aws:lambda:us-west-2:123456789012:function:my-function:someAlias'
686
+ const arnParts = context.invokedFunctionArn.split(':');
687
+ gFaasId = arnParts.slice(0, 7).join(':');
688
+ const cloudAccountId = arnParts[4];
689
+
690
+ if (agent._apmClient) {
691
+ log.trace(
692
+ { awsRequestId: context.awsRequestId },
693
+ 'lambda: setExtraMetadata',
694
+ );
695
+ agent._apmClient.setExtraMetadata(getMetadata(agent, cloudAccountId));
696
+ }
697
+ }
698
+
699
+ if (agent._apmClient) {
700
+ agent._apmClient.lambdaStart();
701
+ }
702
+
703
+ const triggerType = triggerTypeFromEvent(event);
704
+
705
+ // Look for trace-context info in headers or messageAttributes.
706
+ let traceparent;
707
+ let tracestate;
708
+ if (
709
+ (triggerType === TRIGGER_API_GATEWAY || triggerType === TRIGGER_ELB) &&
710
+ event.headers
711
+ ) {
712
+ // https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
713
+ // says "Header names are lowercased." However, that isn't the case for
714
+ // payload format version 1.0. We need lowercased headers for processing.
715
+ if (!event.requestContext.http) {
716
+ // 1.0
717
+ event.normedHeaders = lowerCaseObjectKeys(event.headers);
718
+ } else {
719
+ event.normedHeaders = event.headers;
720
+ }
721
+ traceparent =
722
+ event.normedHeaders.traceparent ||
723
+ event.normedHeaders['elastic-apm-traceparent'];
724
+ tracestate = event.normedHeaders.tracestate;
725
+ }
726
+
727
+ // Start the transaction and set some possibly trigger-specific data.
728
+ const trans = agent.startTransaction(context.functionName, type, {
729
+ childOf: traceparent,
730
+ tracestate,
731
+ });
732
+ switch (triggerType) {
733
+ case TRIGGER_API_GATEWAY:
734
+ setApiGatewayData(agent, trans, event, context, gFaasId, isColdStart);
735
+ break;
736
+ case TRIGGER_ELB:
737
+ setElbData(agent, trans, event, context, gFaasId, isColdStart);
738
+ break;
739
+ case TRIGGER_SQS:
740
+ setSqsData(agent, trans, event, context, gFaasId, isColdStart);
741
+ break;
742
+ case TRIGGER_SNS:
743
+ setSnsData(agent, trans, event, context, gFaasId, isColdStart);
744
+ break;
745
+ case TRIGGER_S3_SINGLE_EVENT:
746
+ setS3SingleData(trans, event, context, gFaasId, isColdStart);
747
+ break;
748
+ case TRIGGER_GENERIC:
749
+ setGenericData(trans, event, context, gFaasId, isColdStart);
750
+ break;
751
+ default:
752
+ log.warn(
753
+ `not setting transaction data for triggerType=${triggerType}`,
754
+ );
755
+ }
756
+
757
+ // Wrap context and callback to finish and send transaction.
758
+ // Note: Wrapping context needs to happen *before any `await` calls* in
759
+ // this function, otherwise the Lambda Node.js Runtime will call the
760
+ // *unwrapped* `context.{succeed,fail,done}()` methods.
761
+ const transRunContext = ins.currRunContext();
762
+ wrapContext(transRunContext, trans, event, context, triggerType);
763
+ const wrappedCallback = wrapLambdaCallback(
764
+ transRunContext,
765
+ trans,
766
+ event,
767
+ context,
768
+ triggerType,
769
+ callback,
770
+ );
771
+
772
+ await registerTransaction(trans, context.awsRequestId);
773
+
774
+ try {
775
+ const retval = ins.withRunContext(
776
+ transRunContext,
777
+ fn,
778
+ this,
779
+ event,
780
+ context,
781
+ wrappedCallback,
782
+ );
783
+ if (retval instanceof Promise) {
784
+ return retval;
785
+ } else {
786
+ // In this case, our wrapping of the user's handler has changed it
787
+ // from a sync function to an async function. We need to ensure the
788
+ // Lambda Runtime does not end the invocation based on this returned
789
+ // promise -- the invocation should end when the `callback` is called
790
+ // -- so we return a promise that never resolves.
791
+ return new Promise((resolve, reject) => {
792
+ /* never resolves */
793
+ });
794
+ }
795
+ } catch (handlerErr) {
796
+ wrappedCallback(handlerErr);
797
+ // Return a promise that never resolves, so that the Lambda Runtime's
798
+ // doesn't attempt its "success" handling.
799
+ return new Promise((resolve, reject) => {
800
+ /* never resolves */
801
+ });
802
+ }
803
+ };
804
+ };
805
+ }
806
+
807
+ function isLambdaExecutionEnvironment() {
808
+ return !!process.env.AWS_LAMBDA_FUNCTION_NAME;
809
+ }
810
+
811
+ // Returns the full file path to the user's handler handler module
812
+ //
813
+ // The Lambda Runtime allows a user's handler module to have either a .js or
814
+ // .cjs extension. The getFilePath looks for a .js file first, and if not found
815
+ // presumes a csj file exists. If neither file exists this means the user either
816
+ // as a misconfigured handler (they'd never reach this code) or is using a
817
+ // .mjs file extension (which indicates an ECMAScript/import module, which the
818
+ // agent does not support.
819
+ //
820
+ // TODO: support "extensionless"? per https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/v3.2.1/src/UserFunction.js#L149 Is this for a dir/index.js?
821
+ // TODO: support ESM and .mjs
822
+ //
823
+ // @param {string} taskRoot
824
+ // @param {string} moduleRoot - The subdir under `taskRoot` holding the module.
825
+ // @param {string} module - The module name.
826
+ // @return {string | null}
827
+ function getFilePath(taskRoot, moduleRoot, module) {
828
+ const lambdaStylePath = path.resolve(taskRoot, moduleRoot, module);
829
+ if (fs.existsSync(lambdaStylePath + '.js')) {
830
+ return lambdaStylePath + '.js';
831
+ } else if (fs.existsSync(lambdaStylePath + '.cjs')) {
832
+ return lambdaStylePath + '.cjs';
833
+ } else {
834
+ return null;
835
+ }
836
+ }
837
+
838
+ /**
839
+ * Gather module and export info for the Lambda "handler" string.
840
+ *
841
+ * Compare to the Node.js Lambda runtime's equivalent processing here:
842
+ * https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/v3.2.1/src/UserFunction.js#L288
843
+ *
844
+ * @param {object} env - The process environment.
845
+ * @param {any} [logger] - Optional logger for trace/warn log output.
846
+ */
847
+ function getLambdaHandlerInfo(env, logger) {
848
+ if (
849
+ !isLambdaExecutionEnvironment() ||
850
+ !env._HANDLER ||
851
+ !env.LAMBDA_TASK_ROOT
852
+ ) {
853
+ return null;
854
+ }
855
+
856
+ // Dev Note: This intentionally uses some of the same var names at
857
+ // https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/v3.2.1/src/UserFunction.js#L288
858
+ const fullHandlerString = env._HANDLER;
859
+ const moduleAndHandler = path.basename(fullHandlerString);
860
+ const moduleRoot = fullHandlerString.substring(
861
+ 0,
862
+ fullHandlerString.indexOf(moduleAndHandler),
863
+ );
864
+ const FUNCTION_EXPR = /^([^.]*)\.(.*)$/;
865
+ const match = moduleAndHandler.match(FUNCTION_EXPR);
866
+ if (!match || match.length !== 3) {
867
+ if (logger) {
868
+ logger.warn(
869
+ { fullHandlerString, moduleAndHandler },
870
+ 'Lambda handler string did not match FUNCTION_EXPR',
871
+ );
872
+ }
873
+ return null;
874
+ }
875
+ const module = match[1];
876
+ const handlerPath = match[2];
877
+
878
+ const moduleAbsPath = getFilePath(env.LAMBDA_TASK_ROOT, moduleRoot, module);
879
+ if (!moduleAbsPath) {
880
+ if (logger) {
881
+ logger.warn(
882
+ { fullHandlerString, moduleRoot, module },
883
+ 'could not find Lambda handler module file (ESM not yet supported)',
884
+ );
885
+ }
886
+ return null;
887
+ }
888
+
889
+ const lambdaHandlerInfo = {
890
+ filePath: moduleAbsPath,
891
+ modName: module,
892
+ propPath: handlerPath,
893
+ };
894
+ if (logger) {
895
+ logger.trace({ fullHandlerString, lambdaHandlerInfo }, 'lambdaHandlerInfo');
896
+ }
897
+ return lambdaHandlerInfo;
898
+ }
899
+
900
+ function lowerCaseObjectKeys(obj) {
901
+ const lowerCased = {};
902
+ for (const key of Object.keys(obj)) {
903
+ lowerCased[key.toLowerCase()] = obj[key];
904
+ }
905
+ return lowerCased;
906
+ }
907
+
908
+ // Extract span links from up to 1000 messages in this batch.
909
+ // https://github.com/elastic/apm/blob/main/specs/agents/tracing-instrumentation-messaging.md#receiving-trace-context
910
+ //
911
+ // A span link is created from a `traceparent` message attribute in a message.
912
+ // `msg.messageAttributes` is of the form:
913
+ // { <attribute-name>: { DataType: <attr-type>, StringValue: <attr-value>, ... } }
914
+ // For example:
915
+ // { traceparent: { DataType: 'String', StringValue: 'test-traceparent' } }
916
+ function spanLinksFromSqsRecords(records) {
917
+ const links = [];
918
+ const limit = Math.min(
919
+ records.length,
920
+ MAX_MESSAGES_PROCESSED_FOR_TRACE_CONTEXT,
921
+ );
922
+ for (let i = 0; i < limit; i++) {
923
+ const attrs = records[i].messageAttributes;
924
+ if (!attrs) {
925
+ continue;
926
+ }
927
+
928
+ let traceparent;
929
+ const attrNames = Object.keys(attrs);
930
+ for (let j = 0; j < attrNames.length; j++) {
931
+ const attrVal = attrs[attrNames[j]];
932
+ if (attrVal.dataType !== 'String') {
933
+ continue;
934
+ }
935
+ const attrNameLc = attrNames[j].toLowerCase();
936
+ if (attrNameLc === 'traceparent') {
937
+ traceparent = attrVal.stringValue;
938
+ break;
939
+ }
940
+ }
941
+ if (traceparent) {
942
+ links.push({ context: traceparent });
943
+ }
944
+ }
945
+ return links;
946
+ }
947
+
948
+ // Extract span links from up to 1000 messages in this batch.
949
+ // https://github.com/elastic/apm/blob/main/specs/agents/tracing-instrumentation-messaging.md#receiving-trace-context
950
+ //
951
+ // A span link is created from a `traceparent` message attribute in a message.
952
+ // `record.Sns.MessageAttributes` is of the form:
953
+ // { <attribute-name>: { Type: <attr-type>, Value: <attr-value> } }
954
+ // For example:
955
+ // { traceparent: { Type: 'String', Value: 'test-traceparent' } }
956
+ function spanLinksFromSnsRecords(records) {
957
+ const links = [];
958
+ const limit = Math.min(
959
+ records.length,
960
+ MAX_MESSAGES_PROCESSED_FOR_TRACE_CONTEXT,
961
+ );
962
+ for (let i = 0; i < limit; i++) {
963
+ const attrs = records[i].Sns && records[i].Sns.MessageAttributes;
964
+ if (!attrs) {
965
+ continue;
966
+ }
967
+
968
+ let traceparent;
969
+ const attrNames = Object.keys(attrs);
970
+ for (let j = 0; j < attrNames.length; j++) {
971
+ const attrVal = attrs[attrNames[j]];
972
+ if (attrVal.Type !== 'String') {
973
+ continue;
974
+ }
975
+ const attrNameLc = attrNames[j].toLowerCase();
976
+ if (attrNameLc === 'traceparent') {
977
+ traceparent = attrVal.Value;
978
+ break;
979
+ }
980
+ }
981
+ if (traceparent) {
982
+ links.push({ context: traceparent });
983
+ }
984
+ }
985
+ return links;
986
+ }
987
+
988
+ module.exports = {
989
+ isLambdaExecutionEnvironment,
990
+ elasticApmAwsLambda,
991
+ getLambdaHandlerInfo,
992
+ };