function instrument()

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);
}