@reventlessdev/reventless-local 3.0.0-alpha.316 → 3.0.0-alpha.318

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +11 -11
  3. package/src/LocalPlatformRegistry.res +19 -0
  4. package/src/LocalPlatformRegistry.res.mjs +19 -1
  5. package/src/Platform.res +24 -2
  6. package/src/Platform.res.mjs +22 -2
  7. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +38 -0
  8. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +29 -0
  9. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +87 -0
  10. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +76 -0
  11. package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res +8 -1
  12. package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs +17 -4
  13. package/src/adapter/EventLog/EventLogStorage_InMemory.res +49 -2
  14. package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +57 -21
  15. package/src/adapter/EventLog/EventLogStorage_Sqlite.res +85 -0
  16. package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +50 -0
  17. package/src/adapter/EventLog/LocalEventLogStorage.res +3 -0
  18. package/src/adapter/EventLog/LocalEventLogStorage.res.mjs +6 -2
  19. package/src/adapter/LocalBus.res +32 -5
  20. package/src/adapter/LocalBus.res.mjs +27 -7
  21. package/src/adapter/LocalEventLogRead.res +477 -0
  22. package/src/adapter/LocalEventLogRead.res.mjs +507 -0
  23. package/src/adapter/LocalEventTap.res +18 -80
  24. package/src/adapter/LocalEventTap.res.mjs +19 -108
  25. package/src/adapter/LocalFeed.res +433 -0
  26. package/src/adapter/LocalFeed.res.mjs +419 -0
  27. package/src/adapter/LocalLogFeed.res +217 -0
  28. package/src/adapter/LocalLogFeed.res.mjs +295 -0
  29. package/src/adapter/LocalPgEventLogRead.res +85 -0
  30. package/src/adapter/LocalPgEventLogRead.res.mjs +91 -0
  31. package/src/adapter/LocalPlatformObserve.res +456 -0
  32. package/src/adapter/LocalPlatformObserve.res.mjs +505 -0
  33. package/src/adapter/LocalTapLines.res +147 -0
  34. package/src/adapter/LocalTapLines.res.mjs +229 -0
  35. package/src/adapter/PlatformGraphQL_Server.res +36 -3
  36. package/src/adapter/PlatformGraphQL_Server.res.mjs +15 -1
  37. package/src/reexport/CommandTopic.res.mjs +5 -2
  38. package/tests/adapter/GraphQLWsFixtures.mjs +46 -0
  39. package/tests/adapter/LocalEventLogReadTest.res +539 -0
  40. package/tests/adapter/LocalEventLogReadTest.res.mjs +466 -0
  41. package/tests/adapter/LocalFeedTest.res +314 -0
  42. package/tests/adapter/LocalFeedTest.res.mjs +329 -0
  43. package/tests/adapter/LocalPlatformObserveTest.res +165 -0
  44. package/tests/adapter/LocalPlatformObserveTest.res.mjs +183 -0
  45. package/tests/adapter/LocalTapLinesTest.res +182 -0
  46. package/tests/adapter/LocalTapLinesTest.res.mjs +242 -0
  47. package/tests/adapter/PlatformSubscriptionsTest.res +123 -0
  48. package/tests/adapter/PlatformSubscriptionsTest.res.mjs +132 -0
  49. package/tests/components/automationslice/AutomationSliceCallbackTest.res +80 -0
  50. package/tests/components/automationslice/AutomationSliceCallbackTest.res.mjs +81 -0
  51. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +17 -0
  52. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +24 -0
  53. package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res +30 -0
  54. package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res.mjs +28 -0
  55. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +58 -0
  56. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs +71 -19
  57. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePiiTest.res.mjs +7 -7
@@ -0,0 +1,419 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Nodenet from "node:net";
4
+ import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
5
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
6
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
7
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
8
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
9
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
10
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
11
+ import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
12
+
13
+ let log = Logger$ReventlessCore.fromEnv();
14
+
15
+ function settingOf(value) {
16
+ let v = Stdlib_Option.filter(Stdlib_Option.map(value, prim => prim.trim()), v => v !== "");
17
+ if (v === undefined) {
18
+ return "Ephemeral";
19
+ }
20
+ switch (v) {
21
+ case "false" :
22
+ case "none" :
23
+ case "off" :
24
+ return "Off";
25
+ default:
26
+ let p = Stdlib_Int.fromString(v, undefined);
27
+ if (p !== undefined && p >= 1024 && p <= 65535) {
28
+ return {
29
+ TAG: "Fixed",
30
+ _0: p
31
+ };
32
+ } else {
33
+ return "Ephemeral";
34
+ }
35
+ }
36
+ }
37
+
38
+ function levelOfString(s) {
39
+ let match = s.toLowerCase();
40
+ switch (match) {
41
+ case "debug" :
42
+ return "Debug";
43
+ case "error" :
44
+ return "Error";
45
+ case "info" :
46
+ return "Info";
47
+ case "warn" :
48
+ case "warning" :
49
+ return "Warn";
50
+ default:
51
+ return;
52
+ }
53
+ }
54
+
55
+ function parseFilter(line) {
56
+ let d;
57
+ try {
58
+ d = Stdlib_JSON.Decode.object(JSON.parse(line));
59
+ } catch (exn) {
60
+ return;
61
+ }
62
+ if (d === undefined) {
63
+ return;
64
+ }
65
+ let strings = k => Stdlib_Option.map(Stdlib_Option.flatMap(d[k], Stdlib_JSON.Decode.array), a => Stdlib_Array.filterMap(a, Stdlib_JSON.Decode.string));
66
+ let string = k => Stdlib_Option.flatMap(d[k], Stdlib_JSON.Decode.string);
67
+ return {
68
+ plugins: strings("plugins"),
69
+ level: Stdlib_Option.flatMap(string("level"), levelOfString),
70
+ components: strings("components"),
71
+ eventTypes: strings("eventTypes"),
72
+ correlationId: string("correlationId")
73
+ };
74
+ }
75
+
76
+ function matches(honours, filter, facets) {
77
+ let oneOf = (wanted, field, value) => {
78
+ if (wanted !== undefined && honours.includes(field)) {
79
+ return Stdlib_Option.mapOr(value, false, v => wanted.includes(v));
80
+ } else {
81
+ return true;
82
+ }
83
+ };
84
+ let tmp = false;
85
+ if (oneOf(filter.plugins, "Plugins", facets.plugin) && oneOf(filter.components, "Components", facets.comp) && oneOf(filter.eventTypes, "EventTypes", facets.eventType)) {
86
+ let id = filter.correlationId;
87
+ tmp = id !== undefined && honours.includes("CorrelationId") ? Primitive_object.equal(facets.correlationId, id) : true;
88
+ }
89
+ if (!tmp) {
90
+ return false;
91
+ }
92
+ let min = filter.level;
93
+ if (min !== undefined && honours.includes("Level")) {
94
+ return Stdlib_Option.mapOr(facets.level, false, l => Logger$ReventlessCore.levelToInt(l) >= Logger$ReventlessCore.levelToInt(min));
95
+ } else {
96
+ return true;
97
+ }
98
+ }
99
+
100
+ function skippedLine(count) {
101
+ return JSON.stringify(Object.fromEntries([
102
+ [
103
+ "kind",
104
+ "skipped"
105
+ ],
106
+ [
107
+ "count",
108
+ count
109
+ ]
110
+ ]));
111
+ }
112
+
113
+ function socketSink(socket) {
114
+ return {
115
+ write: line => socket.write(line + "\n"),
116
+ onceDrain: f => {
117
+ socket.once("drain", f);
118
+ },
119
+ buffered: () => Stdlib_Option.getOr(Primitive_option.fromNullable(socket.writableLength), 0),
120
+ close: () => {
121
+ socket.destroy();
122
+ }
123
+ };
124
+ }
125
+
126
+ function make(name, honours, backlog, maxLinesOpt, maxBytesOpt, filterWaitMsOpt, param) {
127
+ let maxLines = maxLinesOpt !== undefined ? maxLinesOpt : 10000;
128
+ let maxBytes = maxBytesOpt !== undefined ? maxBytesOpt : 8388608;
129
+ let filterWaitMs = filterWaitMsOpt !== undefined ? filterWaitMsOpt : 100;
130
+ return {
131
+ name: name,
132
+ honours: honours,
133
+ maxLines: maxLines,
134
+ maxBytes: maxBytes,
135
+ filterWaitMs: filterWaitMs,
136
+ backlog: backlog,
137
+ listeners: [],
138
+ server: undefined,
139
+ port: undefined
140
+ };
141
+ }
142
+
143
+ function port(feed) {
144
+ return feed.port;
145
+ }
146
+
147
+ function isLive(l) {
148
+ let match = l.phase;
149
+ return match !== "Waiting";
150
+ }
151
+
152
+ function hasReaders(feed) {
153
+ return feed.listeners.some(isLive);
154
+ }
155
+
156
+ function queuedLines(l) {
157
+ return l.queue.length - l.head | 0;
158
+ }
159
+
160
+ function queuedBytes(feed) {
161
+ return Stdlib_Array.reduce(feed.listeners, 0, (sum, l) => sum + l.queuedBytes | 0);
162
+ }
163
+
164
+ function heldBytes(feed) {
165
+ return Stdlib_Array.reduce(feed.listeners, 0, (sum, l) => (sum + l.queuedBytes | 0) + l.sink.buffered() | 0);
166
+ }
167
+
168
+ function drop(feed, l) {
169
+ Stdlib_Option.forEach(l.timer, prim => {
170
+ clearTimeout(prim);
171
+ });
172
+ feed.listeners = feed.listeners.filter(other => other !== l);
173
+ }
174
+
175
+ function dequeue(l) {
176
+ if (queuedLines(l) === 0) {
177
+ return;
178
+ }
179
+ let line = l.queue[l.head];
180
+ l.head = l.head + 1 | 0;
181
+ l.queuedBytes = l.queuedBytes - line.length | 0;
182
+ if (l.head > 1024 && (l.head << 1) > l.queue.length) {
183
+ l.queue = l.queue.slice(l.head, l.queue.length);
184
+ l.head = 0;
185
+ }
186
+ return line;
187
+ }
188
+
189
+ function pump(feed, l) {
190
+ let $$continue = true;
191
+ while ($$continue && !l.blocked) {
192
+ let next;
193
+ if (l.skipped > 0) {
194
+ let line = skippedLine(l.skipped);
195
+ l.skipped = 0;
196
+ next = line;
197
+ } else {
198
+ next = dequeue(l);
199
+ }
200
+ if (next !== undefined) {
201
+ let exit = 0;
202
+ let val;
203
+ try {
204
+ val = l.sink.write(next);
205
+ exit = 1;
206
+ } catch (exn) {
207
+ drop(feed, l);
208
+ $$continue = false;
209
+ }
210
+ if (exit === 1) {
211
+ if (val) {
212
+
213
+ } else {
214
+ l.blocked = true;
215
+ l.sink.onceDrain(() => {
216
+ l.blocked = false;
217
+ pump(feed, l);
218
+ });
219
+ }
220
+ }
221
+ } else {
222
+ $$continue = false;
223
+ }
224
+ };
225
+ }
226
+
227
+ function enqueue(feed, l, line) {
228
+ l.queue.push(line);
229
+ l.queuedBytes = l.queuedBytes + line.length | 0;
230
+ while (queuedLines(l) > 0 && (queuedLines(l) > feed.maxLines || l.queuedBytes > feed.maxBytes)) {
231
+ dequeue(l);
232
+ l.skipped = l.skipped + 1 | 0;
233
+ };
234
+ }
235
+
236
+ function offer(feed, l, item) {
237
+ if (isLive(l) && matches(feed.honours, l.filter, item.facets)) {
238
+ enqueue(feed, l, item.line);
239
+ return pump(feed, l);
240
+ }
241
+ }
242
+
243
+ function publish(feed, item) {
244
+ feed.listeners.forEach(l => offer(feed, l, item));
245
+ }
246
+
247
+ function broadcast(feed, line) {
248
+ publish(feed, {
249
+ line: line,
250
+ facets: {}
251
+ });
252
+ }
253
+
254
+ function start_(feed, l, filter) {
255
+ if (!isLive(l)) {
256
+ Stdlib_Option.forEach(l.timer, prim => {
257
+ clearTimeout(prim);
258
+ });
259
+ l.timer = undefined;
260
+ Stdlib_Option.forEach(filter, f => {
261
+ l.filter = f;
262
+ });
263
+ l.phase = "Live";
264
+ Stdlib_Option.forEach(feed.backlog, backlog => {
265
+ backlog().forEach(item => {
266
+ if (matches(feed.honours, l.filter, item.facets)) {
267
+ return enqueue(feed, l, item.line);
268
+ }
269
+ });
270
+ });
271
+ return pump(feed, l);
272
+ }
273
+ }
274
+
275
+ function newListener(sink, phase) {
276
+ return {
277
+ sink: sink,
278
+ phase: phase,
279
+ filter: {},
280
+ queue: [],
281
+ head: 0,
282
+ queuedBytes: 0,
283
+ skipped: 0,
284
+ blocked: false,
285
+ inbound: "",
286
+ timer: undefined
287
+ };
288
+ }
289
+
290
+ function accept(feed, socket) {
291
+ let l = newListener(socketSink(socket), "Waiting");
292
+ socket.setEncoding("utf8");
293
+ socket.on("error", param => drop(feed, l));
294
+ socket.on("close", () => drop(feed, l));
295
+ socket.on("data", chunk => {
296
+ if (isLive(l)) {
297
+ return;
298
+ }
299
+ l.inbound = l.inbound + chunk;
300
+ let i = l.inbound.indexOf("\n");
301
+ if (i === -1) {
302
+ if (l.inbound.length > 65536) {
303
+ l.inbound = "";
304
+ return start_(feed, l, undefined);
305
+ } else {
306
+ return;
307
+ }
308
+ }
309
+ let line = l.inbound.slice(0, i);
310
+ l.inbound = "";
311
+ let f = parseFilter(line);
312
+ if (f !== undefined) {
313
+ return start_(feed, l, f);
314
+ } else {
315
+ log.warn(feed.name, undefined, `ignored a filter line that is not a JSON object`);
316
+ return start_(feed, l, undefined);
317
+ }
318
+ });
319
+ l.timer = Primitive_option.some(setTimeout(() => start_(feed, l, undefined), feed.filterWaitMs));
320
+ feed.listeners = feed.listeners.concat([l]);
321
+ }
322
+
323
+ function start(feed, setting, onBoundOpt, param) {
324
+ let onBound = onBoundOpt !== undefined ? onBoundOpt : param => {};
325
+ let match = feed.server;
326
+ if (typeof setting !== "object" && setting === "Off") {
327
+ return;
328
+ }
329
+ if (match !== undefined) {
330
+ return;
331
+ }
332
+ let requested;
333
+ requested = typeof setting !== "object" ? 0 : setting._0;
334
+ let s = Nodenet.createServer(socket => accept(feed, socket));
335
+ s.on("error", err => {
336
+ log.warn(feed.name, undefined, `could not listen: ` + Stdlib_Option.getOr(Stdlib_JsExn.message(err), "unknown error"));
337
+ feed.server = undefined;
338
+ feed.port = undefined;
339
+ });
340
+ s.unref();
341
+ s.listen(requested, "127.0.0.1", () => {
342
+ let addr = s.address();
343
+ if (addr == null) {
344
+ return;
345
+ }
346
+ let bound = addr.port;
347
+ feed.port = bound;
348
+ log.info(feed.name, undefined, `listening on 127.0.0.1:` + bound.toString());
349
+ onBound(bound);
350
+ });
351
+ feed.server = Primitive_option.some(s);
352
+ }
353
+
354
+ function resetForTests(feed) {
355
+ feed.listeners.forEach(l => Stdlib_Option.forEach(l.timer, prim => {
356
+ clearTimeout(prim);
357
+ }));
358
+ feed.listeners = [];
359
+ feed.server = undefined;
360
+ feed.port = undefined;
361
+ }
362
+
363
+ function addConnectionForTests(feed, socket) {
364
+ feed.listeners = feed.listeners.concat([newListener(socketSink(socket), "Live")]);
365
+ }
366
+
367
+ function subscribe(feed, filter, sink) {
368
+ let l = newListener(sink, "Waiting");
369
+ feed.listeners = feed.listeners.concat([l]);
370
+ start_(feed, l, filter);
371
+ return () => drop(feed, l);
372
+ }
373
+
374
+ function stopForTests(feed) {
375
+ let s = feed.server;
376
+ if (s !== undefined) {
377
+ let s$1 = Primitive_option.valFromOption(s);
378
+ feed.listeners.forEach(l => l.sink.close());
379
+ resetForTests(feed);
380
+ return new Promise((resolve, _reject) => {
381
+ s$1.close(() => resolve());
382
+ });
383
+ }
384
+ resetForTests(feed);
385
+ return Promise.resolve();
386
+ }
387
+
388
+ export {
389
+ log,
390
+ settingOf,
391
+ levelOfString,
392
+ parseFilter,
393
+ matches,
394
+ skippedLine,
395
+ socketSink,
396
+ make,
397
+ port,
398
+ isLive,
399
+ hasReaders,
400
+ queuedLines,
401
+ queuedBytes,
402
+ heldBytes,
403
+ drop,
404
+ dequeue,
405
+ pump,
406
+ enqueue,
407
+ offer,
408
+ publish,
409
+ broadcast,
410
+ start_,
411
+ newListener,
412
+ accept,
413
+ start,
414
+ resetForTests,
415
+ addConnectionForTests,
416
+ subscribe,
417
+ stopForTests,
418
+ }
419
+ /* log Not a pure module */
@@ -0,0 +1,217 @@
1
+ // The local platform's recent log records, and the feed that serves them to a
2
+ // tool that did not start the platform and so cannot read its stdout.
3
+ //
4
+ // Two rings keep what was logged: the most recent records, and, separately, the
5
+ // most recent warnings and errors, so a problem outlives a busy minute. Both are
6
+ // bounded by count and by bytes. `REVENTLESS_LOG_RING` and
7
+ // `REVENTLESS_LOG_PROBLEM_RING` set them, as `<count>` or `<count>,<bytes>`;
8
+ // `0` keeps nothing.
9
+ //
10
+ // The feed is a `LocalFeed` on a loopback socket whose port the registry entry
11
+ // names as `logPort`. On connect it sends the rings, filtered, then each record
12
+ // as it is logged. A line is the record's JSON line, the one stdout prints with
13
+ // `REVENTLESS_LOG_FORMAT=json`. `REVENTLESS_LOG_FEED=off|<port>` controls it the
14
+ // way `REVENTLESS_EVENT_TAP` controls the tap.
15
+
16
+ module Logger = ReventlessCore.Logger
17
+
18
+ // ─── Rings ───────────────────────────────────────────────────────────────────
19
+
20
+ type entry = {
21
+ // Arrival order across both rings, so a record kept by both is sent once.
22
+ seq: int,
23
+ record: Logger.record,
24
+ item: LocalFeed.item,
25
+ }
26
+
27
+ type ring = {
28
+ mutable maxCount: int,
29
+ mutable maxBytes: int,
30
+ mutable entries: array<entry>,
31
+ mutable head: int,
32
+ mutable bytes: int,
33
+ }
34
+
35
+ let makeRing = (~maxCount: int, ~maxBytes: int): ring => {
36
+ maxCount,
37
+ maxBytes,
38
+ entries: [],
39
+ head: 0,
40
+ bytes: 0,
41
+ }
42
+
43
+ let size = (r: ring) => r.entries->Array.length - r.head
44
+
45
+ let entryBytes = (e: entry) => e.item.line->String.length
46
+
47
+ let push = (r: ring, e: entry) =>
48
+ if r.maxCount > 0 && r.maxBytes > 0 {
49
+ r.entries->Array.push(e)
50
+ r.bytes = r.bytes + entryBytes(e)
51
+ while size(r) > 0 && (size(r) > r.maxCount || r.bytes > r.maxBytes) {
52
+ r.bytes = r.bytes - entryBytes(r.entries->Array.getUnsafe(r.head))
53
+ r.head = r.head + 1
54
+ }
55
+
56
+ // Compact once the dropped prefix is most of the array.
57
+ if r.head > 1024 && r.head * 2 > r.entries->Array.length {
58
+ r.entries = r.entries->Array.slice(~start=r.head, ~end=r.entries->Array.length)
59
+ r.head = 0
60
+ }
61
+ }
62
+
63
+ let toArray = (r: ring): array<entry> =>
64
+ r.entries->Array.slice(~start=r.head, ~end=r.entries->Array.length)
65
+
66
+ let ringBytes = (r: ring) => r.bytes
67
+
68
+ let clear = (r: ring) => {
69
+ r.entries = []
70
+ r.head = 0
71
+ r.bytes = 0
72
+ }
73
+
74
+ let ringEnvVar = "REVENTLESS_LOG_RING"
75
+ let problemRingEnvVar = "REVENTLESS_LOG_PROBLEM_RING"
76
+
77
+ /** `<count>` or `<count>,<bytes>`; anything else keeps the defaults. */
78
+ let boundsOf = (value: option<string>, ~count: int, ~bytes: int): (int, int) =>
79
+ switch value->Option.map(v => v->String.trim->String.split(",")) {
80
+ | Some([c]) => (c->String.trim->Int.fromString->Option.getOr(count), bytes)
81
+ | Some([c, b]) => (
82
+ c->String.trim->Int.fromString->Option.getOr(count),
83
+ b->String.trim->Int.fromString->Option.getOr(bytes),
84
+ )
85
+ | _ => (count, bytes)
86
+ }
87
+
88
+ let recent = makeRing(~maxCount=5_000, ~maxBytes=8 * 1024 * 1024)
89
+ let problems = makeRing(~maxCount=1_000, ~maxBytes=2 * 1024 * 1024)
90
+
91
+ let isProblem = (level: Logger.level) =>
92
+ switch level {
93
+ | Warn | Error => true
94
+ | Debug | Info => false
95
+ }
96
+
97
+ /** What both rings hold, oldest first, each record once. */
98
+ let snapshot = (): array<entry> => {
99
+ let seen = Set.make()
100
+ recent
101
+ ->toArray
102
+ ->Array.concat(problems->toArray)
103
+ ->Array.filter(e =>
104
+ if seen->Set.has(e.seq) {
105
+ false
106
+ } else {
107
+ seen->Set.add(e.seq)
108
+ true
109
+ }
110
+ )
111
+ ->Array.toSorted((a, b) => (a.seq - b.seq)->Int.toFloat)
112
+ }
113
+
114
+ // ─── Records as feed lines ───────────────────────────────────────────────────
115
+
116
+ let facetsOf = (r: Logger.record): LocalFeed.facets => {
117
+ plugin: ?r.plugin,
118
+ level: r.level,
119
+ comp: ?r.comp,
120
+ correlationId: ?(r.annotations->Dict.get("correlationId")),
121
+ }
122
+
123
+ let itemOf = (r: Logger.record): LocalFeed.item => {
124
+ line: r->Logger.recordToJson->JSON.stringify,
125
+ facets: facetsOf(r),
126
+ }
127
+
128
+ /** The filter fields a log line can be narrowed by. Event types are about events
129
+ and are ignored here. */
130
+ let honours: array<LocalFeed.filterField> = [Plugins, Level, Components, CorrelationId]
131
+
132
+ /** The recent records a filter selects, oldest first, at most `limit` (the most
133
+ recent ones). */
134
+ let recentRecords = (~filter: LocalFeed.filter, ~limit: option<int>=?): array<Logger.record> => {
135
+ let selected =
136
+ snapshot()
137
+ ->Array.filter(e => LocalFeed.matches(~honours, filter, e.item.facets))
138
+ ->Array.map(e => e.record)
139
+ switch limit {
140
+ | Some(n) if n >= 0 && selected->Array.length > n =>
141
+ selected->Array.slice(~start=selected->Array.length - n, ~end=selected->Array.length)
142
+ | _ => selected
143
+ }
144
+ }
145
+
146
+ // ─── The feed ────────────────────────────────────────────────────────────────
147
+
148
+ let envVar = "REVENTLESS_LOG_FEED"
149
+
150
+ let feed = LocalFeed.make(
151
+ ~name="LogFeed",
152
+ ~honours,
153
+ ~backlog=() => snapshot()->Array.map(e => e.item),
154
+ ~maxBytes=16 * 1024 * 1024,
155
+ (),
156
+ )
157
+
158
+ let port = () => feed->LocalFeed.port
159
+ let hasReaders = () => feed->LocalFeed.hasReaders
160
+
161
+ let seq = ref(0)
162
+ let removeSink: ref<option<unit => unit>> = ref(None)
163
+
164
+ let onRecord = (record: Logger.record) => {
165
+ seq := seq.contents + 1
166
+ let entry = {seq: seq.contents, record, item: itemOf(record)}
167
+ push(recent, entry)
168
+ if isProblem(record.level) {
169
+ push(problems, entry)
170
+ }
171
+ if hasReaders() {
172
+ feed->LocalFeed.publish(entry.item)
173
+ }
174
+ }
175
+
176
+ /** Keeps a warning or an error that was never logged — the platform's own note of a
177
+ rejected command, say — in the problem ring and sends it to the feed's
178
+ listeners, without printing it. A record below warn is ignored. */
179
+ let keepProblem = (record: Logger.record) =>
180
+ if isProblem(record.level) {
181
+ seq := seq.contents + 1
182
+ let entry = {seq: seq.contents, record, item: itemOf(record)}
183
+ push(problems, entry)
184
+ if hasReaders() {
185
+ feed->LocalFeed.publish(entry.item)
186
+ }
187
+ }
188
+
189
+ /** Starts keeping records, once per process: sizes the rings from the
190
+ environment and registers the sink. */
191
+ let install = (~env=NodeProcess.env) => {
192
+ let (c, b) = boundsOf(env->Dict.get(ringEnvVar), ~count=5_000, ~bytes=8 * 1024 * 1024)
193
+ recent.maxCount = c
194
+ recent.maxBytes = b
195
+ let (pc, pb) = boundsOf(env->Dict.get(problemRingEnvVar), ~count=1_000, ~bytes=2 * 1024 * 1024)
196
+ problems.maxCount = pc
197
+ problems.maxBytes = pb
198
+ switch removeSink.contents {
199
+ | Some(_) => ()
200
+ | None => removeSink := Some(Logger.addSink(onRecord))
201
+ }
202
+ }
203
+
204
+ /** Binds the feed's socket and reports the port it got, once it is listening. */
205
+ let start = (~env=NodeProcess.env, ~onBound: int => unit=_ => (), ()): unit =>
206
+ feed->LocalFeed.start(~setting=LocalFeed.settingOf(env->Dict.get(envVar)), ~onBound, ())
207
+
208
+ /** Test seam: removes the sink and empties the rings. */
209
+ let resetForTests = () => {
210
+ removeSink.contents->Option.forEach(remove => remove())
211
+ removeSink := None
212
+ clear(recent)
213
+ clear(problems)
214
+ feed->LocalFeed.resetForTests
215
+ }
216
+
217
+ let stopForTests = () => feed->LocalFeed.stopForTests