teamdigitale/italia-ts-commons

View on GitHub
src/appinsights.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import * as appInsights from "applicationinsights";
import { DistributedTracingModes } from "applicationinsights";
import Config = require("applicationinsights/out/Library/Config");
import {
  getKeepAliveAgentOptions,
  isFetchKeepaliveEnabled,
  newHttpAgent,
  newHttpsAgent,
} from "./agent";

interface IInsightsRequestData {
  readonly baseType: "RequestData";
  readonly baseData: {
    readonly ver: number;
    // eslint-disable-next-line @typescript-eslint/ban-types
    readonly properties: {};
    // eslint-disable-next-line @typescript-eslint/ban-types
    readonly measurements: {};
    readonly id: string;
    readonly name: string;
    // eslint-disable-next-line functional/prefer-readonly-type
    url: string;
    readonly source?: string;
    readonly duration: string;
    readonly responseCode: string;
    readonly success: boolean;
  };
}

export interface IInsightsTracingConfig {
  readonly isTracingDisabled?: boolean;
  readonly cloudRole?: string;
  readonly applicationVersion?: string;
}

export type ApplicationInsightsConfig = IInsightsTracingConfig &
  Partial<
    Pick<
      Config,
      "httpAgent" | "httpsAgent" | "samplingPercentage" | "disableAppInsights"
    >
  >;

/**
 * Internal usage, do not export
 */
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
function startAppInsights(
  connectionString: string,
  aiConfig: ApplicationInsightsConfig
): appInsights.TelemetryClient {
  const ai = appInsights.setup(connectionString);

  if (aiConfig.isTracingDisabled) {
    ai.setAutoCollectConsole(false)
      .setAutoCollectPerformance(false)
      .setAutoCollectDependencies(false)
      .setAutoCollectRequests(false)
      .setAutoDependencyCorrelation(false);
  }

  // @see https://github.com/Azure/azure-functions-host/issues/3747
  // @see https://github.com/Azure/azure-functions-nodejs-worker/pull/244
  ai.setDistributedTracingMode(DistributedTracingModes.AI_AND_W3C)
    .setSendLiveMetrics(false)
    // @see https://stackoverflow.com/questions/49438235/application-insights-metric-in-aws-lambda/49441135#49441135
    .setUseDiskRetryCaching(false)
    .start();

  appInsights.defaultClient.addTelemetryProcessor(
    // eslint-disable-next-line @typescript-eslint/no-use-before-define
    removeQueryParamsPreprocessor
  );

  // eslint-disable-next-line @typescript-eslint/no-use-before-define
  appInsights.defaultClient.addTelemetryProcessor(disableSamplingByTag);

  // Configure the data context of the telemetry client
  // refering to the current application version with a specific CloudRole

  if (aiConfig.applicationVersion !== undefined) {
    // eslint-disable-next-line functional/immutable-data
    appInsights.defaultClient.context.tags[
      appInsights.defaultClient.context.keys.applicationVersion
    ] = aiConfig.applicationVersion;
  }

  if (aiConfig.cloudRole !== undefined) {
    // eslint-disable-next-line functional/immutable-data
    appInsights.defaultClient.context.tags[
      appInsights.defaultClient.context.keys.cloudRole
    ] = aiConfig.cloudRole;
  }

  // override some default values when provided
  const config = appInsights.defaultClient.config;

  // eslint-disable-next-line functional/immutable-data
  config.httpAgent = aiConfig.httpAgent ?? config.httpAgent;

  // eslint-disable-next-line functional/immutable-data
  config.httpsAgent = aiConfig.httpsAgent ?? config.httpsAgent;

  // eslint-disable-next-line functional/immutable-data
  config.samplingPercentage =
    aiConfig.samplingPercentage ?? config.samplingPercentage;

  // eslint-disable-next-line functional/immutable-data
  config.disableAppInsights =
    aiConfig.disableAppInsights ?? config.disableAppInsights;

  return appInsights.defaultClient;
}

// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
export function removeQueryParamsPreprocessor(
  envelope: appInsights.Contracts.Envelope,
  _?: {
    readonly [name: string]: unknown;
  }
): boolean {
  if (envelope.data.baseType === "RequestData") {
    const originalUrl = (envelope.data as IInsightsRequestData).baseData.url;
    // eslint-disable-next-line functional/immutable-data
    (envelope.data as IInsightsRequestData).baseData.url =
      originalUrl.split("?")[0];
  }
  return true;
}

// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
export function disableSamplingByTag(
  envelope: appInsights.Contracts.Envelope,
  _?: {
    readonly [name: string]: unknown;
  }
): boolean {
  if (envelope.tags.samplingEnabled === "false") {
    // eslint-disable-next-line functional/immutable-data
    envelope.sampleRate = 100;
  }
  return true;
}

/**
 * Configure Application Insights default client
 * using settings taken from the environment:
 *
 * - setup tracing options
 * - setup cloudRole and version
 * - eventually setup http keeplive to prevent SNAT port exhaustion
 * - start application insights
 *
 * As the default client is a singleton shared between functions
 * you may want to prevent bootstrapping insights more than once
 * checking if appInsights.defaultClient id already set in the caller.
 *
 * To enable http agent keepalive set up these environment variables:
 * https://github.com/pagopa/io-ts-commons/blob/master/src/agent.ts#L11
 *
 * If you need to programmatically call Application Insights methods
 * set operationId = context.Tracecontext.traceparent to correlate
 * the call with the parent request.
 *
 */
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
export function initAppInsights(
  aiConnectionString: string,
  config?: ApplicationInsightsConfig,
  env: typeof process.env = process.env
): ReturnType<typeof startAppInsights> {
  // @see https://github.com/pagopa/io-ts-commons/blob/master/src/agent.ts
  // @see https://docs.microsoft.com/it-it/azure/load-balancer/load-balancer-outbound-connections
  const agentOpts = isFetchKeepaliveEnabled(env)
    ? {
        httpAgent: newHttpAgent(getKeepAliveAgentOptions(env)),
        httpsAgent: newHttpsAgent(getKeepAliveAgentOptions(env)),
      }
    : {};

  // defaults to the name of the function app if not set in config
  const cloudRole = config?.cloudRole || env.WEBSITE_SITE_NAME;

  return startAppInsights(aiConnectionString, {
    cloudRole,
    ...config,
    ...agentOpts,
  });
}