@reventlessdev/reventless-local 3.0.0-alpha.317 → 3.0.0-alpha.319

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 (64) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +10 -10
  3. package/src/LocalPlatformRegistry.res +26 -1
  4. package/src/LocalPlatformRegistry.res.mjs +25 -5
  5. package/src/Platform.res +27 -2
  6. package/src/Platform.res.mjs +24 -4
  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 +149 -0
  34. package/src/adapter/LocalTapLines.res.mjs +232 -0
  35. package/src/adapter/PlatformGraphQL_Server.res +36 -3
  36. package/src/adapter/PlatformGraphQL_Server.res.mjs +15 -1
  37. package/src/adapter/ProjectionCheckpoint.res.mjs +3 -3
  38. package/src/components/AutomationSlice_Builder.res.mjs +1 -1
  39. package/src/components/LocalAutomationSchedules.res.mjs +1 -1
  40. package/src/components/LocalAutomationSweep.res.mjs +1 -1
  41. package/src/reexport/CommandTopic.res.mjs +5 -2
  42. package/tests/LocalPlatformRegistryTest.res +17 -0
  43. package/tests/LocalPlatformRegistryTest.res.mjs +14 -2
  44. package/tests/LocalPlatformStartTest.res.mjs +1 -1
  45. package/tests/adapter/GraphQLWsFixtures.mjs +46 -0
  46. package/tests/adapter/LocalEventLogReadTest.res +539 -0
  47. package/tests/adapter/LocalEventLogReadTest.res.mjs +466 -0
  48. package/tests/adapter/LocalFeedTest.res +314 -0
  49. package/tests/adapter/LocalFeedTest.res.mjs +329 -0
  50. package/tests/adapter/LocalPlatformObserveTest.res +165 -0
  51. package/tests/adapter/LocalPlatformObserveTest.res.mjs +183 -0
  52. package/tests/adapter/LocalTapLinesTest.res +207 -0
  53. package/tests/adapter/LocalTapLinesTest.res.mjs +274 -0
  54. package/tests/adapter/PlatformSubscriptionsTest.res +123 -0
  55. package/tests/adapter/PlatformSubscriptionsTest.res.mjs +132 -0
  56. package/tests/components/automationslice/AutomationSliceCallbackTest.res +80 -0
  57. package/tests/components/automationslice/AutomationSliceCallbackTest.res.mjs +81 -0
  58. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +17 -0
  59. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +24 -0
  60. package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res +30 -0
  61. package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res.mjs +28 -0
  62. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +58 -0
  63. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs +71 -19
  64. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePiiTest.res.mjs +7 -7
@@ -0,0 +1,505 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
4
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
5
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
6
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
+ import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
8
+ import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
9
+ import * as LocalFeed$ReventlessLocal from "./LocalFeed.res.mjs";
10
+ import * as LocalLogFeed$ReventlessLocal from "./LocalLogFeed.res.mjs";
11
+ import * as LocalEventTap$ReventlessLocal from "./LocalEventTap.res.mjs";
12
+ import * as LocalEventLogRead$ReventlessLocal from "./LocalEventLogRead.res.mjs";
13
+ import * as Auth_GraphqlContext$ReventlessLocal from "./Auth/Auth_GraphqlContext.res.mjs";
14
+
15
+ let eventLogTypes = [
16
+ "scalar AWSJSON",
17
+ `input Platform_EventLogFilter {
18
+ plugins: [String!]
19
+ logs: [String!]
20
+ eventTypes: [String!]
21
+ entityId: String
22
+ tagKey: String
23
+ tagValue: String
24
+ msgId: String
25
+ correlationId: String
26
+ causationId: String
27
+ user: String
28
+ timeFrom: String
29
+ timeTo: String
30
+ }`,
31
+ `type Platform_EventLogTag {
32
+ key: String!
33
+ value: String!
34
+ }`,
35
+ `type Platform_EventLogRecord {
36
+ plugin: String
37
+ log: String!
38
+ position: String!
39
+ eventType: String!
40
+ entityId: String
41
+ tags: [Platform_EventLogTag!]!
42
+ payload: AWSJSON!
43
+ meta: AWSJSON!
44
+ recordedAt: String!
45
+ }`,
46
+ `type Platform_EventLogEdge {
47
+ node: Platform_EventLogRecord!
48
+ cursor: String!
49
+ }`,
50
+ `type Platform_EventLogPageInfo {
51
+ hasNextPage: Boolean!
52
+ endCursor: String
53
+ }`,
54
+ `type Platform_EventLogConnection {
55
+ edges: [Platform_EventLogEdge!]!
56
+ pageInfo: Platform_EventLogPageInfo!
57
+ }`
58
+ ];
59
+
60
+ let logLevelTypes = [
61
+ `enum Platform_LogLevel {
62
+ DEBUG
63
+ INFO
64
+ WARN
65
+ ERROR
66
+ }`,
67
+ `type Platform_LogLevelSetting {
68
+ plugin: String
69
+ comp: String
70
+ level: Platform_LogLevel!
71
+ }`,
72
+ `type Platform_LogLevels {
73
+ base: Platform_LogLevel
74
+ settings: [Platform_LogLevelSetting!]!
75
+ }`
76
+ ];
77
+
78
+ let feedTypes = [`input Platform_FeedFilter {
79
+ plugins: [String!]
80
+ level: Platform_LogLevel
81
+ components: [String!]
82
+ eventTypes: [String!]
83
+ correlationId: String
84
+ }`];
85
+
86
+ let eventLogField = "Platform_EventLog";
87
+
88
+ let logLevelsField = "Platform_LogLevels";
89
+
90
+ let setLogLevelField = "Platform_SetLogLevel";
91
+
92
+ let queryFields = [
93
+ ` ` + eventLogField + `(filter: Platform_EventLogFilter, first: Int, after: String): Platform_EventLogConnection!`,
94
+ ` ` + logLevelsField + `: Platform_LogLevels!`
95
+ ];
96
+
97
+ let logAppendedField = "Platform_LogAppended";
98
+
99
+ let eventAppendedField = "Platform_EventAppended";
100
+
101
+ let subscriptionFields = [
102
+ ` ` + logAppendedField + `(filter: Platform_FeedFilter): AWSJSON!`,
103
+ ` ` + eventAppendedField + `(filter: Platform_FeedFilter): AWSJSON!`
104
+ ];
105
+
106
+ let mutationFields = [` ` + setLogLevelField + `(plugin: String, comp: String, level: Platform_LogLevel): Platform_LogLevels!`];
107
+
108
+ function arg(args, key) {
109
+ return Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(args), d => d[key]);
110
+ }
111
+
112
+ function pageJson(page) {
113
+ return Object.fromEntries([
114
+ [
115
+ "edges",
116
+ page.edges.map(param => Object.fromEntries([
117
+ [
118
+ "node",
119
+ LocalEventLogRead$ReventlessLocal.recordToJson(param[0])
120
+ ],
121
+ [
122
+ "cursor",
123
+ param[1]
124
+ ]
125
+ ]))
126
+ ],
127
+ [
128
+ "pageInfo",
129
+ Object.fromEntries([
130
+ [
131
+ "hasNextPage",
132
+ page.hasNextPage
133
+ ],
134
+ [
135
+ "endCursor",
136
+ Stdlib_Option.mapOr(page.endCursor, null, prim => prim)
137
+ ]
138
+ ])
139
+ ]
140
+ ]);
141
+ }
142
+
143
+ async function eventLog(_root, args, _ctx) {
144
+ return pageJson(await LocalEventLogRead$ReventlessLocal.read(LocalEventLogRead$ReventlessLocal.filterOfJson(arg(args, "filter")), Stdlib_Option.map(Stdlib_Option.flatMap(arg(args, "first"), Stdlib_JSON.Decode.float), prim => prim | 0), Stdlib_Option.flatMap(arg(args, "after"), Stdlib_JSON.Decode.string)));
145
+ }
146
+
147
+ function levelOfWire(s) {
148
+ switch (s) {
149
+ case "DEBUG" :
150
+ return "Debug";
151
+ case "ERROR" :
152
+ return "Error";
153
+ case "INFO" :
154
+ return "Info";
155
+ case "WARN" :
156
+ return "Warn";
157
+ default:
158
+ return;
159
+ }
160
+ }
161
+
162
+ function levelsJson() {
163
+ return Object.fromEntries([
164
+ [
165
+ "base",
166
+ Stdlib_Option.mapOr(Logger$ReventlessCore.effectiveMin(), null, Logger$ReventlessCore.levelLabel)
167
+ ],
168
+ [
169
+ "settings",
170
+ Logger$ReventlessCore.levelSettings().map(s => Object.fromEntries([[
171
+ "level",
172
+ Logger$ReventlessCore.levelLabel(s.level)
173
+ ]].concat(Stdlib_Option.mapOr(s.plugin, [], p => [[
174
+ "plugin",
175
+ p
176
+ ]])).concat(Stdlib_Option.mapOr(s.comp, [], c => [[
177
+ "comp",
178
+ c
179
+ ]]))))
180
+ ]
181
+ ]);
182
+ }
183
+
184
+ async function logLevels(_root, _args, _ctx) {
185
+ return levelsJson();
186
+ }
187
+
188
+ let log = Logger$ReventlessCore.fromEnv();
189
+
190
+ async function setLogLevel(_root, args, _ctx) {
191
+ let plugin = Stdlib_Option.flatMap(arg(args, "plugin"), Stdlib_JSON.Decode.string);
192
+ let comp = Stdlib_Option.flatMap(arg(args, "comp"), Stdlib_JSON.Decode.string);
193
+ let level = Stdlib_Option.flatMap(Stdlib_Option.flatMap(arg(args, "level"), Stdlib_JSON.Decode.string), levelOfWire);
194
+ Logger$ReventlessCore.setLevel(plugin, comp, level);
195
+ let scope = plugin !== undefined ? (
196
+ comp !== undefined ? comp + ` of ` + plugin : plugin
197
+ ) : (
198
+ comp !== undefined ? comp : "every line"
199
+ );
200
+ log.info("Platform", undefined, level !== undefined ? `log level for ` + scope + ` set to ` + Logger$ReventlessCore.levelLabel(level) : `log level for ` + scope + ` cleared`);
201
+ return levelsJson();
202
+ }
203
+
204
+ function feedFilterOf(json) {
205
+ let d = Stdlib_Option.getOr(Stdlib_Option.flatMap(json, Stdlib_JSON.Decode.object), {});
206
+ let a = k => Stdlib_Option.map(Stdlib_Option.flatMap(d[k], Stdlib_JSON.Decode.array), items => Stdlib_Array.filterMap(items, Stdlib_JSON.Decode.string));
207
+ return {
208
+ plugins: a("plugins"),
209
+ level: Stdlib_Option.flatMap(Stdlib_Option.flatMap(d["level"], Stdlib_JSON.Decode.string), levelOfWire),
210
+ components: a("components"),
211
+ eventTypes: a("eventTypes"),
212
+ correlationId: Stdlib_Option.flatMap(d["correlationId"], Stdlib_JSON.Decode.string)
213
+ };
214
+ }
215
+
216
+ let makeAsyncIterable = (function (next, ret) {
217
+ return { next, return: ret, [Symbol.asyncIterator]() { return this } }
218
+ });
219
+
220
+ let tapSentinel = "@@RVLESS_EVT@@ ";
221
+
222
+ function lineToJson(line) {
223
+ let body = line.startsWith(tapSentinel) ? line.slice(tapSentinel.length, line.length) : line;
224
+ try {
225
+ return JSON.parse(body);
226
+ } catch (exn) {
227
+ return line;
228
+ }
229
+ }
230
+
231
+ function feedSubscription(feed, filter) {
232
+ let pending = [];
233
+ let waiting = {
234
+ contents: undefined
235
+ };
236
+ let drainers = {
237
+ contents: []
238
+ };
239
+ let closed = {
240
+ contents: false
241
+ };
242
+ let sink_write = line => {
243
+ let value = lineToJson(line);
244
+ let resolve = waiting.contents;
245
+ if (resolve !== undefined) {
246
+ waiting.contents = undefined;
247
+ resolve({
248
+ done: false,
249
+ value: value
250
+ });
251
+ } else {
252
+ pending.push(value);
253
+ }
254
+ return pending.length < 256;
255
+ };
256
+ let sink_onceDrain = f => {
257
+ drainers.contents = drainers.contents.concat([f]);
258
+ };
259
+ let sink_buffered = () => Stdlib_Array.reduce(pending, 0, (sum, v) => sum + JSON.stringify(v).length | 0);
260
+ let sink_close = () => {
261
+ closed.contents = true;
262
+ };
263
+ let sink = {
264
+ write: sink_write,
265
+ onceDrain: sink_onceDrain,
266
+ buffered: sink_buffered,
267
+ close: sink_close
268
+ };
269
+ let unsubscribe = LocalFeed$ReventlessLocal.subscribe(feed, filter, sink);
270
+ let next = () => {
271
+ let value = pending.shift();
272
+ if (value === undefined) {
273
+ if (closed.contents) {
274
+ return Promise.resolve({
275
+ done: true
276
+ });
277
+ } else {
278
+ return new Promise((resolve, param) => {
279
+ waiting.contents = resolve;
280
+ });
281
+ }
282
+ }
283
+ if (pending.length < 128) {
284
+ let toCall = drainers.contents;
285
+ drainers.contents = [];
286
+ toCall.forEach(f => f());
287
+ }
288
+ return Promise.resolve({
289
+ done: false,
290
+ value: value
291
+ });
292
+ };
293
+ let $$return = () => {
294
+ closed.contents = true;
295
+ unsubscribe();
296
+ Stdlib_Option.forEach(waiting.contents, resolve => resolve({
297
+ done: true
298
+ }));
299
+ waiting.contents = undefined;
300
+ return Promise.resolve({
301
+ done: true
302
+ });
303
+ };
304
+ return makeAsyncIterable(next, $$return);
305
+ }
306
+
307
+ function feedField(feed) {
308
+ return {
309
+ subscribe: Auth_GraphqlContext$ReventlessLocal.requireGroup(Role$Reventless.adminGroup(), async (_root, args, _ctx) => feedSubscription(feed, feedFilterOf(arg(args, "filter")))),
310
+ resolve: payload => payload
311
+ };
312
+ }
313
+
314
+ function adminOnly(resolvers) {
315
+ return Object.fromEntries(resolvers.map(param => [
316
+ param[0],
317
+ Auth_GraphqlContext$ReventlessLocal.requireGroup(Role$Reventless.adminGroup(), param[1])
318
+ ]));
319
+ }
320
+
321
+ function registerGraphQL(server) {
322
+ server.registerTypes(eventLogTypes.concat(logLevelTypes).concat(feedTypes));
323
+ server.registerQueries(queryFields, adminOnly([
324
+ [
325
+ eventLogField,
326
+ eventLog
327
+ ],
328
+ [
329
+ logLevelsField,
330
+ logLevels
331
+ ]
332
+ ]));
333
+ server.registerMutations(mutationFields, adminOnly([[
334
+ setLogLevelField,
335
+ setLogLevel
336
+ ]]));
337
+ server.registerSubscriptions(subscriptionFields, Object.fromEntries([
338
+ [
339
+ logAppendedField,
340
+ feedField(LocalLogFeed$ReventlessLocal.feed)
341
+ ],
342
+ [
343
+ eventAppendedField,
344
+ feedField(LocalEventTap$ReventlessLocal.feed)
345
+ ]
346
+ ]));
347
+ }
348
+
349
+ let recentLogsUri = "platform/logs/recent";
350
+
351
+ let eventsUri = "platform/events";
352
+
353
+ function queryOf(uri) {
354
+ let params = {};
355
+ let qs = uri.split("?")[1];
356
+ if (qs !== undefined) {
357
+ qs.split("&").forEach(pair => {
358
+ let decode = s => {
359
+ try {
360
+ return decodeURIComponent(s.replaceAll("+", " "));
361
+ } catch (exn) {
362
+ return s;
363
+ }
364
+ };
365
+ let i = pair.indexOf("=");
366
+ if (i !== -1) {
367
+ params[decode(pair.slice(0, i))] = decode(pair.slice(i + 1 | 0, pair.length));
368
+ return;
369
+ }
370
+ });
371
+ }
372
+ return params;
373
+ }
374
+
375
+ function listOf(q, key) {
376
+ return Stdlib_Option.map(q[key], v => v.split(",").map(prim => prim.trim()).filter(s => s !== ""));
377
+ }
378
+
379
+ function intOf(q, key) {
380
+ return Stdlib_Option.flatMap(q[key], v => Stdlib_Int.fromString(v, undefined));
381
+ }
382
+
383
+ function resourceResult(uri, json) {
384
+ return {
385
+ contents: [{
386
+ uri: uri,
387
+ text: JSON.stringify(json)
388
+ }]
389
+ };
390
+ }
391
+
392
+ async function recentLogs(uri) {
393
+ let q = queryOf(uri);
394
+ let filter_plugins = listOf(q, "plugins");
395
+ let filter_level = Stdlib_Option.flatMap(q["level"], LocalFeed$ReventlessLocal.levelOfString);
396
+ let filter_components = listOf(q, "components");
397
+ let filter_correlationId = q["correlationId"];
398
+ let filter = {
399
+ plugins: filter_plugins,
400
+ level: filter_level,
401
+ components: filter_components,
402
+ correlationId: filter_correlationId
403
+ };
404
+ let records = LocalLogFeed$ReventlessLocal.recentRecords(filter, Stdlib_Option.getOr(intOf(q, "limit"), 200));
405
+ return resourceResult(uri, Object.fromEntries([[
406
+ "records",
407
+ records.map(Logger$ReventlessCore.recordToJson)
408
+ ]]));
409
+ }
410
+
411
+ async function events(uri) {
412
+ let q = queryOf(uri);
413
+ let filter_plugins = listOf(q, "plugins");
414
+ let filter_logs = listOf(q, "logs");
415
+ let filter_eventTypes = listOf(q, "eventTypes");
416
+ let filter_entityId = q["entityId"];
417
+ let filter_tagKey = q["tagKey"];
418
+ let filter_tagValue = q["tagValue"];
419
+ let filter_msgId = q["msgId"];
420
+ let filter_correlationId = q["correlationId"];
421
+ let filter_causationId = q["causationId"];
422
+ let filter_user = q["user"];
423
+ let filter_timeFrom = q["timeFrom"];
424
+ let filter_timeTo = q["timeTo"];
425
+ let filter = {
426
+ plugins: filter_plugins,
427
+ logs: filter_logs,
428
+ eventTypes: filter_eventTypes,
429
+ entityId: filter_entityId,
430
+ tagKey: filter_tagKey,
431
+ tagValue: filter_tagValue,
432
+ msgId: filter_msgId,
433
+ correlationId: filter_correlationId,
434
+ causationId: filter_causationId,
435
+ user: filter_user,
436
+ timeFrom: filter_timeFrom,
437
+ timeTo: filter_timeTo
438
+ };
439
+ let page = await LocalEventLogRead$ReventlessLocal.read(filter, intOf(q, "first"), q["after"]);
440
+ return resourceResult(uri, pageJson(page));
441
+ }
442
+
443
+ function registerMcp(server) {
444
+ server.registerResource(recentLogsUri, {
445
+ uriTemplate: recentLogsUri,
446
+ name: recentLogsUri,
447
+ description: "The platform's recent log records, oldest first. Query parameters: plugins, level, components, correlationId (narrow), limit (default 200).",
448
+ mimeType: "application/json"
449
+ }, recentLogs);
450
+ server.registerResource(eventsUri, {
451
+ uriTemplate: eventsUri,
452
+ name: eventsUri,
453
+ description: "Every event of every log, newest first, one page at a time. Query parameters: plugins, logs, eventTypes, entityId, tagKey, tagValue, msgId, correlationId, causationId, user, timeFrom, timeTo, first, after (the page's endCursor).",
454
+ mimeType: "application/json"
455
+ }, events);
456
+ }
457
+
458
+ let Logger;
459
+
460
+ let GQL;
461
+
462
+ let subscriptionWindow = 256;
463
+
464
+ export {
465
+ Logger,
466
+ GQL,
467
+ eventLogTypes,
468
+ logLevelTypes,
469
+ feedTypes,
470
+ eventLogField,
471
+ logLevelsField,
472
+ setLogLevelField,
473
+ queryFields,
474
+ logAppendedField,
475
+ eventAppendedField,
476
+ subscriptionFields,
477
+ mutationFields,
478
+ arg,
479
+ pageJson,
480
+ eventLog,
481
+ levelOfWire,
482
+ levelsJson,
483
+ logLevels,
484
+ log,
485
+ setLogLevel,
486
+ feedFilterOf,
487
+ makeAsyncIterable,
488
+ subscriptionWindow,
489
+ tapSentinel,
490
+ lineToJson,
491
+ feedSubscription,
492
+ feedField,
493
+ adminOnly,
494
+ registerGraphQL,
495
+ recentLogsUri,
496
+ eventsUri,
497
+ queryOf,
498
+ listOf,
499
+ intOf,
500
+ resourceResult,
501
+ recentLogs,
502
+ events,
503
+ registerMcp,
504
+ }
505
+ /* log Not a pure module */
@@ -0,0 +1,149 @@
1
+ // The lines the event tap carries besides events: what became of each command,
2
+ // and each row a projection changed. Both go to the tap's listeners only; a
3
+ // rejected command is also kept in the log feed's problem ring.
4
+ //
5
+ // They are sent without the tap's sentinel prefix, as the `skipped` line is, and
6
+ // each carries a `kind`, so a reader that picks event lines by the prefix passes
7
+ // over them.
8
+
9
+ module Logger = ReventlessCore.Logger
10
+ module Outcome = ReventlessCore.CommandTopic_Helpers
11
+ module ViewChange = ReventlessCore.ViewChange
12
+
13
+ let str = JSON.Encode.string
14
+ let now = () => Date.make()->Date.toISOString
15
+
16
+ let lineOf = (fields: array<(string, JSON.t)>): string =>
17
+ fields->Dict.fromArray->JSON.Encode.object->JSON.stringify
18
+
19
+ let optional = (name: string, value: option<string>): array<(string, JSON.t)> =>
20
+ value->Option.mapOr([], v => [(name, str(v))])
21
+
22
+ // ─── Command outcomes ────────────────────────────────────────────────────────
23
+
24
+ /** The component a report names, as the log lines of its handler name it. */
25
+ let compOf = (r: Outcome.commandOutcomeReport) => `CommandTopic(${r.component})`
26
+
27
+ let pluginOf = (r: Outcome.commandOutcomeReport) => Logger.resolvePlugin(~comp=compOf(r), ())
28
+
29
+ /** The tap line for one outcome. */
30
+ let outcomeLine = (r: Outcome.commandOutcomeReport): string => {
31
+ let plugin = pluginOf(r)
32
+ let head =
33
+ [("kind", str("commandOutcome")), ("component", str(r.component))]
34
+ ->Array.concat(optional("plugin", plugin))
35
+ ->Array.concat([("msgId", str(r.reference))])
36
+ ->Array.concat(optional("correlationId", r.correlationId))
37
+ ->Array.concat(optional("causationId", r.causationId))
38
+ let body = switch r.outcome {
39
+ | OutcomeAccepted({entityId, eventCount}) =>
40
+ [("outcome", str("accepted"))]
41
+ ->Array.concat(optional("entityId", entityId))
42
+ ->Array.concat([("eventCount", JSON.Encode.int(eventCount))])
43
+ | OutcomeRejected({errorCode, cause}) => [
44
+ ("outcome", str("rejected")),
45
+ ("errorCode", str(errorCode)),
46
+ ("cause", str(Outcome.refusalCauseToGraphQL(cause))),
47
+ ]
48
+ }
49
+ lineOf(head->Array.concat(body)->Array.concat([("ts", str(now()))]))
50
+ }
51
+
52
+ /** A rejection as a log record: an error when the platform failed, a warning
53
+ when the model or the caller's access refused. The detail is redacted as a
54
+ logged detail is. */
55
+ let rejectionRecord = (r: Outcome.commandOutcomeReport): option<Logger.record> =>
56
+ switch r.outcome {
57
+ | OutcomeAccepted(_) => None
58
+ | OutcomeRejected({errorCode, errorDetail, cause}) =>
59
+ let level: Logger.level = switch cause {
60
+ | InfrastructureFailure => Error
61
+ | DomainRejection | AccessRefusal => Warn
62
+ }
63
+ let detailJson = switch errorDetail {
64
+ | "" => []
65
+ | text =>
66
+ let parsed = try JSON.parseOrThrow(text) catch {
67
+ | _ => str(text)
68
+ }
69
+ [("errorDetail", parsed->Reventless.LogRedaction.redactJson->Logger.truncateDetail)]
70
+ }
71
+ let annotations = Dict.fromArray([("msgId", r.reference)])
72
+ r.correlationId->Option.forEach(id => annotations->Dict.set("correlationId", id))
73
+ r.causationId->Option.forEach(id => annotations->Dict.set("causationId", id))
74
+ Some({
75
+ time: now(),
76
+ level,
77
+ message: "command rejected",
78
+ service: ?Logger.serviceName(),
79
+ plugin: ?pluginOf(r),
80
+ comp: compOf(r),
81
+ annotations,
82
+ detail: [
83
+ ("component", str(r.component)),
84
+ ("errorCode", str(errorCode)),
85
+ ("cause", str(Outcome.refusalCauseToGraphQL(cause))),
86
+ ]
87
+ ->Array.concat(detailJson)
88
+ ->Dict.fromArray
89
+ ->JSON.Encode.object,
90
+ })
91
+ }
92
+
93
+ let onOutcome = (r: Outcome.commandOutcomeReport) => {
94
+ rejectionRecord(r)->Option.forEach(LocalLogFeed.keepProblem)
95
+ if LocalEventTap.hasReaders() {
96
+ LocalEventTap.publish({
97
+ line: outcomeLine(r),
98
+ facets: {plugin: ?pluginOf(r), correlationId: ?r.correlationId},
99
+ })
100
+ }
101
+ }
102
+
103
+ // ─── View changes ────────────────────────────────────────────────────────────
104
+
105
+ /** The tap line for one row change: the summary the log line carries, with the
106
+ row before and after, redacted. */
107
+ let viewChangeLine = (c: ViewChange.t): string => {
108
+ let side = (name, value: option<JSON.t>) =>
109
+ value->Option.mapOr([], v => [
110
+ (name, v->Reventless.LogRedaction.redactJson->Logger.truncateDetail),
111
+ ])
112
+ lineOf(
113
+ [("kind", str("viewChange"))]
114
+ ->Array.concat(c->ViewChange.summaryFields)
115
+ ->Array.concat(optional("correlationId", c.correlationId))
116
+ ->Array.concat(side("before", c.before))
117
+ ->Array.concat(side("after", c.after))
118
+ ->Array.concat([("ts", str(now()))]),
119
+ )
120
+ }
121
+
122
+ let onViewChange = (c: ViewChange.t) =>
123
+ if LocalEventTap.hasReaders() {
124
+ LocalEventTap.publish({
125
+ line: viewChangeLine(c),
126
+ facets: {
127
+ // The plugin is looked up by the view's name, as a log line's is by the
128
+ // name inside its component.
129
+ plugin: ?Logger.resolvePlugin(~comp=`View(${c.view})`, ()),
130
+ correlationId: ?c.correlationId,
131
+ },
132
+ })
133
+ }
134
+
135
+ // ─── Registration ────────────────────────────────────────────────────────────
136
+
137
+ let removals: ref<array<unit => unit>> = ref([])
138
+
139
+ /** Starts sending both lines, once per process. */
140
+ let install = () =>
141
+ if removals.contents == [] {
142
+ removals := [Outcome.registerCommandOutcome(onOutcome), ViewChange.register(onViewChange)]
143
+ }
144
+
145
+ /** Test seam: stops sending them. */
146
+ let resetForTests = () => {
147
+ removals.contents->Array.forEach(remove => remove())
148
+ removals := []
149
+ }