pm2
Version:
Production process manager for Node.JS applications with a built-in load balancer.
381 lines (337 loc) • 9.96 kB
JavaScript
'use strict'
const Configuration = require('./configuration')
const Debug = require('debug')
const { ServiceManager } = require('./serviceManager')
const { createTransport } = require('./services/transport')
const { FeatureManager } = require('./featureManager')
const { ActionService } = require('./services/actions')
const { MetricService, MetricType, MetricMeasurements } = require('./services/metrics')
const Meter = require('./utils/metrics/meter')
const Histogram = require('./utils/metrics/histogram')
const Gauge = require('./utils/metrics/gauge')
const Counter = require('./utils/metrics/counter')
const { EventsFeature } = require('./features/events')
const { TracingFeature } = require('./features/tracing')
const { canUseInspector } = require('./constants')
const { RuntimeStatsService } = require('./services/runtimeStats')
const defaultConfig = {
catchExceptions: true,
profiling: true,
metrics: {
v8: true,
network: false,
eventLoop: true,
runtime: true,
http: true
},
standalone: false,
apmOptions: undefined,
tracing: {
enabled: false,
outbound: false
}
}
class PMX {
constructor () {
this.initialConfig = undefined
this.featureManager = new FeatureManager()
this.transport = null
this.actionService = null
this.metricService = null
this.runtimeStatsService = null
this.logger = Debug('axm:main')
this.initialized = false
}
/**
* Init the APM instance, you should *always* init it before using any method
*/
init (config) {
const callsite = (new Error().stack || '').split('\n')[2]
if (callsite && callsite.length > 0) {
this.logger(`init from ${callsite}`)
}
if (this.initialized === true) {
this.logger(`Calling init but was already the case, destroying and recreating`)
this.destroy()
}
if (config === undefined) {
config = defaultConfig
}
if (!config.standalone) {
const autoStandalone = process.env.PM2_SECRET_KEY && process.env.PM2_PUBLIC_KEY && process.env.PM2_APP_NAME
config.standalone = !!autoStandalone
config.apmOptions = autoStandalone ? {
secretKey: process.env.PM2_SECRET_KEY,
publicKey: process.env.PM2_PUBLIC_KEY,
appName: process.env.PM2_APP_NAME
} : undefined
}
// Register the transport before any other service
this.transport = createTransport(config.standalone === true ? 'websocket' : 'ipc', config.apmOptions)
ServiceManager.set('transport', this.transport)
if (canUseInspector()) {
const Inspector = require('./services/inspector')
const inspectorService = new Inspector()
inspectorService.init()
ServiceManager.set('inspector', inspectorService)
}
// register the action service
this.actionService = new ActionService()
this.actionService.init()
ServiceManager.set('actions', this.actionService)
// register the metric service
this.metricService = new MetricService()
this.metricService.init()
ServiceManager.set('metrics', this.metricService)
this.runtimeStatsService = new RuntimeStatsService()
this.runtimeStatsService.init()
if (this.runtimeStatsService.isEnabled()) {
ServiceManager.set('runtimeStats', this.runtimeStatsService)
}
// init features
this.featureManager.init(config)
Configuration.init(config)
// save the configuration
this.initialConfig = config
this.initialized = true
return this
}
/**
* Destroy the APM instance, every method will stop working afterwards
*/
destroy () {
this.logger('destroy')
this.featureManager.destroy()
if (this.actionService !== null) {
this.actionService.destroy()
}
if (this.transport !== null) {
this.transport.destroy()
}
if (this.metricService !== null) {
this.metricService.destroy()
}
if (this.runtimeStatsService !== null) {
this.runtimeStatsService.destroy()
}
const inspectorService = ServiceManager.get('inspector')
if (inspectorService !== undefined) {
inspectorService.destroy()
}
}
/**
* Fetch current configuration of the APM
*/
getConfig () {
return this.initialConfig
}
/**
* Notify an error to PM2 Plus/Enterprise, note that you can attach a context to it
* to provide more insight about the error
*/
notifyError (error, context) {
const notify = this.featureManager.get('notify')
return notify.notifyError(error, context)
}
/**
* Register metrics in bulk
*/
metrics (metric) {
const res = []
if (metric === undefined || metric === null) {
console.error(`Received empty metric to create`)
console.trace()
return []
}
let metrics = !Array.isArray(metric) ? [ metric ] : metric
for (let metric of metrics) {
if (typeof metric.name !== 'string') {
console.error(`Trying to create a metrics without a name`, metric)
console.trace()
res.push({})
continue
}
if (metric.type === undefined) {
metric.type = MetricType.gauge
}
switch (metric.type) {
case MetricType.counter : {
res.push(this.counter(metric))
continue
}
case MetricType.gauge : {
res.push(this.gauge(metric))
continue
}
case MetricType.histogram : {
res.push(this.histogram(metric))
continue
}
case MetricType.meter : {
res.push(this.meter(metric))
continue
}
case MetricType.metric : {
res.push(this.gauge(metric))
continue
}
default: {
console.error(`Invalid metric type ${metric.type} for metric ${metric.name}`)
console.trace()
res.push({})
continue
}
}
}
return res
}
/**
* Create an histogram metric
*/
histogram (config) {
if (typeof config === 'string') {
config = {
name: config,
measurement: MetricMeasurements.mean
}
}
if (this.metricService === null) {
return console.trace(`Tried to register a metric without initializing /io`)
}
return this.metricService.histogram(config)
}
/**
* Create a gauge metric
*/
metric (config) {
if (typeof config === 'string') {
config = {
name: config
}
}
if (this.metricService === null) {
return console.trace(`Tried to register a metric without initializing /io`)
}
return this.metricService.metric(config)
}
/**
* Create a gauge metric
*/
gauge (config) {
if (typeof config === 'string') {
config = {
name: config
}
}
if (this.metricService === null) {
return console.trace(`Tried to register a metric without initializing /io`)
}
return this.metricService.metric(config)
}
/**
* Create a counter metric
*/
counter (config) {
if (typeof config === 'string') {
config = {
name: config
}
}
if (this.metricService === null) {
return console.trace(`Tried to register a metric without initializing /io`)
}
return this.metricService.counter(config)
}
/**
* Create a meter metric
*/
meter (config) {
if (typeof config === 'string') {
config = {
name: config
}
}
if (this.metricService === null) {
return console.trace(`Tried to register a metric without initializing /io`)
}
return this.metricService.meter(config)
}
/**
* Register a custom action that will be executed when the someone called
* it from the API
*/
action (name, opts, fn) {
// backward compatiblity
if (typeof name === 'object') {
const tmp = name
name = tmp.name
opts = tmp.options
fn = tmp.action
}
if (this.actionService === null) {
return console.trace(`Tried to register a action without initializing /io`)
}
return this.actionService.registerAction(name, opts, fn)
}
onExit (callback) {
if (typeof callback === 'function') {
const handler = () => callback()
process.on('exit', handler)
process.on('SIGINT', () => { handler(); process.exit() })
process.on('SIGTERM', () => { handler(); process.exit() })
}
}
/**
* Emit a custom event to Keymetrics
* @deprecated
*
* The feature has been removed from PM2 Plus and will be removed in future release
*/
emit (name, data) {
const events = this.featureManager.get('events')
return events.emit(name, data)
}
/**
* Get the tracing agent to add more information about traces
*/
getTracer () {
const tracing = this.featureManager.get('tracing')
return tracing.getTracer()
}
initModule (opts, cb) {
if (!opts) opts = {}
if (opts.reference) {
opts.name = opts.reference
delete opts.reference
}
opts = Object.assign({
widget: {}
}, opts)
opts.widget = Object.assign({
type : 'generic',
logo : 'https://app.keymetrics.io/img/logo/keymetrics-300.png',
theme : ['#111111', '#1B2228', '#807C7C', '#807C7C']
}, opts.widget)
opts.isModule = true
opts = Configuration.init(opts)
return typeof cb === 'function' ? cb(null, opts) : opts
}
/**
* Return a custom express middleware that will send an error to the backend
* with all the details of the http request
*/
expressErrorHandler () {
const notify = this.featureManager.get('notify')
return notify.expressErrorHandler()
}
/**
* Return a custom koa middleware that will send an error to the backend
* with all the details of the http request
*/
koaErrorHandler () {
const notify = this.featureManager.get('notify')
return notify.koaErrorHandler()
}
}
module.exports = PMX
module.exports.defaultConfig = defaultConfig