// @flow // NOTE: dont use Module here, this file is used by Module import autoBind from 'auto-bind' import System from './System' import { isError } from './errorUtils' import { errorToObj } from './stringUtils' import type { MetricDimensions, MinimalMetricDimensions } from './types' import type { Metric } from './InfluxDBClient' type ModuleConfig = { isEntryPoint?: ?boolean, } export default class Module { // Instance MODULE_NAME: string _moduleConfig: ModuleConfig constructor(moduleName: string, moduleConfig: ?ModuleConfig) { this.MODULE_NAME = (moduleName.split('/').pop() || '').split('.')[0] this._moduleConfig = moduleConfig || {} // $FlowIgnore // NOTE: the outer timeout is set to avoid calling System from the Moduile ctor, which can access System before it was loaded setTimeout(() => { const lifesignIntervalMS = System.getConfig().lifesignIntervalMS if (lifesignIntervalMS && this._moduleConfig.isEntryPoint) { setInterval(() => { this.noteCount('lifesign') }, lifesignIntervalMS) } }, 1000) autoBind(this) } debug = (message: string, data: ?any, logOnly: ?boolean) => { this._internalLog('debug', this.MODULE_NAME, message, data, logOnly) } log = (message: string, data: ?any, logOnly: ?boolean) => { this._internalLog('info', this.MODULE_NAME, message, data, logOnly) } warn = (message: string, data: ?any, logOnly: ?boolean) => { this._internalLog('warn', this.MODULE_NAME, message, data, logOnly) } error = (message: string, data: ?any, logOnly: ?boolean) => { this._internalLog('error', this.MODULE_NAME, message, data, logOnly) } trackCountsDated = async (countMetrics: Array) => { try { await _trackMetricsToInfluxDB(countMetrics.map(metric => ({ ...metric, metricType: 'counters', module: this.MODULE_NAME, }))) } catch (err) { this.error('Failed to track dated counts', { err }) } } trackCountDated = async (statName: string, value: number, timestampMs: number, dims: MetricDimensions) => { // $FlowIgnore await this.trackCountsDated([ { statName, value, timestampMs, dims } ]) } noteGauge = async (statName: string, value: number, dims: ?MetricDimensions) => { try { // $FlowIgnore await _trackMetricToInfluxDB(statName, value, { ...dims, metricType: 'gauges', module: this.MODULE_NAME, }) } catch (err) { this.error('Failed to track gauge', { err, statName }) } } noteCount = async (statName: string, value: number = 1, dims: ?MetricDimensions) => { try { // $FlowIgnore await _trackMetricToInfluxDB(statName, value, { ...dims, metricType: 'counters', module: this.MODULE_NAME, }) } catch (err) { this.error('Failed to track count', { err, statName }, true) // avoid call loop of error count logs tracking } } noteTimer = async (statName: string, durationMs: number, dims: ?MetricDimensions) => { if (typeof durationMs !== 'number') throw new Error(`durationMs must be a number (${statName})`) // cant use context here, circular dependency... try { // $FlowIgnore await _trackMetricToInfluxDB(statName, durationMs, { ...dims, metricType: 'timers', module: this.MODULE_NAME, }) } catch (err) { this.error('Failed to track timer', { err, statName }) } } async trackOp(op: () => Promise, name: ?string, dims: ?MetricDimensions, options: ?{ log?: ?boolean }): Promise { if (name == null) { return await op() } options = options || {} dims = { ...dims, module: this.MODULE_NAME } const startMS = Date.now() let endMS = startMS try { options.log && this.log(`Operation '${name}' started...`) this.noteCount(`${name}.start`, 1, dims) const ret = await op() this.noteCount(`${name}.success`, 1, dims) endMS = Date.now() options.log && this.log(`Operation '${name}' completed`, { execMS: endMS - startMS }) return ret } catch (err) { endMS = Date.now() options.log && this.log(`Operation '${name}' threw an error`, { execMS: endMS - startMS, err: err }) this.noteCount(`${name}.fail`, 1, dims) throw err } finally { this.noteCount(`${name}.end`, 1, dims) this.noteTimer(name, endMS - startMS, dims) } } _internalLog(level: string, module: string, message: string, data: ?any, logOnly: ?boolean) { try { // check if level is activated if (System.activeLogLevels && !System.activeLogLevels.includes(level)) return const logRecord = { level: level, module: module, message: message, data: _correctData(data), } // log to other destinations System.queueLogRecord(logRecord) if (!logOnly) this.noteCount(`logging.${level}`, 1) } catch (err) { console.error(`Error thrown during logging operation: ${err.toString()}`) // eslint-disable-line no-console } } } async function _trackMetricsToInfluxDB(metrics: Array) { await Promise.all(System.influxDBClients.map(idbClient => idbClient.trackMetrics(metrics.map(metric => ({ ...metric, timestampMs: Date.now(), }))))) } async function _trackMetricToInfluxDB(statName: string, value: number, dims: MinimalMetricDimensions, timestampMs?: ?number) { await _trackMetricsToInfluxDB([ { statName, value, dims, timestampMs } ]) } function _correctData(data: ?any): ?any { if (data != null) { if (isError(data)) { data = { err: data, } } let iteratingData = data let maxDepth = 4 while (--maxDepth && iteratingData && isError(iteratingData.err)) { // $FlowIgnore iteratingData.err = errorToObj(iteratingData.err) // $FlowIgnore iteratingData = iteratingData.err.data } } return data }