@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,456 @@
1
+ // What the platform API and the platform MCP server offer a tool that watches
2
+ // this platform: every event of every log newest first (`Platform_EventLog`),
3
+ // the log level while running (`Platform_LogLevels`, `Platform_SetLogLevel`),
4
+ // the log feed and the event tap as subscriptions (`Platform_LogAppended`,
5
+ // `Platform_EventAppended`), and the MCP resources `platform/logs/recent` and
6
+ // `platform/events`.
7
+ //
8
+ // Every field here is admin-checked, also when the platform serves one API for
9
+ // both, where nothing else would check it.
10
+
11
+ module Logger = ReventlessCore.Logger
12
+ module GQL = ReventlessGraphqlServer.GraphQL_ServerInstance
13
+
14
+ // ─── SDL ─────────────────────────────────────────────────────────────────────
15
+
16
+ let eventLogTypes = [
17
+ "scalar AWSJSON",
18
+ `input Platform_EventLogFilter {
19
+ plugins: [String!]
20
+ logs: [String!]
21
+ eventTypes: [String!]
22
+ entityId: String
23
+ tagKey: String
24
+ tagValue: String
25
+ msgId: String
26
+ correlationId: String
27
+ causationId: String
28
+ user: String
29
+ timeFrom: String
30
+ timeTo: String
31
+ }`,
32
+ `type Platform_EventLogTag {
33
+ key: String!
34
+ value: String!
35
+ }`,
36
+ `type Platform_EventLogRecord {
37
+ plugin: String
38
+ log: String!
39
+ position: String!
40
+ eventType: String!
41
+ entityId: String
42
+ tags: [Platform_EventLogTag!]!
43
+ payload: AWSJSON!
44
+ meta: AWSJSON!
45
+ recordedAt: String!
46
+ }`,
47
+ `type Platform_EventLogEdge {
48
+ node: Platform_EventLogRecord!
49
+ cursor: String!
50
+ }`,
51
+ `type Platform_EventLogPageInfo {
52
+ hasNextPage: Boolean!
53
+ endCursor: String
54
+ }`,
55
+ `type Platform_EventLogConnection {
56
+ edges: [Platform_EventLogEdge!]!
57
+ pageInfo: Platform_EventLogPageInfo!
58
+ }`,
59
+ ]
60
+
61
+ let logLevelTypes = [
62
+ `enum Platform_LogLevel {
63
+ DEBUG
64
+ INFO
65
+ WARN
66
+ ERROR
67
+ }`,
68
+ `type Platform_LogLevelSetting {
69
+ plugin: String
70
+ comp: String
71
+ level: Platform_LogLevel!
72
+ }`,
73
+ `type Platform_LogLevels {
74
+ base: Platform_LogLevel
75
+ settings: [Platform_LogLevelSetting!]!
76
+ }`,
77
+ ]
78
+
79
+ let feedTypes = [
80
+ `input Platform_FeedFilter {
81
+ plugins: [String!]
82
+ level: Platform_LogLevel
83
+ components: [String!]
84
+ eventTypes: [String!]
85
+ correlationId: String
86
+ }`,
87
+ ]
88
+
89
+ let eventLogField = "Platform_EventLog"
90
+ let logLevelsField = "Platform_LogLevels"
91
+ let setLogLevelField = "Platform_SetLogLevel"
92
+
93
+ let queryFields = [
94
+ ` ${eventLogField}(filter: Platform_EventLogFilter, first: Int, after: String): Platform_EventLogConnection!`,
95
+ ` ${logLevelsField}: Platform_LogLevels!`,
96
+ ]
97
+
98
+ let logAppendedField = "Platform_LogAppended"
99
+ let eventAppendedField = "Platform_EventAppended"
100
+
101
+ // Each value is one line of the feed as JSON: a log record (the log feed's line),
102
+ // an event tap line without its sentinel prefix, or `{"kind":"skipped","count":N}`.
103
+ let subscriptionFields = [
104
+ ` ${logAppendedField}(filter: Platform_FeedFilter): AWSJSON!`,
105
+ ` ${eventAppendedField}(filter: Platform_FeedFilter): AWSJSON!`,
106
+ ]
107
+
108
+ let mutationFields = [
109
+ ` ${setLogLevelField}(plugin: String, comp: String, level: Platform_LogLevel): Platform_LogLevels!`,
110
+ ]
111
+
112
+ // ─── Resolvers ───────────────────────────────────────────────────────────────
113
+
114
+ let arg = (args: JSON.t, key: string): option<JSON.t> =>
115
+ args->JSON.Decode.object->Option.flatMap(d => d->Dict.get(key))
116
+
117
+ let pageJson = (page: LocalEventLogRead.page): JSON.t =>
118
+ Dict.fromArray([
119
+ (
120
+ "edges",
121
+ page.edges
122
+ ->Array.map(((record, cursor)) =>
123
+ Dict.fromArray([
124
+ ("node", LocalEventLogRead.recordToJson(record)),
125
+ ("cursor", JSON.Encode.string(cursor)),
126
+ ])->JSON.Encode.object
127
+ )
128
+ ->JSON.Encode.array,
129
+ ),
130
+ (
131
+ "pageInfo",
132
+ Dict.fromArray([
133
+ ("hasNextPage", JSON.Encode.bool(page.hasNextPage)),
134
+ ("endCursor", page.endCursor->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
135
+ ])->JSON.Encode.object,
136
+ ),
137
+ ])->JSON.Encode.object
138
+
139
+ let eventLog: GQL.resolverFn = async (_root, args, _ctx) => {
140
+ let page = await LocalEventLogRead.read(
141
+ ~filter=LocalEventLogRead.filterOfJson(arg(args, "filter")),
142
+ ~first=?arg(args, "first")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt),
143
+ ~after=?arg(args, "after")->Option.flatMap(JSON.Decode.string),
144
+ )
145
+ pageJson(page)
146
+ }
147
+
148
+ let levelOfWire = (s: string): option<Logger.level> =>
149
+ switch s {
150
+ | "DEBUG" => Some(Debug)
151
+ | "INFO" => Some(Info)
152
+ | "WARN" => Some(Warn)
153
+ | "ERROR" => Some(Error)
154
+ | _ => None
155
+ }
156
+
157
+ let levelsJson = (): JSON.t =>
158
+ Dict.fromArray([
159
+ (
160
+ "base",
161
+ Logger.effectiveMin()->Option.mapOr(JSON.Encode.null, l =>
162
+ l->Logger.levelLabel->JSON.Encode.string
163
+ ),
164
+ ),
165
+ (
166
+ "settings",
167
+ Logger.levelSettings()
168
+ ->Array.map(s =>
169
+ [("level", s.level->Logger.levelLabel->JSON.Encode.string)]
170
+ ->Array.concat(s.plugin->Option.mapOr([], p => [("plugin", JSON.Encode.string(p))]))
171
+ ->Array.concat(s.comp->Option.mapOr([], c => [("comp", JSON.Encode.string(c))]))
172
+ ->Dict.fromArray
173
+ ->JSON.Encode.object
174
+ )
175
+ ->JSON.Encode.array,
176
+ ),
177
+ ])->JSON.Encode.object
178
+
179
+ let logLevels: GQL.resolverFn = async (_root, _args, _ctx) => levelsJson()
180
+
181
+ let log = Logger.fromEnv()
182
+
183
+ let setLogLevel: GQL.resolverFn = async (_root, args, _ctx) => {
184
+ let plugin = arg(args, "plugin")->Option.flatMap(JSON.Decode.string)
185
+ let comp = arg(args, "comp")->Option.flatMap(JSON.Decode.string)
186
+ let level = arg(args, "level")->Option.flatMap(JSON.Decode.string)->Option.flatMap(levelOfWire)
187
+ Logger.setLevel(~plugin?, ~comp?, level)
188
+ let scope = switch (plugin, comp) {
189
+ | (Some(p), Some(c)) => `${c} of ${p}`
190
+ | (None, Some(c)) => c
191
+ | (Some(p), None) => p
192
+ | (None, None) => "every line"
193
+ }
194
+ log.info(
195
+ ~comp="Platform",
196
+ switch level {
197
+ | Some(l) => `log level for ${scope} set to ${l->Logger.levelLabel}`
198
+ | None => `log level for ${scope} cleared`
199
+ },
200
+ )
201
+ levelsJson()
202
+ }
203
+
204
+ // ─── The feeds as subscriptions ──────────────────────────────────────────────
205
+ // A subscriber is one more listener on the feed, with the same filter, bound and
206
+ // `skipped` line; its sink hands lines to the subscription as the client takes
207
+ // them, and asks the feed to wait while the client is behind.
208
+
209
+ let feedFilterOf = (json: option<JSON.t>): LocalFeed.filter => {
210
+ let d = json->Option.flatMap(JSON.Decode.object)->Option.getOr(Dict.make())
211
+ let a = k =>
212
+ d
213
+ ->Dict.get(k)
214
+ ->Option.flatMap(JSON.Decode.array)
215
+ ->Option.map(items => items->Array.filterMap(JSON.Decode.string))
216
+ {
217
+ plugins: ?a("plugins"),
218
+ level: ?(d->Dict.get("level")->Option.flatMap(JSON.Decode.string)->Option.flatMap(levelOfWire)),
219
+ components: ?a("components"),
220
+ eventTypes: ?a("eventTypes"),
221
+ correlationId: ?(d->Dict.get("correlationId")->Option.flatMap(JSON.Decode.string)),
222
+ }
223
+ }
224
+
225
+ type asyncIterable
226
+ type iteratorResult = {done: bool, value?: JSON.t}
227
+
228
+ let makeAsyncIterable: (
229
+ unit => promise<iteratorResult>,
230
+ unit => promise<iteratorResult>,
231
+ ) => asyncIterable = %raw(`function (next, ret) {
232
+ return { next, return: ret, [Symbol.asyncIterator]() { return this } }
233
+ }`)
234
+
235
+ // How many lines a subscription holds for its client before the feed is asked to
236
+ // wait; beyond that the feed's own bounded queue takes over.
237
+ let subscriptionWindow = 256
238
+
239
+ let tapSentinel = "@@RVLESS_EVT@@ "
240
+
241
+ /** A line as the subscription delivers it: JSON, without the tap's prefix. */
242
+ let lineToJson = (line: string): JSON.t => {
243
+ let body =
244
+ line->String.startsWith(tapSentinel)
245
+ ? line->String.slice(~start=tapSentinel->String.length, ~end=line->String.length)
246
+ : line
247
+ try body->JSON.parseOrThrow catch {
248
+ | _ => JSON.Encode.string(line)
249
+ }
250
+ }
251
+
252
+ let feedSubscription = (feed: LocalFeed.t, ~filter: LocalFeed.filter): asyncIterable => {
253
+ let pending: array<JSON.t> = []
254
+ let waiting: ref<option<iteratorResult => unit>> = ref(None)
255
+ let drainers: ref<array<unit => unit>> = ref([])
256
+ let closed = ref(false)
257
+ let sink: LocalFeed.sink = {
258
+ write: line => {
259
+ let value = lineToJson(line)
260
+ switch waiting.contents {
261
+ | Some(resolve) =>
262
+ waiting := None
263
+ resolve({done: false, value})
264
+ | None => pending->Array.push(value)
265
+ }
266
+ pending->Array.length < subscriptionWindow
267
+ },
268
+ onceDrain: f => drainers := drainers.contents->Array.concat([f]),
269
+ buffered: () => pending->Array.reduce(0, (sum, v) => sum + v->JSON.stringify->String.length),
270
+ close: () => closed := true,
271
+ }
272
+ let unsubscribe = feed->LocalFeed.subscribe(~filter, sink)
273
+ let next = () =>
274
+ switch pending->Array.shift {
275
+ | Some(value) =>
276
+ if pending->Array.length < subscriptionWindow / 2 {
277
+ let toCall = drainers.contents
278
+ drainers := []
279
+ toCall->Array.forEach(f => f())
280
+ }
281
+ Promise.resolve({done: false, value})
282
+ | None if closed.contents => Promise.resolve({done: true})
283
+ | None => Promise.make((resolve, _) => waiting := Some(resolve))
284
+ }
285
+ let return = () => {
286
+ closed := true
287
+ unsubscribe()
288
+ waiting.contents->Option.forEach(resolve => resolve({done: true}))
289
+ waiting := None
290
+ Promise.resolve({done: true})
291
+ }
292
+ makeAsyncIterable(next, return)
293
+ }
294
+
295
+ type subscriptionResolver = {
296
+ subscribe: GQL.resolverFn,
297
+ resolve: JSON.t => JSON.t,
298
+ }
299
+ external toResolverFn: subscriptionResolver => GQL.resolverFn = "%identity"
300
+ external fromAsyncIterable: asyncIterable => JSON.t = "%identity"
301
+
302
+ let feedField = (feed: LocalFeed.t): GQL.resolverFn =>
303
+ toResolverFn({
304
+ subscribe: Auth_GraphqlContext.requireGroup(~group=Reventless.Role.adminGroup(), async (
305
+ _root,
306
+ args,
307
+ _ctx,
308
+ ) => feedSubscription(feed, ~filter=feedFilterOf(arg(args, "filter")))->fromAsyncIterable),
309
+ resolve: payload => payload,
310
+ })
311
+
312
+ let adminOnly = (resolvers: array<(string, GQL.resolverFn)>): dict<GQL.resolverFn> =>
313
+ resolvers
314
+ ->Array.map(((name, resolver)) => (
315
+ name,
316
+ Auth_GraphqlContext.requireGroup(~group=Reventless.Role.adminGroup(), resolver),
317
+ ))
318
+ ->Dict.fromArray
319
+
320
+ /** Registers the fields on the platform API. */
321
+ let registerGraphQL = (server: GQL.t) => {
322
+ server.registerTypes(
323
+ ~sdlTypes=eventLogTypes->Array.concat(logLevelTypes)->Array.concat(feedTypes),
324
+ )
325
+ server.registerQueries(
326
+ ~sdlFields=queryFields,
327
+ ~resolvers=adminOnly([(eventLogField, eventLog), (logLevelsField, logLevels)]),
328
+ )
329
+ server.registerMutations(
330
+ ~sdlFields=mutationFields,
331
+ ~resolvers=adminOnly([(setLogLevelField, setLogLevel)]),
332
+ )
333
+ server.registerSubscriptions(
334
+ ~sdlFields=subscriptionFields,
335
+ ~resolvers=Dict.fromArray([
336
+ (logAppendedField, feedField(LocalLogFeed.feed)),
337
+ (eventAppendedField, feedField(LocalEventTap.feed)),
338
+ ]),
339
+ )
340
+ }
341
+
342
+ // ─── MCP resources ───────────────────────────────────────────────────────────
343
+
344
+ let recentLogsUri = "platform/logs/recent"
345
+ let eventsUri = "platform/events"
346
+
347
+ @val external decodeURIComponent: string => string = "decodeURIComponent"
348
+
349
+ /** The query string of a resource URI, decoded. */
350
+ let queryOf = (uri: string): dict<string> => {
351
+ let params = Dict.make()
352
+ switch uri->String.split("?")->Array.get(1) {
353
+ | None => ()
354
+ | Some(qs) =>
355
+ qs
356
+ ->String.split("&")
357
+ ->Array.forEach(pair => {
358
+ let decode = s =>
359
+ try decodeURIComponent(s->String.replaceAll("+", " ")) catch {
360
+ | _ => s
361
+ }
362
+ switch pair->String.indexOf("=") {
363
+ | -1 => ()
364
+ | i =>
365
+ params->Dict.set(
366
+ decode(pair->String.slice(~start=0, ~end=i)),
367
+ decode(pair->String.slice(~start=i + 1, ~end=pair->String.length)),
368
+ )
369
+ }
370
+ })
371
+ }
372
+ params
373
+ }
374
+
375
+ let listOf = (q: dict<string>, key: string): option<array<string>> =>
376
+ q
377
+ ->Dict.get(key)
378
+ ->Option.map(v => v->String.split(",")->Array.map(String.trim)->Array.filter(s => s != ""))
379
+
380
+ let intOf = (q: dict<string>, key: string): option<int> =>
381
+ q->Dict.get(key)->Option.flatMap(v => Int.fromString(v))
382
+
383
+ let resourceResult = (~uri: string, json: JSON.t): McpSdk.readResourceResult => {
384
+ McpSdk.contents: [{McpSdk.uri, text: json->JSON.stringify}],
385
+ }
386
+
387
+ /** `platform/logs/recent?plugins=a,b&level=warn&components=c&correlationId=x&limit=n`:
388
+ the recent records the filter selects, oldest first, at most `limit`
389
+ (default 200) of the most recent. */
390
+ let recentLogs: MCP_ServerInstance.resourceHandler = async uri => {
391
+ let q = queryOf(uri)
392
+ let filter: LocalFeed.filter = {
393
+ plugins: ?listOf(q, "plugins"),
394
+ level: ?(q->Dict.get("level")->Option.flatMap(LocalFeed.levelOfString)),
395
+ components: ?listOf(q, "components"),
396
+ correlationId: ?(q->Dict.get("correlationId")),
397
+ }
398
+ let records = LocalLogFeed.recentRecords(~filter, ~limit=intOf(q, "limit")->Option.getOr(200))
399
+ resourceResult(
400
+ ~uri,
401
+ Dict.fromArray([
402
+ ("records", records->Array.map(Logger.recordToJson)->JSON.Encode.array),
403
+ ])->JSON.Encode.object,
404
+ )
405
+ }
406
+
407
+ /** `platform/events?…`: one page of the read across every log, newest first.
408
+ Takes the filter of `Platform_EventLog` as query parameters (lists comma
409
+ separated) and `first` and `after`. */
410
+ let events: MCP_ServerInstance.resourceHandler = async uri => {
411
+ let q = queryOf(uri)
412
+ let filter: LocalEventLogRead.filter = {
413
+ plugins: ?listOf(q, "plugins"),
414
+ logs: ?listOf(q, "logs"),
415
+ eventTypes: ?listOf(q, "eventTypes"),
416
+ entityId: ?(q->Dict.get("entityId")),
417
+ tagKey: ?(q->Dict.get("tagKey")),
418
+ tagValue: ?(q->Dict.get("tagValue")),
419
+ msgId: ?(q->Dict.get("msgId")),
420
+ correlationId: ?(q->Dict.get("correlationId")),
421
+ causationId: ?(q->Dict.get("causationId")),
422
+ user: ?(q->Dict.get("user")),
423
+ timeFrom: ?(q->Dict.get("timeFrom")),
424
+ timeTo: ?(q->Dict.get("timeTo")),
425
+ }
426
+ let page = await LocalEventLogRead.read(
427
+ ~filter,
428
+ ~first=?intOf(q, "first"),
429
+ ~after=?q->Dict.get("after"),
430
+ )
431
+ resourceResult(~uri, pageJson(page))
432
+ }
433
+
434
+ /** Registers the resources on the platform MCP server. */
435
+ let registerMcp = (server: MCP_ServerInstance.t) => {
436
+ server.registerResource(
437
+ ~name=recentLogsUri,
438
+ ~definition={
439
+ uriTemplate: recentLogsUri,
440
+ name: recentLogsUri,
441
+ description: "The platform's recent log records, oldest first. Query parameters: plugins, level, components, correlationId (narrow), limit (default 200).",
442
+ mimeType: "application/json",
443
+ },
444
+ ~handler=recentLogs,
445
+ )
446
+ server.registerResource(
447
+ ~name=eventsUri,
448
+ ~definition={
449
+ uriTemplate: eventsUri,
450
+ name: eventsUri,
451
+ 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).",
452
+ mimeType: "application/json",
453
+ },
454
+ ~handler=events,
455
+ )
456
+ }