tracer.js

/* eslint-disable camelcase,max-len */
'use strict';
/**
 * Node.js Express application request Open Telemetry tracing.
 *
 * @module tracer
 * @requires @google-cloud/opentelemetry-cloud-trace-exporter
 * @requires @google-cloud/opentelemetry-cloud-trace-propagator
 * @requires @opentelemetry/api
 * @requires @opentelemetry/instrumentation
 * @requires @opentelemetry/instrumentation-express
 * @requires @opentelemetry/instrumentation-http
 * @requires @opentelemetry/resources
 * @requires @opentelemetry/sdk-trace-node
 * @requires @opentelemetry/sdk-trace-base
 * @requires @opentelemetry/semantic-conventions
 * @see https://cloud.google.com/trace/docs/setup/nodejs-ot
 */
const path = require('path');
const common = require(path.join(__dirname, 'common'));
const opentelemetry = require('@opentelemetry/api');

// Not functionally required but gives some insight what happens behind the scenes
const {diag, DiagConsoleLogger, DiagLogLevel} = opentelemetry;
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);

const {AlwaysOnSampler, SamplingDecision} = require('@opentelemetry/sdk-trace-base');
const {BatchSpanProcessor} = require('@opentelemetry/sdk-trace-base');
const {CloudPropagator} = require('@google-cloud/opentelemetry-cloud-trace-propagator');
const {ExpressInstrumentation} = require('@opentelemetry/instrumentation-express');
const {HttpInstrumentation} = require('@opentelemetry/instrumentation-http');
const {NodeTracerProvider} = require('@opentelemetry/sdk-trace-node');
const {registerInstrumentations} = require('@opentelemetry/instrumentation');
const {Resource} = require('@opentelemetry/resources');
const {SemanticAttributes, SemanticResourceAttributes} = require('@opentelemetry/semantic-conventions');
// const {SimpleSpanProcessor} = require('@opentelemetry/sdk-trace-base');
const {TraceExporter} = require('@google-cloud/opentelemetry-cloud-trace-exporter');

/**
 * Node Open Telemetry trace provider.
 *
 * @param {string} serviceName - Name of the service to create traces for.
 * @return {object} - Tracer object.
 */
module.exports = (serviceName) => {
  const provider = new NodeTracerProvider({
    resource: new Resource({
      [SemanticResourceAttributes.SERVICE_NAME]: serviceName,
      [SemanticResourceAttributes.SERVICE_VERSION]: common.version,
    }),
    sampler: filterSampler(ignoreHealthCheck, new AlwaysOnSampler()),
  });
  if (process.env.NODE_ENV === 'test') {
    registerInstrumentations({
      tracerProvider: provider,
      instrumentations: [HttpInstrumentation],
    });
  } else {
    registerInstrumentations({
      tracerProvider: provider,
      instrumentations: [
        // Express instrumentation expects HTTP layers to be instrumented
        HttpInstrumentation,
        ExpressInstrumentation,
      ],
    });
  }

  // Initialize the exporter. When your application is running on Google Cloud,
  // you do not need to provide auth credentials or a project id.
  const exporter = new TraceExporter();
  provider.addSpanProcessor(new BatchSpanProcessor(exporter));
  // Initialize the OpenTelemetry APIs to use the NodeTracerProvider bindings
  provider.register({
    propagator: new CloudPropagator(),
  });
  ['SIGINT', 'SIGTERM'].forEach((signal) => {
    process.on(signal, () => provider.shutdown().catch(console.error));
  });
  // provider.register();

  return opentelemetry.trace.getTracer(common.name);
};

/**
 * Filter data returned to opentelemetry.
 *
 * @param {function} filterFn - Function to use to filter sample.
 * @param {object} parent - Parent sampler object.
 * @return {any} - Trace data filtered by the supplied function.
 */
function filterSampler(filterFn, parent) {
  // noinspection JSUnusedGlobalSymbols
  return {
    shouldSample(ctx, tid, spanName, spanKind, attr, links) {
      if (!filterFn(spanName, spanKind, attr)) {
        return {decision: SamplingDecision.NOT_RECORD};
      }
      return parent.shouldSample(ctx, tid, spanName, spanKind, attr, links);
    },
    toString() {
      return `FilterSampler(${parent.toString()})`;
    },
  };
}

/**
 * Function used by opentelemetry to not trace health check endpoints.
 *
 * @param {string} spanName - Opentelemetry span name.
 * @param {string} spanKind - Opentelemetry span kind.
 * @param {object} attributes - Object attributes used to identify health check endpoints.
 * @return {boolean} - true when health check should be ignored, false otherwise.
 */
function ignoreHealthCheck(spanName, spanKind, attributes) {
  return spanKind !== opentelemetry.SpanKind.SERVER.toString() || attributes[SemanticAttributes.HTTP_ROUTE] !== '/health';
}