keymetrics/pm2-io-apm

View on GitHub
src/services/metrics.ts

Summary

Maintainability
A
2 hrs
Test Coverage
'use strict'

import Meter from '../utils/metrics/meter'
import Counter from '../utils/metrics/counter'
import Histogram from '../utils/metrics/histogram'
import { ServiceManager, Service } from '../serviceManager'
import constants from '../constants'
import { Transport } from './transport'
import * as Debug from 'debug'
import Gauge from '../utils/metrics/gauge'

export enum MetricType {
  'meter' = 'meter',
  'histogram' = 'histogram',
  'counter' = 'counter',
  'gauge' = 'gauge',
  'metric' = 'metric' // deprecated, must use gauge
}

export enum MetricMeasurements {
  'min' = 'min',
  'max' = 'max',
  'sum' = 'sum',
  'count' = 'count',
  'variance' = 'variance',
  'mean' = 'mean',
  'stddev' = 'stddev',
  'median' = 'median',
  'p75' = 'p75',
  'p95' = 'p95',
  'p99' = 'p99',
  'p999' = 'p999'
}

export interface InternalMetric {
  /**
   * Display name of the metric, it should be a clear name that everyone can understand
   */
  name?: string
  type?: MetricType
  /**
   * An precise identifier for your metric, exemple:
   * The heap usage can be shown to user as 'Heap Usage' but internally
   * we want to put few namespace to be sure we talking about the main heap of v8
   * so we would choose something like: process/v8/heap/usage
   */
  id?: string
  /**
   * Choose if the metrics will be saved in our datastore or only be used in realtime
   */
  historic?: boolean
  /**
   * Unit of the metric
   */
  unit?: string
  /**
   * The handler is the function that will be called to get the current value
   * of the metrics
   */
  handler: Function
  /**
   * The implementation is the instance of the class that handle the computation
   * of the metric value
   */
  implementation: any
  /**
   * Last known value of the metric
   */
  value?: number
}

export class Metric {
  /**
   * Display name of the metric, it should be a clear name that everyone can understand
   */
  name?: string
  /**
   * An precise identifier for your metric, exemple:
   * The heap usage can be shown to user as 'Heap Usage' but internally
   * we want to put few namespace to be sure we talking about the main heap of v8
   * so we would choose something like: process/v8/heap/usage
   */
  id?: string
  /**
   * Choose if the metrics will be saved in our datastore or only be used in realtime
   */
  historic?: boolean
  /**
   * Unit of the metric
   */
  unit?: string
  /**
   * Allow to automatically update the metric value
   * Note: only available with io.metric
   * Note: if you use this property, you will not be able to set the value with .set
   */
  value?: () => number
}

export class MetricBulk extends Metric {
  type: MetricType
}

export class HistogramOptions extends Metric {
  measurement: MetricMeasurements
}

export class MetricService implements Service {

  private metrics: Map<string, InternalMetric> = new Map()
  private timer: NodeJS.Timer | null = null
  private transport: Transport | null = null
  private logger: any = Debug('axm:services:metrics')

  init (): void {
    this.transport = ServiceManager.get('transport')
    if (this.transport === null) return this.logger('Failed to init metrics service cause no transporter')

    this.logger('init')
    this.timer = setInterval(() => {
      if (this.transport === null) return this.logger('Abort metrics update since transport is not available')
      this.logger('refreshing metrics value')
      for (let metric of this.metrics.values()) {
        metric.value = metric.handler()
      }
      this.logger('sending update metrics value to transporter')
      // send all the metrics value to the transporter
      const metricsToSend = Array.from(this.metrics.values())
        .filter(metric => {
          // thanks tslint but user can be dumb sometimes
          /* tslint:disable */
          if (metric === null || metric === undefined) return false
          if (metric.value === undefined || metric.value === null) return false

          const isNumber = typeof metric.value === 'number'
          const isString = typeof metric.value === 'string'
          const isBoolean = typeof metric.value === 'boolean'
          const isValidNumber = !isNaN(metric.value)
          /* tslint:enable */
          // we send it only if it's a string or a valid number
          return isString || isBoolean || (isNumber && isValidNumber)
        })
      this.transport.setMetrics(metricsToSend)
    }, constants.METRIC_INTERVAL)
    this.timer.unref()
  }

  registerMetric (metric: InternalMetric): void {
    // thanks tslint but user can be dumb sometimes
    /* tslint:disable */
    if (typeof metric.name !== 'string') {
      console.error(`Invalid metric name declared: ${metric.name}`)
      return console.trace()
    } else if (typeof metric.type !== 'string') {
      console.error(`Invalid metric type declared: ${metric.type}`)
      return console.trace()
    } else if (typeof metric.handler !== 'function') {
      console.error(`Invalid metric handler declared: ${metric.handler}`)
      return console.trace()
    }
    /* tslint:enable */
    if (typeof metric.historic !== 'boolean') {
      metric.historic = true
    }
    this.logger(`Registering new metric: ${metric.name}`)
    this.metrics.set(metric.name, metric)
  }

  meter (opts: Metric): Meter {
    const metric: InternalMetric = {
      name: opts.name,
      type: MetricType.meter,
      id: opts.id,
      historic: opts.historic,
      implementation: new Meter(opts),
      unit: opts.unit,
      handler: function () {
        return this.implementation.isUsed() ? this.implementation.val() : NaN
      }
    }
    this.registerMetric(metric)

    return metric.implementation
  }

  counter (opts: Metric): Counter {
    const metric: InternalMetric = {
      name: opts.name,
      type: MetricType.counter,
      id: opts.id,
      historic: opts.historic,
      implementation: new Counter(opts),
      unit: opts.unit,
      handler: function () {
        return this.implementation.isUsed() ? this.implementation.val() : NaN
      }
    }
    this.registerMetric(metric)

    return metric.implementation
  }

  histogram (opts: HistogramOptions): Histogram {
    // tslint:disable-next-line
    if (opts.measurement === undefined || opts.measurement === null) {
      opts.measurement = MetricMeasurements.mean
    }
    const metric: InternalMetric = {
      name: opts.name,
      type: MetricType.histogram,
      id: opts.id,
      historic: opts.historic,
      implementation: new Histogram(opts),
      unit: opts.unit,
      handler: function () {
        return this.implementation.isUsed() ?
          (Math.round(this.implementation.val() * 100) / 100) : NaN
      }
    }
    this.registerMetric(metric)

    return metric.implementation
  }

  metric (opts: Metric): Gauge {
    let metric: InternalMetric
    if (typeof opts.value === 'function') {
      metric = {
        name: opts.name,
        type: MetricType.gauge,
        id: opts.id,
        implementation: undefined,
        historic: opts.historic,
        unit: opts.unit,
        handler: opts.value
      }
    } else {
      metric = {
        name: opts.name,
        type: MetricType.gauge,
        id: opts.id,
        historic: opts.historic,
        implementation: new Gauge(),
        unit: opts.unit,
        handler: function () {
          return this.implementation.isUsed() ? this.implementation.val() : NaN
        }
      }
    }

    this.registerMetric(metric)

    return metric.implementation
  }

  deleteMetric (name: string) {
    return this.metrics.delete(name)
  }

  destroy () {
    if (this.timer !== null) {
      clearInterval(this.timer)
    }
    this.metrics.clear()
  }
}