UNPKG

6.06 kBJavaScriptView Raw
1// @flow
2
3// NOTE: dont use Module here, this file is used by Module
4
5import autoBind from 'auto-bind'
6
7import System from './System'
8import { isError } from './errorUtils'
9import { errorToObj } from './stringUtils'
10
11import type { MetricDimensions, MinimalMetricDimensions } from './types'
12import type { Metric } from './InfluxDBClient'
13
14type ModuleConfig = {
15 isEntryPoint?: ?boolean,
16}
17
18export default class Module {
19
20 // Instance
21
22 MODULE_NAME: string
23
24 _moduleConfig: ModuleConfig
25
26
27 constructor(moduleName: string, moduleConfig: ?ModuleConfig) {
28 this.MODULE_NAME = (moduleName.split('/').pop() || '').split('.')[0]
29 this._moduleConfig = moduleConfig || {}
30 // $FlowIgnore
31
32 // NOTE: the outer timeout is set to avoid calling System from the Moduile ctor, which can access System before it was loaded
33 setTimeout(() => {
34 const lifesignIntervalMS = System.getConfig().lifesignIntervalMS
35 if (lifesignIntervalMS && this._moduleConfig.isEntryPoint) {
36 setInterval(() => {
37 this.noteCount('lifesign')
38 }, lifesignIntervalMS)
39 }
40 }, 1000)
41
42 autoBind(this)
43 }
44
45
46 debug = (message: string, data: ?any, logOnly: ?boolean) => {
47 this._internalLog('debug', this.MODULE_NAME, message, data, logOnly)
48 }
49
50 log = (message: string, data: ?any, logOnly: ?boolean) => {
51 this._internalLog('info', this.MODULE_NAME, message, data, logOnly)
52 }
53
54 warn = (message: string, data: ?any, logOnly: ?boolean) => {
55 this._internalLog('warn', this.MODULE_NAME, message, data, logOnly)
56 }
57
58 error = (message: string, data: ?any, logOnly: ?boolean) => {
59 this._internalLog('error', this.MODULE_NAME, message, data, logOnly)
60 }
61
62 trackCountsDated = async (countMetrics: Array<Metric>) => {
63 try {
64 await _trackMetricsToInfluxDB(countMetrics.map(metric => ({
65 ...metric,
66 metricType: 'counters',
67 module: this.MODULE_NAME,
68 })))
69 } catch (err) {
70 this.error('Failed to track dated counts', { err })
71 }
72 }
73
74 trackCountDated = async (statName: string, value: number, timestampMs: number, dims: MetricDimensions) => {
75 // $FlowIgnore
76 await this.trackCountsDated([ { statName, value, timestampMs, dims } ])
77 }
78
79 noteGauge = async (statName: string, value: number, dims: ?MetricDimensions) => {
80 try {
81 // $FlowIgnore
82 await _trackMetricToInfluxDB(statName, value, {
83 ...dims,
84 metricType: 'gauges',
85 module: this.MODULE_NAME,
86 })
87 } catch (err) {
88 this.error('Failed to track gauge', { err, statName })
89 }
90 }
91
92 noteCount = async (statName: string, value: number = 1, dims: ?MetricDimensions) => {
93 try {
94 // $FlowIgnore
95 await _trackMetricToInfluxDB(statName, value, {
96 ...dims,
97 metricType: 'counters',
98 module: this.MODULE_NAME,
99 })
100 } catch (err) {
101 this.error('Failed to track count', { err, statName }, true) // avoid call loop of error count logs tracking
102 }
103 }
104
105 noteTimer = async (statName: string, durationMs: number, dims: ?MetricDimensions) => {
106 if (typeof durationMs !== 'number')
107 throw new Error(`durationMs must be a number (${statName})`) // cant use context here, circular dependency...
108 try {
109 // $FlowIgnore
110 await _trackMetricToInfluxDB(statName, durationMs, {
111 ...dims,
112 metricType: 'timers',
113 module: this.MODULE_NAME,
114 })
115 } catch (err) {
116 this.error('Failed to track timer', { err, statName })
117 }
118 }
119
120
121 async trackOp<T>(op: () => Promise<T>, name: ?string, dims: ?MetricDimensions, options: ?{ log?: ?boolean }): Promise<T> {
122 if (name == null) {
123 return await op()
124 }
125
126 options = options || {}
127 dims = { ...dims, module: this.MODULE_NAME }
128 const startMS = Date.now()
129 let endMS = startMS
130 try {
131 options.log && this.log(`Operation '${name}' started...`)
132 this.noteCount(`${name}.start`, 1, dims)
133 const ret = await op()
134 this.noteCount(`${name}.success`, 1, dims)
135 endMS = Date.now()
136 options.log && this.log(`Operation '${name}' completed`, { execMS: endMS - startMS })
137 return ret
138 } catch (err) {
139 endMS = Date.now()
140 options.log && this.log(`Operation '${name}' threw an error`, { execMS: endMS - startMS, err: err })
141 this.noteCount(`${name}.fail`, 1, dims)
142 throw err
143 } finally {
144 this.noteCount(`${name}.end`, 1, dims)
145 this.noteTimer(name, endMS - startMS, dims)
146 }
147 }
148
149
150 _internalLog(level: string, module: string, message: string, data: ?any, logOnly: ?boolean) {
151 try {
152 // check if level is activated
153 if (System.activeLogLevels && !System.activeLogLevels.includes(level))
154 return
155
156 const logRecord = {
157 level: level,
158 module: module,
159 message: message,
160 data: _correctData(data),
161 }
162
163 // log to other destinations
164 System.queueLogRecord(logRecord)
165
166 if (!logOnly)
167 this.noteCount(`logging.${level}`, 1)
168 } catch (err) {
169 console.error(`Error thrown during logging operation: ${err.toString()}`) // eslint-disable-line no-console
170 }
171 }
172}
173
174async function _trackMetricsToInfluxDB(metrics: Array<Metric>) {
175 await Promise.all(System.influxDBClients.map(idbClient => idbClient.trackMetrics(metrics.map(metric => ({
176 ...metric,
177 timestampMs: Date.now(),
178 })))))
179}
180
181async function _trackMetricToInfluxDB(statName: string, value: number, dims: MinimalMetricDimensions, timestampMs?: ?number) {
182 await _trackMetricsToInfluxDB([ { statName, value, dims, timestampMs } ])
183}
184
185function _correctData(data: ?any): ?any {
186 if (data != null) {
187 if (isError(data)) {
188 data = {
189 err: data,
190 }
191 }
192
193 let iteratingData = data
194 let maxDepth = 4
195 while (--maxDepth && iteratingData && isError(iteratingData.err)) {
196 // $FlowIgnore
197 iteratingData.err = errorToObj(iteratingData.err)
198 // $FlowIgnore
199 iteratingData = iteratingData.err.data
200 }
201 }
202
203 return data
204}