@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.
@@ -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
- export const serializeSlogObj = slogObj =>
2
- JSON.stringify(slogObj, (_, arg) =>
3
- typeof arg === 'bigint' ? Number(arg) : arg,
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').SlogSenderPipeWaitReplies) => void} */
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 {object} InitMessage
16
- * @property {'init'} type
17
- * @property {import('./index.js').MakeSlogSenderOptions} options
18
- */
19
- /**
20
- * @typedef {object} SendMessage
21
- * @property {'send'} type
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
- if (slogSender) {
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
- process.on(
81
- 'message',
82
- /** @param {SlogSenderPipeMessages} msg */ msg => {
83
- if (!msg || typeof msg !== 'object') {
84
- logger.warn('received invalid message', msg);
85
- return;
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
- switch (msg.type) {
89
- case 'init': {
90
- void init(msg.options).then(
91
- hasSender => {
92
- send({ type: 'initReply', hasSender });
93
- },
94
- error => {
95
- send({ type: 'initReply', hasSender: false, error });
96
- },
97
- );
98
- break;
99
- }
100
- case 'flush': {
101
- void flush().then(
102
- () => {
103
- send({ type: 'flushReply', error: generateFlushError() });
104
- },
105
- error => {
106
- send({ type: 'flushReply', error: generateFlushError(error) });
107
- },
108
- );
109
- break;
110
- }
111
- case 'send': {
112
- if (!slogSender) {
113
- logger.warn('received send with no sender available');
114
- } else {
115
- try {
116
- slogSender(msg.obj);
117
- } catch (e) {
118
- sendErrors.push(e);
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;