@agoric/telemetry 0.6.3-u19.2 → 0.6.3-u21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -18
- package/package.json +18 -17
- package/src/flight-recorder.js +20 -12
- package/src/frcat-entrypoint.js +2 -2
- package/src/index.js +15 -22
- package/src/make-slog-sender.js +124 -107
- package/src/otel-metrics.js +229 -0
- package/src/prometheus.js +18 -0
- package/src/serialize-slog-obj.js +32 -4
- package/src/slog-sender-pipe-entrypoint.js +64 -67
- package/src/slog-sender-pipe.js +86 -110
- package/test/flight-recorder.test.js +41 -18
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { q, Fail } from '@endo/errors';
|
|
2
|
+
|
|
3
|
+
import * as ActionType from '@agoric/internal/src/action-types.js';
|
|
4
|
+
import { objectMapMutable } from '@agoric/internal/src/js-utils.js';
|
|
5
|
+
import {
|
|
6
|
+
HISTOGRAM_METRICS,
|
|
7
|
+
BLOCK_HISTOGRAM_METRICS,
|
|
8
|
+
KERNEL_STATS_METRICS,
|
|
9
|
+
makeQueueMetrics,
|
|
10
|
+
} from '@agoric/internal/src/metrics.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @import {MetricOptions, ObservableCounter, ObservableUpDownCounter} from '@opentelemetry/api';
|
|
14
|
+
* @import {MeterProvider} from '@opentelemetry/sdk-metrics';
|
|
15
|
+
* @import {TotalMap} from '@agoric/internal';
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const knownActionTypes = new Set(Object.values(ActionType.QueuedActionType));
|
|
19
|
+
|
|
20
|
+
/** @param {import('./index.js').MakeSlogSenderOptions & {otelMeterName: string, otelMeterProvider?: MeterProvider}} opts */
|
|
21
|
+
export const makeSlogSender = async (opts = /** @type {any} */ ({})) => {
|
|
22
|
+
const { otelMeterName, otelMeterProvider } = opts;
|
|
23
|
+
if (!otelMeterName) throw Fail`OTel meter name is required`;
|
|
24
|
+
if (!otelMeterProvider) return;
|
|
25
|
+
|
|
26
|
+
const shutdown = async () => {
|
|
27
|
+
await otelMeterProvider.shutdown();
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const otelMeter = otelMeterProvider.getMeter(otelMeterName);
|
|
31
|
+
|
|
32
|
+
const processedInboundActionCounter = otelMeter.createCounter(
|
|
33
|
+
'cosmic_swingset_inbound_actions',
|
|
34
|
+
{ description: 'Processed inbound action counts by type' },
|
|
35
|
+
);
|
|
36
|
+
const histograms = {
|
|
37
|
+
...objectMapMutable(HISTOGRAM_METRICS, (desc, name) => {
|
|
38
|
+
const { boundaries, ...options } = desc;
|
|
39
|
+
const advice = boundaries && { explicitBucketBoundaries: boundaries };
|
|
40
|
+
return otelMeter.createHistogram(name, { ...options, advice });
|
|
41
|
+
}),
|
|
42
|
+
...objectMapMutable(BLOCK_HISTOGRAM_METRICS, (desc, name) =>
|
|
43
|
+
otelMeter.createHistogram(name, desc),
|
|
44
|
+
),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const inboundQueueMetrics = makeQueueMetrics({
|
|
48
|
+
otelMeter,
|
|
49
|
+
namePrefix: 'cosmic_swingset_inbound_queue',
|
|
50
|
+
descPrefix: 'inbound queue',
|
|
51
|
+
console,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Values for KERNEL_STATS_METRICS could be built up locally by observing slog
|
|
55
|
+
// entries, but they are all collectively reported in "kernel-stats"
|
|
56
|
+
// (@see {@link ../../cosmic-swingset/src/kernel-stats.js exportKernelStats})
|
|
57
|
+
// and for now we just reflect that, which requires implementation as async
|
|
58
|
+
// ("observable") instruments rather than synchronous ones.
|
|
59
|
+
/** @typedef {string} KernelStatsKey */
|
|
60
|
+
/** @typedef {string} KernelMetricName */
|
|
61
|
+
/** @type {TotalMap<KernelStatsKey, number>} */
|
|
62
|
+
const kernelStats = new Map();
|
|
63
|
+
/** @type {Map<KernelMetricName, ObservableCounter | ObservableUpDownCounter>} */
|
|
64
|
+
const kernelStatsCounters = new Map();
|
|
65
|
+
for (const meta of KERNEL_STATS_METRICS) {
|
|
66
|
+
const { key, name, sub, metricType, ...options } = meta;
|
|
67
|
+
kernelStats.set(key, 0);
|
|
68
|
+
if (metricType === 'gauge') {
|
|
69
|
+
kernelStats.set(`${key}Up`, 0);
|
|
70
|
+
kernelStats.set(`${key}Down`, 0);
|
|
71
|
+
kernelStats.set(`${key}Max`, 0);
|
|
72
|
+
} else if (metricType !== 'counter') {
|
|
73
|
+
Fail`Unknown metric type ${q(metricType)} for key ${q(key)} name ${q(name)}`;
|
|
74
|
+
}
|
|
75
|
+
let counter = kernelStatsCounters.get(name);
|
|
76
|
+
if (!counter) {
|
|
77
|
+
counter =
|
|
78
|
+
metricType === 'counter'
|
|
79
|
+
? otelMeter.createObservableCounter(name, options)
|
|
80
|
+
: otelMeter.createObservableUpDownCounter(name, options);
|
|
81
|
+
kernelStatsCounters.set(name, counter);
|
|
82
|
+
}
|
|
83
|
+
const attributes = sub ? { [sub.dimension]: sub.value } : {};
|
|
84
|
+
counter.addCallback(observer => {
|
|
85
|
+
observer.observe(kernelStats.get(key), attributes);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const expectedKernelStats = new Set(kernelStats.keys());
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @typedef {object} LazyStats
|
|
92
|
+
* @property {string} namePrefix
|
|
93
|
+
* @property {MetricOptions} options
|
|
94
|
+
* @property {Set<string>} keys
|
|
95
|
+
* @property {Record<string, number>} data
|
|
96
|
+
*/
|
|
97
|
+
/** @type {(namePrefix: string, description: string) => LazyStats} */
|
|
98
|
+
const makeLazyStats = (namePrefix, description) => {
|
|
99
|
+
return { namePrefix, options: { description }, keys: new Set(), data: {} };
|
|
100
|
+
};
|
|
101
|
+
const dynamicAfterCommitStatsCounters = {
|
|
102
|
+
memoryUsage: makeLazyStats(
|
|
103
|
+
'memoryUsage_',
|
|
104
|
+
'kernel process memory statistic',
|
|
105
|
+
),
|
|
106
|
+
heapStats: makeLazyStats('heapStats_', 'v8 kernel heap statistic'),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const slogSender = ({ type: slogType, ...slogObj }) => {
|
|
110
|
+
switch (slogType) {
|
|
111
|
+
// Consume cosmic-swingset block lifecycle slog entries.
|
|
112
|
+
case 'cosmic-swingset-init': {
|
|
113
|
+
const { inboundQueueInitialLengths: lengths } = slogObj;
|
|
114
|
+
inboundQueueMetrics.initLengths(lengths);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case 'cosmic-swingset-begin-block': {
|
|
118
|
+
const {
|
|
119
|
+
interBlockSeconds,
|
|
120
|
+
afterCommitHangoverSeconds,
|
|
121
|
+
blockLagSeconds,
|
|
122
|
+
} = slogObj;
|
|
123
|
+
|
|
124
|
+
Number.isFinite(interBlockSeconds) &&
|
|
125
|
+
histograms.interBlockSeconds.record(interBlockSeconds);
|
|
126
|
+
histograms.afterCommitHangoverSeconds.record(
|
|
127
|
+
afterCommitHangoverSeconds,
|
|
128
|
+
);
|
|
129
|
+
Number.isFinite(blockLagSeconds) &&
|
|
130
|
+
histograms.blockLagSeconds.record(blockLagSeconds);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
case 'cosmic-swingset-run-finish': {
|
|
134
|
+
histograms.swingset_block_processing_seconds.record(slogObj.seconds);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case 'cosmic-swingset-end-block-finish': {
|
|
138
|
+
const { inboundQueueStartLengths, processedActionCounts } = slogObj;
|
|
139
|
+
inboundQueueMetrics.updateLengths(inboundQueueStartLengths);
|
|
140
|
+
for (const processedActionRecord of processedActionCounts) {
|
|
141
|
+
const { count, phase, type: actionType } = processedActionRecord;
|
|
142
|
+
if (!knownActionTypes.has(actionType)) {
|
|
143
|
+
console.warn('Unknown inbound action type', actionType);
|
|
144
|
+
}
|
|
145
|
+
processedInboundActionCounter.add(count, { actionType });
|
|
146
|
+
inboundQueueMetrics.decLength(phase);
|
|
147
|
+
}
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
case 'cosmic-swingset-commit-block-finish': {
|
|
151
|
+
const {
|
|
152
|
+
runSeconds,
|
|
153
|
+
chainTime,
|
|
154
|
+
saveTime,
|
|
155
|
+
cosmosCommitSeconds,
|
|
156
|
+
fullSaveTime,
|
|
157
|
+
} = slogObj;
|
|
158
|
+
histograms.swingsetRunSeconds.record(runSeconds);
|
|
159
|
+
histograms.swingsetChainSaveSeconds.record(chainTime);
|
|
160
|
+
histograms.swingsetCommitSeconds.record(saveTime);
|
|
161
|
+
histograms.cosmosCommitSeconds.record(cosmosCommitSeconds);
|
|
162
|
+
histograms.fullCommitSeconds.record(fullSaveTime);
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Consume Swingset kernel slog entries.
|
|
167
|
+
case 'vat-startup-finish': {
|
|
168
|
+
histograms.swingset_vat_startup.record(slogObj.seconds * 1000);
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
case 'crank-finish': {
|
|
172
|
+
const { crankType, messageType, seconds } = slogObj;
|
|
173
|
+
// TODO: Reflect crankType/messageType as proper dimensional attributes.
|
|
174
|
+
// For now, we're going for parity with direct metrics.
|
|
175
|
+
if (crankType !== 'routing' && messageType !== 'create-vat') {
|
|
176
|
+
histograms.swingset_crank_processing_time.record(seconds * 1000);
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Consume miscellaneous slog entries.
|
|
182
|
+
case 'kernel-stats': {
|
|
183
|
+
const { stats } = slogObj;
|
|
184
|
+
const notYetFoundKernelStats = new Set(expectedKernelStats);
|
|
185
|
+
for (const [key, value] of Object.entries(stats)) {
|
|
186
|
+
notYetFoundKernelStats.delete(key);
|
|
187
|
+
if (!kernelStats.has(key)) {
|
|
188
|
+
console.warn('Unexpected SwingSet kernel statistic', key);
|
|
189
|
+
}
|
|
190
|
+
kernelStats.set(key, value);
|
|
191
|
+
}
|
|
192
|
+
if (notYetFoundKernelStats.size) {
|
|
193
|
+
console.warn('Expected SwingSet kernel statistics not found', [
|
|
194
|
+
...notYetFoundKernelStats,
|
|
195
|
+
]);
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
case 'cosmic-swingset-after-commit-stats': {
|
|
200
|
+
const dynamicCounterEntries = Object.entries(
|
|
201
|
+
dynamicAfterCommitStatsCounters,
|
|
202
|
+
);
|
|
203
|
+
for (const [slogKey, meta] of dynamicCounterEntries) {
|
|
204
|
+
const { namePrefix, options, keys } = meta;
|
|
205
|
+
meta.data = slogObj[slogKey] || {};
|
|
206
|
+
const newKeys = Object.keys(meta.data).filter(key => !keys.has(key));
|
|
207
|
+
for (const key of newKeys) {
|
|
208
|
+
keys.add(key);
|
|
209
|
+
const name = `${namePrefix}${key}`;
|
|
210
|
+
const gauge = otelMeter.createObservableUpDownCounter(
|
|
211
|
+
name,
|
|
212
|
+
options,
|
|
213
|
+
);
|
|
214
|
+
gauge.addCallback(observer => {
|
|
215
|
+
observer.observe(meta.data[key]);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
default:
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
return Object.assign(slogSender, {
|
|
226
|
+
shutdown,
|
|
227
|
+
usesJsonObject: false,
|
|
228
|
+
});
|
|
229
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Fail } from '@endo/errors';
|
|
2
|
+
|
|
3
|
+
import { getPrometheusMeterProvider } from './index.js';
|
|
4
|
+
import { makeSlogSender as makeOtelMetricsSender } from './otel-metrics.js';
|
|
5
|
+
|
|
6
|
+
/** @param {import('./index.js').MakeSlogSenderOptions & {otelMeterName?: string}} opts */
|
|
7
|
+
export const makeSlogSender = async (opts = {}) => {
|
|
8
|
+
const { env, otelMeterName, serviceName } = opts;
|
|
9
|
+
if (!otelMeterName) throw Fail`OTel meter name is required`;
|
|
10
|
+
const otelMeterProvider = getPrometheusMeterProvider({
|
|
11
|
+
console,
|
|
12
|
+
env,
|
|
13
|
+
serviceName,
|
|
14
|
+
});
|
|
15
|
+
if (!otelMeterProvider) return;
|
|
16
|
+
|
|
17
|
+
return makeOtelMetricsSender({ ...opts, otelMeterName, otelMeterProvider });
|
|
18
|
+
};
|
|
@@ -1,4 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
)
|
|
1
|
+
const { hasOwn } = Object;
|
|
2
|
+
|
|
3
|
+
const replacer = (_key, value) => {
|
|
4
|
+
switch (typeof value) {
|
|
5
|
+
case 'object': {
|
|
6
|
+
if (value instanceof Error) {
|
|
7
|
+
// Represent each error as a serialization-friendly
|
|
8
|
+
// { errorType, message, cause?, errors?, stack? } object
|
|
9
|
+
// (itself subject to recursive replacement, particularly in `cause` and
|
|
10
|
+
// `errors`).
|
|
11
|
+
const obj = { errorType: value.name, message: value.message };
|
|
12
|
+
if (hasOwn(value, 'cause')) obj.cause = value.cause;
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
14
|
+
// @ts-ignore TS2339 property "errors" is only on AggregateError
|
|
15
|
+
if (hasOwn(value, 'errors')) obj.errors = value.errors;
|
|
16
|
+
const stack = value.stack;
|
|
17
|
+
if (stack) obj.stack = stack;
|
|
18
|
+
return obj;
|
|
19
|
+
}
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
case 'bigint':
|
|
23
|
+
// Represent each bigint as a JSON-serializable number, accepting the
|
|
24
|
+
// possible loss of precision.
|
|
25
|
+
return Number(value);
|
|
26
|
+
default:
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const serializeSlogObj = slogObj => JSON.stringify(slogObj, replacer);
|
|
@@ -1,32 +1,34 @@
|
|
|
1
1
|
/* eslint-env node */
|
|
2
|
+
/**
|
|
3
|
+
* @file Run as a child process of {@link ./slog-sender-pipe.js} to isolate an
|
|
4
|
+
* aggregate slog sender (@see {@link ./make-slog-sender.js}). Communicates
|
|
5
|
+
* with its parent via Node.js IPC with advanced (structured clone)
|
|
6
|
+
* serialization.
|
|
7
|
+
* https://nodejs.org/docs/latest/api/child_process.html#advanced-serialization
|
|
8
|
+
*/
|
|
9
|
+
|
|
2
10
|
import '@endo/init';
|
|
3
11
|
|
|
4
12
|
import anylogger from 'anylogger';
|
|
13
|
+
import { Fail } from '@endo/errors';
|
|
5
14
|
import { makeShutdown } from '@agoric/internal/src/node/shutdown.js';
|
|
6
15
|
|
|
7
16
|
import { makeSlogSender } from './make-slog-sender.js';
|
|
8
17
|
|
|
9
18
|
const logger = anylogger('slog-sender-pipe-entrypoint');
|
|
10
19
|
|
|
11
|
-
/** @type {(msg: import('./slog-sender-pipe.js').
|
|
20
|
+
/** @type {(msg: import('./slog-sender-pipe.js').PipeAPIReply) => void} */
|
|
12
21
|
const send = Function.prototype.bind.call(process.send, process);
|
|
13
22
|
|
|
14
23
|
/**
|
|
15
|
-
* @typedef {
|
|
16
|
-
* @
|
|
17
|
-
* @
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* @typedef {
|
|
21
|
-
* @
|
|
22
|
-
* @property {object} obj
|
|
24
|
+
* @typedef {{type: 'init', options: import('./index.js').MakeSlogSenderOptions }} InitMessage
|
|
25
|
+
* @typedef {{type: 'flush' }} FlushMessage
|
|
26
|
+
* @typedef {{type: 'send', obj: Record<string, unknown> }} SendMessage
|
|
27
|
+
*
|
|
28
|
+
* @typedef {InitMessage | FlushMessage} PipeAPIResponsefulMessage
|
|
29
|
+
* @typedef {SendMessage} PipeAPIResponselessMessage
|
|
30
|
+
* @typedef {PipeAPIResponsefulMessage | PipeAPIResponselessMessage} PipeAPIMessage
|
|
23
31
|
*/
|
|
24
|
-
/**
|
|
25
|
-
* @typedef {object} FlushMessage
|
|
26
|
-
* @property {'flush'} type
|
|
27
|
-
*/
|
|
28
|
-
/** @typedef {InitMessage | FlushMessage} SlogSenderPipeWaitMessages */
|
|
29
|
-
/** @typedef {SlogSenderPipeWaitMessages | SendMessage } SlogSenderPipeMessages */
|
|
30
32
|
|
|
31
33
|
const main = async () => {
|
|
32
34
|
/** @type {import('./index.js').SlogSender | undefined} */
|
|
@@ -44,9 +46,7 @@ const main = async () => {
|
|
|
44
46
|
|
|
45
47
|
/** @param {import('./index.js').MakeSlogSenderOptions} opts */
|
|
46
48
|
const init = async ({ env, ...otherOpts } = {}) => {
|
|
47
|
-
|
|
48
|
-
assert.fail('Already initialized');
|
|
49
|
-
}
|
|
49
|
+
!slogSender || Fail`Already initialized`;
|
|
50
50
|
|
|
51
51
|
slogSender = await makeSlogSender({
|
|
52
52
|
...otherOpts,
|
|
@@ -57,9 +57,7 @@ const main = async () => {
|
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
const flush = async () => {
|
|
60
|
-
if (!slogSender)
|
|
61
|
-
assert.fail('No sender available');
|
|
62
|
-
}
|
|
60
|
+
if (!slogSender) throw Fail`No sender available`;
|
|
63
61
|
|
|
64
62
|
await slogSender.forceFlush?.();
|
|
65
63
|
};
|
|
@@ -77,56 +75,55 @@ const main = async () => {
|
|
|
77
75
|
return AggregateError(sendErrors.splice(0));
|
|
78
76
|
};
|
|
79
77
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
78
|
+
/** @param {PipeAPIMessage} msg */
|
|
79
|
+
const onMessage = msg => {
|
|
80
|
+
if (!msg || typeof msg !== 'object') {
|
|
81
|
+
logger.warn('Received invalid message', msg);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
87
84
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
85
|
+
switch (msg.type) {
|
|
86
|
+
case 'init': {
|
|
87
|
+
void init(msg.options).then(
|
|
88
|
+
hasSender => {
|
|
89
|
+
send({ type: 'initReply', hasSender });
|
|
90
|
+
},
|
|
91
|
+
error => {
|
|
92
|
+
send({ type: 'initReply', hasSender: false, error });
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
case 'flush': {
|
|
98
|
+
void flush().then(
|
|
99
|
+
() => {
|
|
100
|
+
send({ type: 'flushReply', error: generateFlushError() });
|
|
101
|
+
},
|
|
102
|
+
error => {
|
|
103
|
+
send({ type: 'flushReply', error: generateFlushError(error) });
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case 'send': {
|
|
109
|
+
if (!slogSender) {
|
|
110
|
+
logger.warn('Received send with no sender available');
|
|
111
|
+
} else {
|
|
112
|
+
try {
|
|
113
|
+
slogSender(harden(msg.obj));
|
|
114
|
+
} catch (e) {
|
|
115
|
+
sendErrors.push(e);
|
|
120
116
|
}
|
|
121
|
-
break;
|
|
122
|
-
}
|
|
123
|
-
default: {
|
|
124
|
-
// @ts-expect-error exhaustive type check
|
|
125
|
-
logger.warn('received unknown message type', msg.type);
|
|
126
117
|
}
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
default: {
|
|
121
|
+
// @ts-expect-error exhaustive type check
|
|
122
|
+
logger.warn('Received unknown message type', msg.type);
|
|
127
123
|
}
|
|
128
|
-
}
|
|
129
|
-
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
process.on('message', onMessage);
|
|
130
127
|
};
|
|
131
128
|
|
|
132
129
|
process.exitCode = 1;
|