senecajs/seneca-telemetry-newrelic

View on GitHub
src/newrelic.ts

Summary

Maintainability
C
1 day
Test Coverage
/* Copyright © 2021 Seneca Project Contributors, MIT License. */


import NewRelic from 'newrelic';
import { Skip, Some } from 'gubu';

import { TracingCollector } from './tracing-collector';
import { MetricsCollector } from './metrics-collector';
import { EventsCollector } from './events-collector';

import { NewRelicOptions, Nullable, Spec } from './types';

function addSegment(spec: Spec) {
  if (spec.ctx.actdef?.func) {
    const { ctx, data } = spec
    const pattern = ctx.actdef.pattern
    const origfunc = ctx.actdef.func
    const meta = data.meta
    const context = ctx.seneca.context
    
    if(ctx.actdef.func.$$newrelic_wrapped$$) {
      return
    }

    // ensure each action has it's own endSegment
    context.newrelic = context.newrelic || {}
    let endSegmentMap =
      (context.newrelic.endSegmentMap = context.newrelic.endSegmentMap || {})

    ctx.actdef.func = function(this: any, ...args: any) {
      const instance = this
      NewRelic.startSegment(
        pattern + '~' + origfunc.name,
        true,
        function handler(endSegmentHandler: any) {
          endSegmentMap[meta.mi] = (endSegmentMap[meta.mi] || {})
          endSegmentMap[meta.mi].endSegmentHandler = endSegmentHandler
          return origfunc.call(instance, ...args)
        },
        function endSegmentHandler() { }
      )

      ctx.actdef.func.$$newrelic_wrapped$$ = true;
    }

    Object.defineProperty(
      ctx.actdef.func, 'name', { value: 'newrelic_' + origfunc.name })
  }
}

function endSegment(spec: Spec) {
  const meta = spec.data.meta
  const context = spec.ctx.seneca.context
  const endSegmentMap = context.newrelic?.endSegmentMap
  if (endSegmentMap && endSegmentMap[meta.mi]) {
    const endSegmentHandler = endSegmentMap[meta.mi].endSegmentHandler
    if (endSegmentHandler) {
      delete endSegmentMap[meta.mi]
      endSegmentHandler()
    }
  }
}

function preload(this: any, opts: any) {
  const seneca = this;
  const { options }: { options: NewRelicOptions } = opts;
  const isPluginEnabled = options && options.enabled

  if(!isPluginEnabled) {
    return
  }

  const segmentIsEnabled = options.segment && options.segment.enabled;
  const tracingIsEnabled = options.tracing && options.tracing.enabled;
  const metricsIsEnabled = options.metrics && options.metrics.enabled;
  const eventsIsEnabled = options.events && options.events.enabled;
  
  let tracingCollector: Nullable<TracingCollector> = null;
  if (tracingIsEnabled && options.tracing) {
    const { accountApiKey, serviceName } = options.tracing;
    tracingCollector = new TracingCollector(
      seneca, accountApiKey, serviceName
    );
  }

  seneca.order.inward.add((spec: Spec) => {
    if (segmentIsEnabled) {
      addSegment(spec);
    }
    if (tracingCollector) {
      tracingCollector.dispatch(spec, 'inward');
    }
  })

  seneca.order.outward.add((spec: Spec) => {
    if (segmentIsEnabled) {
      endSegment(spec);
    }
    if (tracingCollector) {
      tracingCollector.dispatch(spec, 'outward');
    }
  })

  if (metricsIsEnabled) {
    if (!options.metrics?.accountApiKey) {
      throw new Error("Please provide accountApiKey parameter to Metrics API");
    }
    this.metrics_api_key = options.metrics.accountApiKey;
  }

  if (eventsIsEnabled) {
    if (!options.events?.accountApiKey) {
      throw new Error("Please provide accountApiKey parameter to Events API");
    }
    this.events_api_key = options.events.accountApiKey;
  }
}

function newrelic(this: any, options: NewRelicOptions) {
    const isPluginEnabled = options && options.enabled

    if(!isPluginEnabled) {
      return
    }

    const seneca: any = this

    seneca.message('plugin:newrelic,get:info', get_info)

    if (seneca.metrics_api_key) {
      const { metric_count_handler, metric_summary_handler, metric_gauge_handler} = MetricsCollector(seneca.metrics_api_key);
      seneca
        .message({
          plugin: 'newrelic',
          api: 'metric',
          type: 'count',
          value: Number,
          name: String,
          attributes: Skip({}),
        }, metric_count_handler)
        .message({
          plugin: 'newrelic',
          api: 'metric',
          type: 'gauge',
          value: Number,
          name: String,
          attributes: Skip({}),
        }, metric_gauge_handler)
        .message({
          plugin: 'newrelic',
          api: 'metric',
          type: 'summary',
          value: Some({
            count: Skip(Number),
            sum: Skip(Number),
            min: Skip(Number),
            max: Skip(Number),
          }),
          name: String,
          attributes: Skip({})
        }, metric_summary_handler);
    }
    if (seneca.events_api_key) {
      const { event_handler } = EventsCollector(seneca.events_api_key);
      seneca
        .message({
          plugin: 'newrelic',
          api: 'event',
          eventType: String,
          attributes: Some({}),
        }, event_handler)
    }

    async function get_info(this: any, _msg: any) {
      return {
        ok: true,
        name: 'newrelic',
        details: {
          sdk: 'newrelic'
        }
      }
    }

    return {
      exports: {
        native: () => ({})
      }
    }
}

// Default options.
const defaults: NewRelicOptions = {
  enabled: false,
  tracing: {
    enabled: false,
    accountApiKey: '',
    serviceName: '',
  },
  events: {
    enabled: false,
    accountApiKey: '',
  },
  metrics: {
    accountApiKey: '',
    enabled: false,
  },
  segment: {
    enabled: false,
  },
  // TODO: Enable debug logging
  debug: false
}

Object.assign(newrelic, {defaults, preload})

export default newrelic

if ('undefined' !== typeof (module)) {
    module.exports = newrelic
}