@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
package/src/slog-sender-pipe.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Export a `makeSlogSender` that spawns a
|
|
3
|
+
* {@link ./slog-sender-pipe-entrypoint.js} child process to which it forwards
|
|
4
|
+
* all slog entries via Node.js IPC with advanced (structured clone)
|
|
5
|
+
* serialization.
|
|
6
|
+
* https://nodejs.org/docs/latest/api/child_process.html#advanced-serialization
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
import { fork } from 'child_process';
|
|
2
10
|
import path from 'path';
|
|
11
|
+
import { promisify } from 'util';
|
|
3
12
|
import anylogger from 'anylogger';
|
|
4
13
|
|
|
14
|
+
import { q, Fail } from '@endo/errors';
|
|
15
|
+
import { makePromiseKit } from '@endo/promise-kit';
|
|
5
16
|
import { makeQueue } from '@endo/stream';
|
|
6
17
|
|
|
7
18
|
import { makeShutdown } from '@agoric/internal/src/node/shutdown.js';
|
|
@@ -10,6 +21,8 @@ const dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
|
10
21
|
|
|
11
22
|
const logger = anylogger('slog-sender-pipe');
|
|
12
23
|
|
|
24
|
+
const sink = () => {};
|
|
25
|
+
|
|
13
26
|
/**
|
|
14
27
|
* @template {any[]} T
|
|
15
28
|
* @template R
|
|
@@ -23,168 +36,131 @@ const withMutex = operation => {
|
|
|
23
36
|
return async (...args) => {
|
|
24
37
|
await mutex.get();
|
|
25
38
|
const result = operation(...args);
|
|
26
|
-
mutex.put(
|
|
27
|
-
result.then(
|
|
28
|
-
() => {},
|
|
29
|
-
() => {},
|
|
30
|
-
),
|
|
31
|
-
);
|
|
39
|
+
mutex.put(result.then(sink, sink));
|
|
32
40
|
return result;
|
|
33
41
|
};
|
|
34
42
|
};
|
|
35
43
|
|
|
36
44
|
/**
|
|
37
|
-
* @
|
|
38
|
-
* @
|
|
39
|
-
* @property {boolean} hasSender
|
|
40
|
-
* @property {Error} [error]
|
|
45
|
+
* @template [P=unknown]
|
|
46
|
+
* @typedef {{ type: string, error?: Error } & P} PipeReply
|
|
41
47
|
*/
|
|
48
|
+
|
|
42
49
|
/**
|
|
43
|
-
* @typedef {
|
|
44
|
-
*
|
|
45
|
-
*
|
|
50
|
+
* @typedef {{
|
|
51
|
+
* init: {
|
|
52
|
+
* message: import('./slog-sender-pipe-entrypoint.js').InitMessage;
|
|
53
|
+
* reply: PipeReply<{ hasSender: boolean }>;
|
|
54
|
+
* };
|
|
55
|
+
* flush: {
|
|
56
|
+
* message: import('./slog-sender-pipe-entrypoint.js').FlushMessage;
|
|
57
|
+
* reply: PipeReply<{}>;
|
|
58
|
+
* };
|
|
59
|
+
* }} SlogSenderPipeAPI
|
|
60
|
+
*
|
|
61
|
+
* @typedef {keyof SlogSenderPipeAPI} PipeAPICommand
|
|
62
|
+
* @typedef {SlogSenderPipeAPI[PipeAPICommand]["reply"]} PipeAPIReply
|
|
46
63
|
*/
|
|
47
|
-
/** @typedef {SlogSenderInitReply | SlogSenderFlushReply} SlogSenderPipeWaitReplies */
|
|
48
64
|
|
|
49
|
-
/** @param {import('.').MakeSlogSenderOptions}
|
|
50
|
-
export const makeSlogSender = async
|
|
65
|
+
/** @param {import('.').MakeSlogSenderOptions} options */
|
|
66
|
+
export const makeSlogSender = async options => {
|
|
67
|
+
const { env = {} } = options;
|
|
51
68
|
const { registerShutdown } = makeShutdown();
|
|
69
|
+
|
|
52
70
|
const cp = fork(path.join(dirname, 'slog-sender-pipe-entrypoint.js'), [], {
|
|
53
|
-
stdio: ['
|
|
71
|
+
stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
|
|
54
72
|
serialization: 'advanced',
|
|
73
|
+
env,
|
|
55
74
|
});
|
|
56
75
|
// logger.log('done fork');
|
|
57
76
|
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
resolve();
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
})
|
|
74
|
-
),
|
|
75
|
-
);
|
|
77
|
+
const exitKit = makePromiseKit();
|
|
78
|
+
cp.on('error', error => {
|
|
79
|
+
// An exit event *might* be coming, so wait a tick.
|
|
80
|
+
setImmediate(() => exitKit.resolve({ error }));
|
|
81
|
+
});
|
|
82
|
+
cp.on('exit', (exitCode, signal) => {
|
|
83
|
+
exitKit.resolve({ exitCode, signal });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/** @type {(msg: Record<string, unknown> & {type: string}) => Promise<void>} */
|
|
87
|
+
const rawSend = promisify(cp.send.bind(cp));
|
|
88
|
+
const pipeSend = withMutex(rawSend);
|
|
76
89
|
|
|
77
|
-
/**
|
|
78
|
-
* @typedef {{
|
|
79
|
-
* init: {
|
|
80
|
-
* message: import('./slog-sender-pipe-entrypoint.js').InitMessage;
|
|
81
|
-
* reply: SlogSenderInitReply;
|
|
82
|
-
* };
|
|
83
|
-
* flush: {
|
|
84
|
-
* message: import('./slog-sender-pipe-entrypoint.js').FlushMessage;
|
|
85
|
-
* reply: SlogSenderFlushReply;
|
|
86
|
-
* };
|
|
87
|
-
* }} SlogSenderWaitMessagesAndReplies
|
|
88
|
-
*/
|
|
89
|
-
|
|
90
|
-
/** @typedef {keyof SlogSenderWaitMessagesAndReplies} SendWaitCommands */
|
|
91
|
-
/**
|
|
92
|
-
* @template {SlogSenderPipeWaitReplies} T
|
|
93
|
-
* @typedef {Omit<T, 'type' | 'error'>} ReplyPayload
|
|
94
|
-
*/
|
|
95
|
-
|
|
96
|
-
/** @type {import('@endo/stream').AsyncQueue<SlogSenderPipeWaitReplies>} */
|
|
90
|
+
/** @type {import('@endo/stream').AsyncQueue<PipeAPIReply>} */
|
|
97
91
|
const sendWaitQueue = makeQueue();
|
|
98
|
-
/** @type {
|
|
92
|
+
/** @type {PipeAPICommand | undefined} */
|
|
99
93
|
let sendWaitType;
|
|
100
94
|
|
|
101
95
|
const sendWaitReply = withMutex(
|
|
102
96
|
/**
|
|
103
|
-
* @template {
|
|
97
|
+
* @template {PipeAPICommand} T
|
|
104
98
|
* @param {T} type
|
|
105
|
-
* @param {Omit<
|
|
106
|
-
* @returns {Promise<
|
|
99
|
+
* @param {Omit<SlogSenderPipeAPI[T]["message"], 'type'>} payload
|
|
100
|
+
* @returns {Promise<Omit<SlogSenderPipeAPI[T]["reply"], keyof PipeReply>>}
|
|
107
101
|
*/
|
|
108
102
|
async (type, payload) => {
|
|
109
|
-
!sendWaitType ||
|
|
103
|
+
!sendWaitType || Fail`Invalid mutex state`;
|
|
110
104
|
|
|
111
105
|
const msg = { ...payload, type };
|
|
112
106
|
|
|
113
107
|
sendWaitType = type;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
return rest;
|
|
128
|
-
},
|
|
129
|
-
)
|
|
130
|
-
.finally(() => {
|
|
131
|
-
sendWaitType = undefined;
|
|
132
|
-
});
|
|
133
|
-
},
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
cp.on(
|
|
137
|
-
'message',
|
|
138
|
-
/** @param { SlogSenderPipeWaitReplies } msg */
|
|
139
|
-
msg => {
|
|
140
|
-
// logger.log('received', msg);
|
|
141
|
-
if (
|
|
142
|
-
!msg ||
|
|
143
|
-
typeof msg !== 'object' ||
|
|
144
|
-
msg.type !== `${sendWaitType}Reply`
|
|
145
|
-
) {
|
|
146
|
-
logger.warn('Received unexpected message', msg);
|
|
147
|
-
return;
|
|
108
|
+
await null;
|
|
109
|
+
try {
|
|
110
|
+
await pipeSend(msg);
|
|
111
|
+
/** @type {SlogSenderPipeAPI[T]["reply"]} */
|
|
112
|
+
const reply = await sendWaitQueue.get();
|
|
113
|
+
const { type: replyType, error, ...rest } = reply;
|
|
114
|
+
replyType === `${type}Reply` ||
|
|
115
|
+
Fail`Unexpected reply type ${q(replyType)}`;
|
|
116
|
+
if (error) throw error;
|
|
117
|
+
return rest;
|
|
118
|
+
} finally {
|
|
119
|
+
sendWaitType = undefined;
|
|
148
120
|
}
|
|
149
|
-
|
|
150
|
-
sendWaitQueue.put(msg);
|
|
151
121
|
},
|
|
152
122
|
);
|
|
153
123
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
124
|
+
/** @param {PipeReply} msg */
|
|
125
|
+
const onMessage = msg => {
|
|
126
|
+
// logger.log('received', msg);
|
|
127
|
+
if (!msg || msg.type !== `${sendWaitType}Reply`) {
|
|
128
|
+
logger.warn('Received unexpected message', msg);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
sendWaitQueue.put(msg);
|
|
133
|
+
};
|
|
134
|
+
cp.on('message', onMessage);
|
|
157
135
|
|
|
158
|
-
const
|
|
159
|
-
|
|
136
|
+
const flush = async () => {
|
|
137
|
+
await sendWaitReply('flush', {});
|
|
160
138
|
};
|
|
161
139
|
|
|
162
140
|
const shutdown = async () => {
|
|
163
141
|
// logger.log('shutdown');
|
|
164
|
-
if (!cp.connected)
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
142
|
+
if (!cp.connected) return;
|
|
167
143
|
|
|
168
144
|
await flush();
|
|
169
145
|
cp.disconnect();
|
|
146
|
+
await exitKit.promise;
|
|
170
147
|
};
|
|
171
148
|
registerShutdown(shutdown);
|
|
172
149
|
|
|
173
|
-
const { hasSender } = await init
|
|
150
|
+
const { hasSender } = await sendWaitReply('init', { options }).catch(err => {
|
|
174
151
|
cp.disconnect();
|
|
175
152
|
throw err;
|
|
176
153
|
});
|
|
177
|
-
|
|
178
154
|
if (!hasSender) {
|
|
179
155
|
cp.disconnect();
|
|
180
156
|
return undefined;
|
|
181
157
|
}
|
|
182
158
|
|
|
183
|
-
const slogSender =
|
|
159
|
+
const slogSender = obj => {
|
|
160
|
+
void pipeSend({ type: 'send', obj }).catch(sink);
|
|
161
|
+
};
|
|
184
162
|
return Object.assign(slogSender, {
|
|
185
|
-
forceFlush:
|
|
186
|
-
await flush();
|
|
187
|
-
},
|
|
163
|
+
forceFlush: flush,
|
|
188
164
|
shutdown,
|
|
189
165
|
usesJsonObject: false,
|
|
190
166
|
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import { promisify } from 'node:util';
|
|
3
2
|
import tmp from 'tmp';
|
|
4
3
|
import { test } from './prepare-test-env-ava.js';
|
|
5
4
|
|
|
@@ -13,36 +12,48 @@ const bufferTests = test.macro(
|
|
|
13
12
|
/**
|
|
14
13
|
*
|
|
15
14
|
* @param {*} t
|
|
16
|
-
* @param {{makeBuffer:
|
|
15
|
+
* @param {{makeBuffer: typeof makeSimpleCircularBuffer}} input
|
|
17
16
|
*/
|
|
18
17
|
async (t, input) => {
|
|
19
18
|
const BUFFER_SIZE = 512;
|
|
20
19
|
|
|
21
|
-
const {
|
|
22
|
-
|
|
23
|
-
fd,
|
|
24
|
-
removeCallback,
|
|
25
|
-
} = tmp.fileSync({ detachDescriptor: true });
|
|
26
|
-
t.teardown(removeCallback);
|
|
27
|
-
const fileHandle = /** @type {import('fs/promises').FileHandle} */ ({
|
|
28
|
-
close: promisify(fs.close.bind(fs, fd)),
|
|
20
|
+
const { name: tmpFile, removeCallback } = tmp.fileSync({
|
|
21
|
+
discardDescriptor: true,
|
|
29
22
|
});
|
|
30
|
-
|
|
23
|
+
t.teardown(removeCallback);
|
|
24
|
+
const { fileHandle, readCircBuf, writeCircBuf } = await input.makeBuffer({
|
|
31
25
|
circularBufferSize: BUFFER_SIZE,
|
|
32
26
|
circularBufferFilename: tmpFile,
|
|
33
27
|
});
|
|
34
|
-
const
|
|
35
|
-
|
|
28
|
+
const realSlogSender = makeSlogSenderFromBuffer({
|
|
29
|
+
fileHandle,
|
|
30
|
+
writeCircBuf,
|
|
31
|
+
});
|
|
32
|
+
let wasShutdown = false;
|
|
33
|
+
const shutdown = () => {
|
|
34
|
+
if (wasShutdown) return;
|
|
35
|
+
wasShutdown = true;
|
|
36
|
+
|
|
37
|
+
return realSlogSender.shutdown();
|
|
38
|
+
};
|
|
39
|
+
t.teardown(shutdown);
|
|
40
|
+
// To verify lack of attempted mutation by the consumer, send only hardened
|
|
41
|
+
// entries.
|
|
42
|
+
/** @type {typeof realSlogSender} */
|
|
43
|
+
const slogSender = Object.assign(
|
|
44
|
+
(obj, serialized) => realSlogSender(harden(obj), serialized),
|
|
45
|
+
realSlogSender,
|
|
46
|
+
);
|
|
36
47
|
slogSender({ type: 'start' });
|
|
37
48
|
await slogSender.forceFlush();
|
|
38
49
|
t.is(fs.readFileSync(tmpFile, { encoding: 'utf8' }).length, BUFFER_SIZE);
|
|
39
50
|
|
|
40
51
|
const len0 = new Uint8Array(BigUint64Array.BYTES_PER_ELEMENT);
|
|
41
|
-
const { done: done0 } = readCircBuf(len0);
|
|
52
|
+
const { done: done0 } = await readCircBuf(len0);
|
|
42
53
|
t.false(done0, 'readCircBuf should not be done');
|
|
43
54
|
const dv0 = new DataView(len0.buffer);
|
|
44
55
|
const buf0 = new Uint8Array(Number(dv0.getBigUint64(0)));
|
|
45
|
-
const { done: done0b } = readCircBuf(buf0, len0.byteLength);
|
|
56
|
+
const { done: done0b } = await readCircBuf(buf0, len0.byteLength);
|
|
46
57
|
t.false(done0b, 'readCircBuf should not be done');
|
|
47
58
|
const buf0Str = new TextDecoder().decode(buf0);
|
|
48
59
|
t.is(buf0Str, `\n{"type":"start"}`, `start compare failed`);
|
|
@@ -61,12 +72,12 @@ const bufferTests = test.macro(
|
|
|
61
72
|
let offset = 0;
|
|
62
73
|
const len1 = new Uint8Array(BigUint64Array.BYTES_PER_ELEMENT);
|
|
63
74
|
for (let i = 490; i < last; i += 1) {
|
|
64
|
-
const { done: done1 } = readCircBuf(len1, offset);
|
|
75
|
+
const { done: done1 } = await readCircBuf(len1, offset);
|
|
65
76
|
offset += len1.byteLength;
|
|
66
77
|
t.false(done1, `readCircBuf ${i} should not be done`);
|
|
67
78
|
const dv1 = new DataView(len1.buffer);
|
|
68
79
|
const buf1 = new Uint8Array(Number(dv1.getBigUint64(0)));
|
|
69
|
-
const { done: done1b } = readCircBuf(buf1, offset);
|
|
80
|
+
const { done: done1b } = await readCircBuf(buf1, offset);
|
|
70
81
|
offset += buf1.byteLength;
|
|
71
82
|
t.false(done1b, `readCircBuf ${i} should not be done`);
|
|
72
83
|
const buf1Str = new TextDecoder().decode(buf1);
|
|
@@ -77,12 +88,24 @@ const bufferTests = test.macro(
|
|
|
77
88
|
);
|
|
78
89
|
}
|
|
79
90
|
|
|
80
|
-
const { done: done2 } = readCircBuf(len1, offset);
|
|
91
|
+
const { done: done2 } = await readCircBuf(len1, offset);
|
|
81
92
|
t.assert(done2, `readCircBuf ${last} should be done`);
|
|
82
93
|
|
|
83
94
|
slogSender(null, 'PRE-SERIALIZED');
|
|
84
95
|
await slogSender.forceFlush();
|
|
85
96
|
t.truthy(fs.readFileSync(tmpFile).includes('PRE-SERIALIZED'));
|
|
97
|
+
|
|
98
|
+
slogSender(null, 'PRE_SHUTDOWN');
|
|
99
|
+
const shutdownP = shutdown();
|
|
100
|
+
slogSender(null, 'POST_SHUTDOWN');
|
|
101
|
+
await shutdownP;
|
|
102
|
+
slogSender(null, 'SHUTDOWN_COMPLETED');
|
|
103
|
+
|
|
104
|
+
const finalContent = fs.readFileSync(tmpFile);
|
|
105
|
+
|
|
106
|
+
t.truthy(finalContent.includes('PRE_SHUTDOWN'));
|
|
107
|
+
t.falsy(finalContent.includes('POST_SHUTDOWN'));
|
|
108
|
+
t.falsy(finalContent.includes('SHUTDOWN_COMPLETED'));
|
|
86
109
|
},
|
|
87
110
|
);
|
|
88
111
|
|