in lib/instrumentation/azure-functions.js [321:499]
function instrument(agent) {
if (isInstrumented) {
return;
}
isInstrumented = true;
const ins = agent._instrumentation;
const log = agent.logger;
let d;
let core;
try {
core = require('@azure/functions-core');
} catch (err) {
log.warn(
{ err },
'could not import "@azure/functions-core": skipping Azure Functions instrumentation',
);
return;
}
// Note: We *could* hook into 'appTerminate' to attempt a quick flush of the
// current intake request. However, I have not seen a need for it yet.
// d = core.registerHook('appTerminate', async (hookCtx) => {
// log.trace('azure-functions: appTerminate')
// // flush here ...
// })
// hookDisposables.push(d)
// See examples at https://github.com/Azure/azure-functions-nodejs-worker/issues/522
d = core.registerHook('preInvocation', (hookCtx) => {
if (!hookCtx.invocationContext) {
// Doesn't look like `require('@azure/functions-core').PreInvocationContext`. Abort.
return;
}
const context = hookCtx.invocationContext;
const invocationId = context.invocationId;
log.trace({ invocationId }, 'azure-functions: preInvocation');
const isColdStart = isFirstRun;
if (isFirstRun) {
isFirstRun = false;
}
// In programming model v3 the InvocationContext includes
// `bindingDefinitions` and `executionContext`. In v4 the structure is a
// little different.
let bindingDefinitions = context.bindingDefinitions;
if (!bindingDefinitions) {
bindingDefinitions = [];
// Input bindings
bindingDefinitions.push({
name: context?.options?.trigger?.name,
type: context?.options?.trigger?.type,
direction: context?.options?.trigger?.direction,
});
// Output bindings
if (context?.options?.return) {
bindingDefinitions.push(context?.options?.return);
}
}
let executionContext = context.executionContext;
if (!executionContext) {
executionContext = {
functionDirectory: '',
functionName: context.functionName,
};
}
const funcInfo = (hookCtx.hookData.funcInfo = new FunctionInfo(
bindingDefinitions,
executionContext,
log,
));
const triggerType = funcInfo.triggerType;
// `InvocationContext.traceContext` is broken: it results in sampled=false
// (i.e. traces are discarded) and/or broken traces because it creates an
// internal Span ID in the trace that cannot be ingested.
// See this for full explanation:
// https://github.com/elastic/apm-agent-nodejs/pull/4426#issuecomment-2596922653
let traceparent;
let tracestate;
if (triggerType === TRIGGER_HTTP && context?.req?.headers?.traceparent) {
traceparent = context.req.headers.traceparent;
tracestate = context.req.headers.tracestate;
log.trace(
{ traceparent, tracestate },
'azure-functions: get trace-context from HTTP trigger request headers',
);
}
const trans = (hookCtx.hookData.trans = ins.startTransaction(
// This is the default name. Trigger-specific values are added below.
executionContext.functionName,
TRANS_TYPE_FROM_TRIGGER_TYPE[triggerType],
{
childOf: traceparent,
tracestate,
},
));
// Expected env vars are documented at:
// https://learn.microsoft.com/en-us/azure/app-service/reference-app-settings
const accountId = getAzureAccountId();
const resourceGroup = process.env.WEBSITE_RESOURCE_GROUP;
const fnAppName = process.env.WEBSITE_SITE_NAME;
const fnName = executionContext.functionName;
const faasData = {
trigger: {
type: FAAS_TRIGGER_TYPE_FROM_TRIGGER_TYPE[triggerType],
},
execution: invocationId,
coldstart: isColdStart,
};
if (accountId && resourceGroup && fnAppName) {
faasData.id = `/subscriptions/${accountId}/resourceGroups/${resourceGroup}/providers/Microsoft.Web/sites/${fnAppName}/functions/${fnName}`;
}
if (fnAppName && fnName) {
faasData.name = `${fnAppName}/${fnName}`;
}
trans.setFaas(faasData);
if (triggerType === TRIGGER_HTTP) {
// The request object is the first item in `hookCtx.inputs`. See:
// https://github.com/Azure/azure-functions-nodejs-worker/blob/v3.5.2/src/eventHandlers/InvocationHandler.ts#L127
const req = hookCtx.inputs[0];
if (req) {
trans.req = req; // Used for setting `trans.context.request` by `getContextFromRequest()`.
if (agent._conf.usePathAsTransactionName && req.url) {
trans.setDefaultName(`${req.method} ${new URL(req.url).pathname}`);
} else {
const route = funcInfo.routePrefix
? `/${funcInfo.routePrefix}/${funcInfo.httpRoute}`
: `/${funcInfo.httpRoute}`;
trans.setDefaultName(`${req.method} ${route}`);
}
}
}
});
hookDisposables.push(d);
d = core.registerHook('postInvocation', (hookCtx) => {
if (!hookCtx.invocationContext) {
// Doesn't look like `require('@azure/functions-core').PreInvocationContext`. Abort.
return;
}
const invocationId = hookCtx.invocationContext.invocationId;
log.trace({ invocationId }, 'azure-functions: postInvocation');
const trans = hookCtx.hookData.trans;
if (!trans) {
return;
}
const funcInfo = hookCtx.hookData.funcInfo;
if (funcInfo.triggerType === TRIGGER_HTTP) {
setTransDataFromHttpTriggerResult(trans, hookCtx);
} else if (hookCtx.error) {
trans.result = constants.RESULT_FAILURE;
trans.setOutcome(constants.OUTCOME_FAILURE);
} else {
trans.result = constants.RESULT_SUCCESS;
trans.setOutcome(constants.OUTCOME_SUCCESS);
}
if (hookCtx.error) {
// Capture the error before trans.end() so it associates with the
// current trans. `skipOutcome` to avoid setting outcome on a possible
// currentSpan, because this error applies to the transaction, not any
// sub-span.
agent.captureError(hookCtx.error, { skipOutcome: true });
}
trans.end();
});
hookDisposables.push(d);
}