@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.
- package/LICENSE +26 -0
- package/NOTICE.md +442 -0
- package/README.md +48 -0
- package/changes.json +78 -0
- package/index.d.ts +398 -0
- package/index.js +11 -0
- package/lib/InflightEventSet.js +53 -0
- package/lib/activation-method.js +119 -0
- package/lib/agent.js +941 -0
- package/lib/apm-client/apm-client.js +313 -0
- package/lib/apm-client/http-apm-client/CHANGELOG.md +271 -0
- package/lib/apm-client/http-apm-client/README.md +485 -0
- package/lib/apm-client/http-apm-client/central-config.js +41 -0
- package/lib/apm-client/http-apm-client/container-info.js +111 -0
- package/lib/apm-client/http-apm-client/detect-hostname.js +96 -0
- package/lib/apm-client/http-apm-client/index.js +1975 -0
- package/lib/apm-client/http-apm-client/logging.js +31 -0
- package/lib/apm-client/http-apm-client/ndjson.js +20 -0
- package/lib/apm-client/http-apm-client/truncate.js +434 -0
- package/lib/apm-client/noop-apm-client.js +73 -0
- package/lib/async-hooks-polyfill.js +58 -0
- package/lib/cloud-metadata/aws.js +175 -0
- package/lib/cloud-metadata/azure.js +123 -0
- package/lib/cloud-metadata/callback-coordination.js +159 -0
- package/lib/cloud-metadata/gcp.js +133 -0
- package/lib/cloud-metadata/index.js +175 -0
- package/lib/config/config.js +458 -0
- package/lib/config/normalizers.js +701 -0
- package/lib/config/schema.js +1007 -0
- package/lib/constants.js +35 -0
- package/lib/errors.js +303 -0
- package/lib/filters/sanitize-field-names.js +69 -0
- package/lib/http-request.js +249 -0
- package/lib/instrumentation/azure-functions.js +519 -0
- package/lib/instrumentation/context.js +56 -0
- package/lib/instrumentation/dropped-spans-stats.js +112 -0
- package/lib/instrumentation/elasticsearch-shared.js +63 -0
- package/lib/instrumentation/express-utils.js +91 -0
- package/lib/instrumentation/generic-span.js +322 -0
- package/lib/instrumentation/http-shared.js +424 -0
- package/lib/instrumentation/ids.js +39 -0
- package/lib/instrumentation/index.js +1127 -0
- package/lib/instrumentation/modules/@apollo/server.js +30 -0
- package/lib/instrumentation/modules/@aws-sdk/client-dynamodb.js +143 -0
- package/lib/instrumentation/modules/@aws-sdk/client-s3.js +230 -0
- package/lib/instrumentation/modules/@aws-sdk/client-sns.js +197 -0
- package/lib/instrumentation/modules/@aws-sdk/client-sqs.js +336 -0
- package/lib/instrumentation/modules/@elastic/elasticsearch.js +343 -0
- package/lib/instrumentation/modules/@hapi/hapi.js +221 -0
- package/lib/instrumentation/modules/@opentelemetry/api.js +86 -0
- package/lib/instrumentation/modules/@opentelemetry/sdk-metrics.js +79 -0
- package/lib/instrumentation/modules/@redis/client/dist/lib/client/commands-queue.js +178 -0
- package/lib/instrumentation/modules/@redis/client/dist/lib/client/index.js +49 -0
- package/lib/instrumentation/modules/@smithy/smithy-client.js +198 -0
- package/lib/instrumentation/modules/_lambda-handler.js +40 -0
- package/lib/instrumentation/modules/apollo-server-core.js +49 -0
- package/lib/instrumentation/modules/aws-sdk/dynamodb.js +155 -0
- package/lib/instrumentation/modules/aws-sdk/s3.js +184 -0
- package/lib/instrumentation/modules/aws-sdk/sns.js +232 -0
- package/lib/instrumentation/modules/aws-sdk/sqs.js +361 -0
- package/lib/instrumentation/modules/aws-sdk.js +76 -0
- package/lib/instrumentation/modules/bluebird.js +93 -0
- package/lib/instrumentation/modules/cassandra-driver.js +280 -0
- package/lib/instrumentation/modules/elasticsearch.js +191 -0
- package/lib/instrumentation/modules/express-graphql.js +66 -0
- package/lib/instrumentation/modules/express-queue.js +28 -0
- package/lib/instrumentation/modules/express.js +162 -0
- package/lib/instrumentation/modules/fastify.js +172 -0
- package/lib/instrumentation/modules/finalhandler.js +41 -0
- package/lib/instrumentation/modules/generic-pool.js +85 -0
- package/lib/instrumentation/modules/graphql.js +256 -0
- package/lib/instrumentation/modules/handlebars.js +22 -0
- package/lib/instrumentation/modules/http.js +112 -0
- package/lib/instrumentation/modules/http2.js +320 -0
- package/lib/instrumentation/modules/https.js +68 -0
- package/lib/instrumentation/modules/ioredis.js +94 -0
- package/lib/instrumentation/modules/jade.js +18 -0
- package/lib/instrumentation/modules/kafkajs.js +476 -0
- package/lib/instrumentation/modules/knex.js +91 -0
- package/lib/instrumentation/modules/koa-router.js +74 -0
- package/lib/instrumentation/modules/koa.js +15 -0
- package/lib/instrumentation/modules/memcached.js +99 -0
- package/lib/instrumentation/modules/mimic-response.js +45 -0
- package/lib/instrumentation/modules/mongodb/lib/cmap/connection_pool.js +40 -0
- package/lib/instrumentation/modules/mongodb-core.js +206 -0
- package/lib/instrumentation/modules/mongodb.js +259 -0
- package/lib/instrumentation/modules/mysql.js +200 -0
- package/lib/instrumentation/modules/mysql2.js +140 -0
- package/lib/instrumentation/modules/pg.js +148 -0
- package/lib/instrumentation/modules/pug.js +18 -0
- package/lib/instrumentation/modules/redis.js +176 -0
- package/lib/instrumentation/modules/restify.js +52 -0
- package/lib/instrumentation/modules/tedious.js +159 -0
- package/lib/instrumentation/modules/undici.js +270 -0
- package/lib/instrumentation/modules/ws.js +59 -0
- package/lib/instrumentation/noop-transaction.js +81 -0
- package/lib/instrumentation/run-context/AbstractRunContextManager.js +215 -0
- package/lib/instrumentation/run-context/AsyncHooksRunContextManager.js +106 -0
- package/lib/instrumentation/run-context/AsyncLocalStorageRunContextManager.js +73 -0
- package/lib/instrumentation/run-context/BasicRunContextManager.js +82 -0
- package/lib/instrumentation/run-context/RunContext.js +151 -0
- package/lib/instrumentation/run-context/index.js +23 -0
- package/lib/instrumentation/shimmer.js +123 -0
- package/lib/instrumentation/span-compression.js +239 -0
- package/lib/instrumentation/span.js +621 -0
- package/lib/instrumentation/template-shared.js +43 -0
- package/lib/instrumentation/timer.js +84 -0
- package/lib/instrumentation/transaction.js +571 -0
- package/lib/lambda.js +992 -0
- package/lib/load-source-map.js +100 -0
- package/lib/logging.js +212 -0
- package/lib/metrics/index.js +92 -0
- package/lib/metrics/platforms/generic/index.js +40 -0
- package/lib/metrics/platforms/generic/process-cpu.js +22 -0
- package/lib/metrics/platforms/generic/process-top.js +157 -0
- package/lib/metrics/platforms/generic/stats.js +34 -0
- package/lib/metrics/platforms/generic/system-cpu.js +51 -0
- package/lib/metrics/platforms/linux/index.js +19 -0
- package/lib/metrics/platforms/linux/stats.js +213 -0
- package/lib/metrics/queue.js +90 -0
- package/lib/metrics/registry.js +52 -0
- package/lib/metrics/reporter.js +119 -0
- package/lib/metrics/runtime.js +77 -0
- package/lib/middleware/connect.js +16 -0
- package/lib/opentelemetry-bridge/OTelBridgeNonRecordingSpan.js +150 -0
- package/lib/opentelemetry-bridge/OTelBridgeRunContext.js +124 -0
- package/lib/opentelemetry-bridge/OTelContextManager.js +82 -0
- package/lib/opentelemetry-bridge/OTelSpan.js +344 -0
- package/lib/opentelemetry-bridge/OTelTracer.js +201 -0
- package/lib/opentelemetry-bridge/OTelTracerProvider.js +25 -0
- package/lib/opentelemetry-bridge/README.md +244 -0
- package/lib/opentelemetry-bridge/index.js +15 -0
- package/lib/opentelemetry-bridge/oblog.js +23 -0
- package/lib/opentelemetry-bridge/opentelemetry-core-mini/README.md +3 -0
- package/lib/opentelemetry-bridge/opentelemetry-core-mini/internal/validators.js +52 -0
- package/lib/opentelemetry-bridge/opentelemetry-core-mini/trace/TraceState.js +109 -0
- package/lib/opentelemetry-bridge/otelutils.js +99 -0
- package/lib/opentelemetry-bridge/setup.js +76 -0
- package/lib/opentelemetry-metrics/ElasticApmMetricExporter.js +285 -0
- package/lib/opentelemetry-metrics/index.js +50 -0
- package/lib/parsers.js +225 -0
- package/lib/propwrap.js +147 -0
- package/lib/stacktraces.js +537 -0
- package/lib/symbols.js +15 -0
- package/lib/tracecontext/index.js +118 -0
- package/lib/tracecontext/traceparent.js +185 -0
- package/lib/tracecontext/tracestate.js +388 -0
- package/lib/wildcard-matcher.js +52 -0
- package/loader.mjs +7 -0
- package/package.json +299 -0
- package/start.d.ts +8 -0
- package/start.js +29 -0
- package/types/aws-lambda.d.ts +98 -0
- package/types/connect.d.ts +23 -0
|
@@ -0,0 +1,519 @@
|
|
|
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
|
+
// Instrumentation of Azure Functions.
|
|
10
|
+
// Spec: https://github.com/elastic/apm/blob/main/specs/agents/tracing-instrumentation-azure-functions.md
|
|
11
|
+
//
|
|
12
|
+
// This instrumentation is started if the `FUNCTIONS_WORKER_RUNTIME` envvar
|
|
13
|
+
// indicates we are in an Azure Functions environment. This is different from
|
|
14
|
+
// most instrumentations that hook into user code `require()`ing a particular
|
|
15
|
+
// module.
|
|
16
|
+
//
|
|
17
|
+
// The azure-functions-nodejs-worker repo holds the "nodejsWorker.js" process
|
|
18
|
+
// code in which user Functions are executed. That repo monkey-patches
|
|
19
|
+
// `Module.prototype.require` to inject a virtual `@azure/functions-core`
|
|
20
|
+
// module which exposes a hooks mechanism for invocation start and end. See
|
|
21
|
+
// https://github.com/Azure/azure-functions-nodejs-worker/blob/v3.5.2/src/setupCoreModule.ts#L20-L54
|
|
22
|
+
// and `registerHook` usage below.
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
const constants = require('../constants');
|
|
28
|
+
|
|
29
|
+
let isInstrumented = false;
|
|
30
|
+
let hookDisposables = []; // This holds the `Disposable` objects with which to remove previously registered @azure/functions-core hooks.
|
|
31
|
+
|
|
32
|
+
// https://github.com/elastic/apm/blob/main/specs/agents/tracing-instrumentation-azure-functions.md#deriving-cold-starts
|
|
33
|
+
let isFirstRun = true;
|
|
34
|
+
|
|
35
|
+
// The trigger types for which we support special handling.
|
|
36
|
+
const TRIGGER_OTHER = 1; //
|
|
37
|
+
const TRIGGER_HTTP = 2; // https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook
|
|
38
|
+
const TRIGGER_TIMER = 3; // https://learn.microsoft.com/en-ca/azure/azure-functions/functions-bindings-timer
|
|
39
|
+
|
|
40
|
+
const TRANS_TYPE_FROM_TRIGGER_TYPE = {
|
|
41
|
+
[TRIGGER_OTHER]: 'request',
|
|
42
|
+
[TRIGGER_HTTP]: 'request',
|
|
43
|
+
// Note: `transaction.type = "scheduled"` is not in the shared APM agent spec,
|
|
44
|
+
// but the Java agent used the same value for some instrumentations.
|
|
45
|
+
[TRIGGER_TIMER]: 'scheduled',
|
|
46
|
+
};
|
|
47
|
+
// See APM spec and OTel `faas.trigger` at
|
|
48
|
+
// https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/faas/
|
|
49
|
+
const FAAS_TRIGGER_TYPE_FROM_TRIGGER_TYPE = {
|
|
50
|
+
[TRIGGER_OTHER]: 'other',
|
|
51
|
+
[TRIGGER_HTTP]: 'http',
|
|
52
|
+
// Note: `faas.trigger = "timer"` is not in the shared APM agent spec yet.
|
|
53
|
+
[TRIGGER_TIMER]: 'timer',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const gHttpRouteFromFuncDir = new Map();
|
|
57
|
+
const DEFAULT_ROUTE_PREFIX = 'api';
|
|
58
|
+
let gRoutePrefix = null;
|
|
59
|
+
|
|
60
|
+
// Mimic a subset of `FunctionInfo` from Azure code
|
|
61
|
+
// https://github.com/Azure/azure-functions-nodejs-library/blob/v3.5.0/src/FunctionInfo.ts
|
|
62
|
+
// to help with handling.
|
|
63
|
+
// ...plus some additional functionality for `httpRoute` and `routePrefix`.
|
|
64
|
+
class FunctionInfo {
|
|
65
|
+
constructor(bindingDefinitions, executionContext, log) {
|
|
66
|
+
// Example `bindingDefinitions`:
|
|
67
|
+
// [{"name":"req","type":"httpTrigger","direction":"in"},
|
|
68
|
+
// {"name":"res","type":"http","direction":"out"}]
|
|
69
|
+
this.triggerType = TRIGGER_OTHER;
|
|
70
|
+
this.httpOutputName = '';
|
|
71
|
+
this.hasHttpTrigger = false;
|
|
72
|
+
this.hasReturnBinding = false;
|
|
73
|
+
this.outputBindingNames = [];
|
|
74
|
+
for (const bd of bindingDefinitions) {
|
|
75
|
+
if (bd.direction !== 'in') {
|
|
76
|
+
if (bd.type && bd.type.toLowerCase() === 'http') {
|
|
77
|
+
this.httpOutputName = bd.name;
|
|
78
|
+
}
|
|
79
|
+
this.outputBindingNames.push(bd.name);
|
|
80
|
+
if (bd.name === '$return') {
|
|
81
|
+
this.hasReturnBinding = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (bd.type) {
|
|
85
|
+
const typeLc = bd.type.toLowerCase();
|
|
86
|
+
switch (typeLc) {
|
|
87
|
+
case 'httptrigger': // "type": "httpTrigger"
|
|
88
|
+
this.triggerType = TRIGGER_HTTP;
|
|
89
|
+
break;
|
|
90
|
+
case 'timertrigger':
|
|
91
|
+
this.triggerType = TRIGGER_TIMER;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// If this is an HTTP triggered-function, then get its route template and
|
|
98
|
+
// route prefix.
|
|
99
|
+
// https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger#customize-the-http-endpoint
|
|
100
|
+
// A possible custom "route" is not included in the given context, so we
|
|
101
|
+
// attempt to load the "function.json" file. A possible custom route prefix
|
|
102
|
+
// is in "host.json".
|
|
103
|
+
this.httpRoute = null;
|
|
104
|
+
this.routePrefix = null;
|
|
105
|
+
if (this.triggerType === TRIGGER_HTTP) {
|
|
106
|
+
const funcDir = executionContext.functionDirectory;
|
|
107
|
+
if (!funcDir) {
|
|
108
|
+
this.httpRoute = executionContext.functionName;
|
|
109
|
+
} else if (gHttpRouteFromFuncDir.has(funcDir)) {
|
|
110
|
+
this.httpRoute = gHttpRouteFromFuncDir.get(funcDir);
|
|
111
|
+
} else {
|
|
112
|
+
try {
|
|
113
|
+
const fj = JSON.parse(
|
|
114
|
+
fs.readFileSync(path.join(funcDir, 'function.json')),
|
|
115
|
+
);
|
|
116
|
+
for (let i = 0; i < fj.bindings.length; i++) {
|
|
117
|
+
const binding = fj.bindings[i];
|
|
118
|
+
if (
|
|
119
|
+
binding.direction === 'in' &&
|
|
120
|
+
binding.type &&
|
|
121
|
+
binding.type.toLowerCase() === 'httptrigger'
|
|
122
|
+
) {
|
|
123
|
+
if (binding.route !== undefined) {
|
|
124
|
+
this.httpRoute = binding.route;
|
|
125
|
+
} else {
|
|
126
|
+
this.httpRoute = executionContext.functionName;
|
|
127
|
+
}
|
|
128
|
+
gHttpRouteFromFuncDir.set(funcDir, this.httpRoute);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
log.trace(
|
|
132
|
+
{ funcDir, httpRoute: this.httpRoute },
|
|
133
|
+
'azure-functions: loaded route',
|
|
134
|
+
);
|
|
135
|
+
} catch (httpRouteErr) {
|
|
136
|
+
log.debug(
|
|
137
|
+
'azure-functions: could not determine httpRoute for function %s: %s',
|
|
138
|
+
executionContext.functionName,
|
|
139
|
+
httpRouteErr.message,
|
|
140
|
+
);
|
|
141
|
+
this.httpRoute = executionContext.functionName;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (gRoutePrefix) {
|
|
146
|
+
this.routePrefix = gRoutePrefix;
|
|
147
|
+
} else if (!funcDir) {
|
|
148
|
+
this.routePrefix = gRoutePrefix = DEFAULT_ROUTE_PREFIX;
|
|
149
|
+
} else {
|
|
150
|
+
try {
|
|
151
|
+
const hj = JSON.parse(
|
|
152
|
+
fs.readFileSync(path.join(path.dirname(funcDir), 'host.json')),
|
|
153
|
+
);
|
|
154
|
+
if (
|
|
155
|
+
hj &&
|
|
156
|
+
hj.extensions &&
|
|
157
|
+
hj.extensions.http &&
|
|
158
|
+
hj.extensions.http.routePrefix !== undefined
|
|
159
|
+
) {
|
|
160
|
+
const rawRoutePrefix = hj.extensions.http.routePrefix;
|
|
161
|
+
this.routePrefix = gRoutePrefix = normRoutePrefix(rawRoutePrefix);
|
|
162
|
+
log.trace(
|
|
163
|
+
{ hj, routePrefix: this.routePrefix, rawRoutePrefix },
|
|
164
|
+
'azure-functions: loaded route prefix',
|
|
165
|
+
);
|
|
166
|
+
} else {
|
|
167
|
+
this.routePrefix = gRoutePrefix = DEFAULT_ROUTE_PREFIX;
|
|
168
|
+
}
|
|
169
|
+
} catch (routePrefixErr) {
|
|
170
|
+
log.debug(
|
|
171
|
+
'azure-functions: could not determine routePrefix: %s',
|
|
172
|
+
routePrefixErr.message,
|
|
173
|
+
);
|
|
174
|
+
this.routePrefix = gRoutePrefix = DEFAULT_ROUTE_PREFIX;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Normalize a routePrefix to *not* have a leading slash.
|
|
182
|
+
//
|
|
183
|
+
// Given routePrefix='/foo' and functionName='MyFn', Microsoft.AspNetCore.Routing
|
|
184
|
+
// will create a route `//foo/MyFn`. Actual HTTP requests to `GET /foo/MyFn`,
|
|
185
|
+
// `GET //foo/MyFn`, and any number of leading slashes will work. So let's
|
|
186
|
+
// settle on the more typical single leading slash.
|
|
187
|
+
function normRoutePrefix(routePrefix) {
|
|
188
|
+
return routePrefix.startsWith('/') ? routePrefix.slice(1) : routePrefix;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Set transaction data for HTTP triggers from the Lambda function result.
|
|
193
|
+
*/
|
|
194
|
+
function setTransDataFromHttpTriggerResult(trans, hookCtx) {
|
|
195
|
+
if (hookCtx.error) {
|
|
196
|
+
trans.setOutcome(constants.OUTCOME_FAILURE);
|
|
197
|
+
trans.result = 'HTTP 5xx';
|
|
198
|
+
trans.res = {
|
|
199
|
+
statusCode: 500,
|
|
200
|
+
};
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Attempt to get what the Azure Functions system will use for the HTTP response
|
|
205
|
+
// data. This is a pain because Azure Functions supports a number of different
|
|
206
|
+
// ways the user can return a response. Part of the handling for this is:
|
|
207
|
+
// https://github.com/Azure/azure-functions-nodejs-library/blob/v3.5.0/src/InvocationModel.ts#L77-L144
|
|
208
|
+
const funcInfo = hookCtx.hookData.funcInfo;
|
|
209
|
+
const result = hookCtx.result;
|
|
210
|
+
const context = hookCtx.invocationContext;
|
|
211
|
+
let httpRes;
|
|
212
|
+
if (funcInfo.hasReturnBinding) {
|
|
213
|
+
httpRes = hookCtx.result;
|
|
214
|
+
} else {
|
|
215
|
+
if (
|
|
216
|
+
result &&
|
|
217
|
+
typeof result === 'object' &&
|
|
218
|
+
result[funcInfo.httpOutputName] !== undefined
|
|
219
|
+
) {
|
|
220
|
+
httpRes = result[funcInfo.httpOutputName];
|
|
221
|
+
} else if (
|
|
222
|
+
context.bindings &&
|
|
223
|
+
context.bindings[funcInfo.httpOutputName] !== undefined
|
|
224
|
+
) {
|
|
225
|
+
httpRes = context.bindings[funcInfo.httpOutputName];
|
|
226
|
+
} else if (context.res !== undefined) {
|
|
227
|
+
httpRes = context.res;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Azure Functions requires that the HTTP output response value be an 'object',
|
|
232
|
+
// otherwise it errors out the response (statusCode=500) and logs an error:
|
|
233
|
+
// Stack: Error: The HTTP response must be an 'object' type that can include properties such as 'body', 'status', and 'headers'. Learn more: https://go.microsoft.com/fwlink/?linkid=2112563
|
|
234
|
+
if (typeof httpRes !== 'object') {
|
|
235
|
+
trans.setOutcome(constants.OUTCOME_FAILURE);
|
|
236
|
+
trans.result = 'HTTP 5xx';
|
|
237
|
+
trans.res = {
|
|
238
|
+
statusCode: 500,
|
|
239
|
+
};
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let statusCode = Number(httpRes.status);
|
|
244
|
+
if (!Number.isInteger(statusCode)) {
|
|
245
|
+
// While https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger
|
|
246
|
+
// suggests the default may be "HTTP 204 No Content", my observation is that
|
|
247
|
+
// 200 is the actual default.
|
|
248
|
+
statusCode = 200;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (statusCode < 500) {
|
|
252
|
+
trans.setOutcome(constants.OUTCOME_SUCCESS);
|
|
253
|
+
} else {
|
|
254
|
+
trans.setOutcome(constants.OUTCOME_FAILURE);
|
|
255
|
+
}
|
|
256
|
+
trans.result = 'HTTP ' + statusCode.toString()[0] + 'xx';
|
|
257
|
+
trans.res = {
|
|
258
|
+
statusCode,
|
|
259
|
+
body: httpRes.body,
|
|
260
|
+
};
|
|
261
|
+
if (httpRes.headers && typeof httpRes.headers === 'object') {
|
|
262
|
+
trans.res.headers = httpRes.headers;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// The Azure account id is also called the "subscription GUID".
|
|
267
|
+
// https://learn.microsoft.com/en-us/azure/app-service/reference-app-settings#app-environment
|
|
268
|
+
function getAzureAccountId() {
|
|
269
|
+
return (
|
|
270
|
+
process.env.WEBSITE_OWNER_NAME &&
|
|
271
|
+
process.env.WEBSITE_OWNER_NAME.split('+', 1)[0]
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ---- exports
|
|
276
|
+
|
|
277
|
+
const isAzureFunctionsEnvironment = !!process.env.FUNCTIONS_WORKER_RUNTIME;
|
|
278
|
+
|
|
279
|
+
// Gather APM metadata for this Azure Function instance per
|
|
280
|
+
// https://github.com/elastic/apm/blob/main/specs/agents/tracing-instrumentation-azure-functions.md#metadata
|
|
281
|
+
function getAzureFunctionsExtraMetadata() {
|
|
282
|
+
const metadata = {
|
|
283
|
+
service: {
|
|
284
|
+
framework: {
|
|
285
|
+
// Passing this service.framework.name to Client#setExtraMetadata()
|
|
286
|
+
// ensures that it "wins" over a framework name from
|
|
287
|
+
// `agent.setFramework()`, because in the client `_extraMetadata`
|
|
288
|
+
// wins over `_conf.frameworkName`.
|
|
289
|
+
name: 'Azure Functions',
|
|
290
|
+
version: process.env.FUNCTIONS_EXTENSION_VERSION,
|
|
291
|
+
},
|
|
292
|
+
runtime: {
|
|
293
|
+
name: process.env.FUNCTIONS_WORKER_RUNTIME,
|
|
294
|
+
},
|
|
295
|
+
node: {
|
|
296
|
+
configured_name: process.env.WEBSITE_INSTANCE_ID,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
// https://github.com/elastic/apm/blob/main/specs/agents/metadata.md#azure-functions
|
|
300
|
+
cloud: {
|
|
301
|
+
provider: 'azure',
|
|
302
|
+
region: process.env.REGION_NAME,
|
|
303
|
+
service: {
|
|
304
|
+
name: 'functions',
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
const accountId = getAzureAccountId();
|
|
309
|
+
if (accountId) {
|
|
310
|
+
metadata.cloud.account = { id: accountId };
|
|
311
|
+
}
|
|
312
|
+
if (process.env.WEBSITE_SITE_NAME) {
|
|
313
|
+
metadata.cloud.instance = { name: process.env.WEBSITE_SITE_NAME };
|
|
314
|
+
}
|
|
315
|
+
if (process.env.WEBSITE_RESOURCE_GROUP) {
|
|
316
|
+
metadata.cloud.project = { name: process.env.WEBSITE_RESOURCE_GROUP };
|
|
317
|
+
}
|
|
318
|
+
return metadata;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function instrument(agent) {
|
|
322
|
+
if (isInstrumented) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
isInstrumented = true;
|
|
326
|
+
|
|
327
|
+
const ins = agent._instrumentation;
|
|
328
|
+
const log = agent.logger;
|
|
329
|
+
let d;
|
|
330
|
+
|
|
331
|
+
let core;
|
|
332
|
+
try {
|
|
333
|
+
core = require('@azure/functions-core');
|
|
334
|
+
} catch (err) {
|
|
335
|
+
log.warn(
|
|
336
|
+
{ err },
|
|
337
|
+
'could not import "@azure/functions-core": skipping Azure Functions instrumentation',
|
|
338
|
+
);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Note: We *could* hook into 'appTerminate' to attempt a quick flush of the
|
|
343
|
+
// current intake request. However, I have not seen a need for it yet.
|
|
344
|
+
// d = core.registerHook('appTerminate', async (hookCtx) => {
|
|
345
|
+
// log.trace('azure-functions: appTerminate')
|
|
346
|
+
// // flush here ...
|
|
347
|
+
// })
|
|
348
|
+
// hookDisposables.push(d)
|
|
349
|
+
|
|
350
|
+
// See examples at https://github.com/Azure/azure-functions-nodejs-worker/issues/522
|
|
351
|
+
d = core.registerHook('preInvocation', (hookCtx) => {
|
|
352
|
+
if (!hookCtx.invocationContext) {
|
|
353
|
+
// Doesn't look like `require('@azure/functions-core').PreInvocationContext`. Abort.
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const context = hookCtx.invocationContext;
|
|
358
|
+
const invocationId = context.invocationId;
|
|
359
|
+
log.trace({ invocationId }, 'azure-functions: preInvocation');
|
|
360
|
+
|
|
361
|
+
const isColdStart = isFirstRun;
|
|
362
|
+
if (isFirstRun) {
|
|
363
|
+
isFirstRun = false;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// In programming model v3 the InvocationContext includes
|
|
367
|
+
// `bindingDefinitions` and `executionContext`. In v4 the structure is a
|
|
368
|
+
// little different.
|
|
369
|
+
let bindingDefinitions = context.bindingDefinitions;
|
|
370
|
+
if (!bindingDefinitions) {
|
|
371
|
+
bindingDefinitions = [];
|
|
372
|
+
// Input bindings
|
|
373
|
+
bindingDefinitions.push({
|
|
374
|
+
name: context?.options?.trigger?.name,
|
|
375
|
+
type: context?.options?.trigger?.type,
|
|
376
|
+
direction: context?.options?.trigger?.direction,
|
|
377
|
+
});
|
|
378
|
+
// Output bindings
|
|
379
|
+
if (context?.options?.return) {
|
|
380
|
+
bindingDefinitions.push(context?.options?.return);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
let executionContext = context.executionContext;
|
|
384
|
+
if (!executionContext) {
|
|
385
|
+
executionContext = {
|
|
386
|
+
functionDirectory: '',
|
|
387
|
+
functionName: context.functionName,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const funcInfo = (hookCtx.hookData.funcInfo = new FunctionInfo(
|
|
392
|
+
bindingDefinitions,
|
|
393
|
+
executionContext,
|
|
394
|
+
log,
|
|
395
|
+
));
|
|
396
|
+
const triggerType = funcInfo.triggerType;
|
|
397
|
+
|
|
398
|
+
// `InvocationContext.traceContext` is broken: it results in sampled=false
|
|
399
|
+
// (i.e. traces are discarded) and/or broken traces because it creates an
|
|
400
|
+
// internal Span ID in the trace that cannot be ingested.
|
|
401
|
+
// See this for full explanation:
|
|
402
|
+
// https://github.com/elastic/apm-agent-nodejs/pull/4426#issuecomment-2596922653
|
|
403
|
+
let traceparent;
|
|
404
|
+
let tracestate;
|
|
405
|
+
if (triggerType === TRIGGER_HTTP && context?.req?.headers?.traceparent) {
|
|
406
|
+
traceparent = context.req.headers.traceparent;
|
|
407
|
+
tracestate = context.req.headers.tracestate;
|
|
408
|
+
log.trace(
|
|
409
|
+
{ traceparent, tracestate },
|
|
410
|
+
'azure-functions: get trace-context from HTTP trigger request headers',
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const trans = (hookCtx.hookData.trans = ins.startTransaction(
|
|
415
|
+
// This is the default name. Trigger-specific values are added below.
|
|
416
|
+
executionContext.functionName,
|
|
417
|
+
TRANS_TYPE_FROM_TRIGGER_TYPE[triggerType],
|
|
418
|
+
{
|
|
419
|
+
childOf: traceparent,
|
|
420
|
+
tracestate,
|
|
421
|
+
},
|
|
422
|
+
));
|
|
423
|
+
|
|
424
|
+
// Expected env vars are documented at:
|
|
425
|
+
// https://learn.microsoft.com/en-us/azure/app-service/reference-app-settings
|
|
426
|
+
const accountId = getAzureAccountId();
|
|
427
|
+
const resourceGroup = process.env.WEBSITE_RESOURCE_GROUP;
|
|
428
|
+
const fnAppName = process.env.WEBSITE_SITE_NAME;
|
|
429
|
+
const fnName = executionContext.functionName;
|
|
430
|
+
const faasData = {
|
|
431
|
+
trigger: {
|
|
432
|
+
type: FAAS_TRIGGER_TYPE_FROM_TRIGGER_TYPE[triggerType],
|
|
433
|
+
},
|
|
434
|
+
execution: invocationId,
|
|
435
|
+
coldstart: isColdStart,
|
|
436
|
+
};
|
|
437
|
+
if (accountId && resourceGroup && fnAppName) {
|
|
438
|
+
faasData.id = `/subscriptions/${accountId}/resourceGroups/${resourceGroup}/providers/Microsoft.Web/sites/${fnAppName}/functions/${fnName}`;
|
|
439
|
+
}
|
|
440
|
+
if (fnAppName && fnName) {
|
|
441
|
+
faasData.name = `${fnAppName}/${fnName}`;
|
|
442
|
+
}
|
|
443
|
+
trans.setFaas(faasData);
|
|
444
|
+
|
|
445
|
+
if (triggerType === TRIGGER_HTTP) {
|
|
446
|
+
// The request object is the first item in `hookCtx.inputs`. See:
|
|
447
|
+
// https://github.com/Azure/azure-functions-nodejs-worker/blob/v3.5.2/src/eventHandlers/InvocationHandler.ts#L127
|
|
448
|
+
const req = hookCtx.inputs[0];
|
|
449
|
+
if (req) {
|
|
450
|
+
trans.req = req; // Used for setting `trans.context.request` by `getContextFromRequest()`.
|
|
451
|
+
if (agent._conf.usePathAsTransactionName && req.url) {
|
|
452
|
+
trans.setDefaultName(`${req.method} ${new URL(req.url).pathname}`);
|
|
453
|
+
} else {
|
|
454
|
+
const route = funcInfo.routePrefix
|
|
455
|
+
? `/${funcInfo.routePrefix}/${funcInfo.httpRoute}`
|
|
456
|
+
: `/${funcInfo.httpRoute}`;
|
|
457
|
+
trans.setDefaultName(`${req.method} ${route}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
hookDisposables.push(d);
|
|
463
|
+
|
|
464
|
+
d = core.registerHook('postInvocation', (hookCtx) => {
|
|
465
|
+
if (!hookCtx.invocationContext) {
|
|
466
|
+
// Doesn't look like `require('@azure/functions-core').PreInvocationContext`. Abort.
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const invocationId = hookCtx.invocationContext.invocationId;
|
|
470
|
+
log.trace({ invocationId }, 'azure-functions: postInvocation');
|
|
471
|
+
|
|
472
|
+
const trans = hookCtx.hookData.trans;
|
|
473
|
+
if (!trans) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const funcInfo = hookCtx.hookData.funcInfo;
|
|
478
|
+
if (funcInfo.triggerType === TRIGGER_HTTP) {
|
|
479
|
+
setTransDataFromHttpTriggerResult(trans, hookCtx);
|
|
480
|
+
} else if (hookCtx.error) {
|
|
481
|
+
trans.result = constants.RESULT_FAILURE;
|
|
482
|
+
trans.setOutcome(constants.OUTCOME_FAILURE);
|
|
483
|
+
} else {
|
|
484
|
+
trans.result = constants.RESULT_SUCCESS;
|
|
485
|
+
trans.setOutcome(constants.OUTCOME_SUCCESS);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (hookCtx.error) {
|
|
489
|
+
// Capture the error before trans.end() so it associates with the
|
|
490
|
+
// current trans. `skipOutcome` to avoid setting outcome on a possible
|
|
491
|
+
// currentSpan, because this error applies to the transaction, not any
|
|
492
|
+
// sub-span.
|
|
493
|
+
agent.captureError(hookCtx.error, { skipOutcome: true });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
trans.end();
|
|
497
|
+
});
|
|
498
|
+
hookDisposables.push(d);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function uninstrument() {
|
|
502
|
+
if (!isInstrumented) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
isInstrumented = false;
|
|
506
|
+
|
|
507
|
+
// Unregister `core.registerHook()` calls from above.
|
|
508
|
+
hookDisposables.forEach((d) => {
|
|
509
|
+
d.dispose();
|
|
510
|
+
});
|
|
511
|
+
hookDisposables = [];
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
module.exports = {
|
|
515
|
+
isAzureFunctionsEnvironment,
|
|
516
|
+
getAzureFunctionsExtraMetadata,
|
|
517
|
+
instrument,
|
|
518
|
+
uninstrument,
|
|
519
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
var { parseUrl } = require('../parsers');
|
|
10
|
+
|
|
11
|
+
// Get the port number including the default port for a protocols
|
|
12
|
+
function getPortNumber(port, protocol) {
|
|
13
|
+
if (port === '') {
|
|
14
|
+
port = protocol === 'http:' ? '80' : protocol === 'https:' ? '443' : '';
|
|
15
|
+
}
|
|
16
|
+
return port;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
exports.getHTTPDestination = function (url) {
|
|
20
|
+
const { port, protocol, hostname } = parseUrl(url);
|
|
21
|
+
const portNumber = getPortNumber(port, protocol);
|
|
22
|
+
|
|
23
|
+
// If hostname begins with [ and ends with ], assume that it's an IPv6 address.
|
|
24
|
+
// since address and port are recorded separately, we are recording the
|
|
25
|
+
// info in canonical form without square brackets
|
|
26
|
+
const ipv6Hostname =
|
|
27
|
+
hostname[0] === '[' && hostname[hostname.length - 1] === ']';
|
|
28
|
+
|
|
29
|
+
const address = ipv6Hostname ? hostname.slice(1, -1) : hostname;
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
address,
|
|
33
|
+
port: Number(portNumber),
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
exports.getDBDestination = function (host, port) {
|
|
38
|
+
const destination = {};
|
|
39
|
+
let haveValues = false;
|
|
40
|
+
|
|
41
|
+
if (host) {
|
|
42
|
+
destination.address = host;
|
|
43
|
+
haveValues = true;
|
|
44
|
+
}
|
|
45
|
+
port = Number(port);
|
|
46
|
+
if (port) {
|
|
47
|
+
destination.port = port;
|
|
48
|
+
haveValues = true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (haveValues) {
|
|
52
|
+
return destination;
|
|
53
|
+
} else {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
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 MAX_DROPPED_SPANS_STATS = 128;
|
|
10
|
+
|
|
11
|
+
class DroppedSpansStats {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.statsMap = new Map();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Record this span in dropped spans stats.
|
|
18
|
+
*
|
|
19
|
+
* @param {Span} span
|
|
20
|
+
* @returns {boolean} True iff this span was added to stats. This return value
|
|
21
|
+
* is only used for testing.
|
|
22
|
+
*/
|
|
23
|
+
captureDroppedSpan(span) {
|
|
24
|
+
if (!span) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const serviceTargetType = span._serviceTarget && span._serviceTarget.type;
|
|
29
|
+
const serviceTargetName = span._serviceTarget && span._serviceTarget.name;
|
|
30
|
+
const resource =
|
|
31
|
+
span._destination &&
|
|
32
|
+
span._destination.service &&
|
|
33
|
+
span._destination.service.resource;
|
|
34
|
+
if (
|
|
35
|
+
!span._exitSpan ||
|
|
36
|
+
!(serviceTargetType || serviceTargetName) ||
|
|
37
|
+
!resource
|
|
38
|
+
) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const stats = this._getOrCreateStats(
|
|
43
|
+
serviceTargetType,
|
|
44
|
+
serviceTargetName,
|
|
45
|
+
resource,
|
|
46
|
+
span.outcome,
|
|
47
|
+
);
|
|
48
|
+
if (!stats) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
stats.duration.count++;
|
|
52
|
+
stats.duration.sum.us += span._duration * 1000;
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_getOrCreateStats(serviceTargetType, serviceTargetName, resource, outcome) {
|
|
57
|
+
const key = [serviceTargetType, serviceTargetName, resource, outcome].join(
|
|
58
|
+
'',
|
|
59
|
+
);
|
|
60
|
+
let stats = this.statsMap.get(key);
|
|
61
|
+
if (stats) {
|
|
62
|
+
return stats;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (this.statsMap.size >= MAX_DROPPED_SPANS_STATS) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
stats = {
|
|
70
|
+
duration: {
|
|
71
|
+
count: 0,
|
|
72
|
+
sum: {
|
|
73
|
+
us: 0,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
destination_service_resource: resource,
|
|
77
|
+
outcome,
|
|
78
|
+
};
|
|
79
|
+
if (serviceTargetType) {
|
|
80
|
+
stats.service_target_type = serviceTargetType;
|
|
81
|
+
}
|
|
82
|
+
if (serviceTargetName) {
|
|
83
|
+
stats.service_target_name = serviceTargetName;
|
|
84
|
+
}
|
|
85
|
+
this.statsMap.set(key, stats);
|
|
86
|
+
return stats;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
encode() {
|
|
90
|
+
// `duration.sum.us` is an integer in the intake API, but is stored as
|
|
91
|
+
// a float. We assume this `.encode()` is typically only called when the
|
|
92
|
+
// transaction is ended, so the in-place loss of the fractional value is
|
|
93
|
+
// acceptable.
|
|
94
|
+
const result = [];
|
|
95
|
+
for (const stats of this.statsMap.values()) {
|
|
96
|
+
stats.duration.sum.us = Math.round(stats.duration.sum.us);
|
|
97
|
+
result.push(stats);
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
size() {
|
|
103
|
+
return this.statsMap.size;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
DroppedSpansStats,
|
|
109
|
+
|
|
110
|
+
// Exported for testing-only.
|
|
111
|
+
MAX_DROPPED_SPANS_STATS,
|
|
112
|
+
};
|