@omnixal/openclaw-nats-plugin 0.2.4 → 0.2.5

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 (31) hide show
  1. package/PLUGIN.md +9 -6
  2. package/README.md +12 -1
  3. package/dashboard/src/lib/CronPanel.svelte +206 -27
  4. package/dashboard/src/lib/LogsPanel.svelte +211 -0
  5. package/dashboard/src/lib/RoutesPanel.svelte +157 -13
  6. package/dashboard/src/lib/api.ts +77 -0
  7. package/dashboard/src/lib/components/ui/modal/index.ts +1 -0
  8. package/dashboard/src/lib/components/ui/modal/modal.svelte +49 -0
  9. package/dashboard/src/lib/utils.ts +8 -0
  10. package/package.json +1 -1
  11. package/sidecar/bun.lock +2 -2
  12. package/sidecar/package.json +1 -1
  13. package/sidecar/src/app.module.ts +2 -0
  14. package/sidecar/src/consumer/consumer.controller.ts +20 -12
  15. package/sidecar/src/consumer/consumer.module.ts +2 -1
  16. package/sidecar/src/db/migrations/0005_strong_supernaut.sql +13 -0
  17. package/sidecar/src/db/migrations/meta/0005_snapshot.json +389 -0
  18. package/sidecar/src/db/migrations/meta/_journal.json +7 -0
  19. package/sidecar/src/db/schema.ts +17 -0
  20. package/sidecar/src/logs/log.controller.ts +50 -0
  21. package/sidecar/src/logs/log.module.ts +11 -0
  22. package/sidecar/src/logs/log.repository.ts +78 -0
  23. package/sidecar/src/logs/log.service.ts +116 -0
  24. package/sidecar/src/router/router.controller.ts +28 -6
  25. package/sidecar/src/router/router.repository.ts +8 -0
  26. package/sidecar/src/router/router.service.ts +4 -0
  27. package/sidecar/src/scheduler/scheduler.controller.ts +32 -3
  28. package/sidecar/src/scheduler/scheduler.module.ts +2 -1
  29. package/sidecar/src/scheduler/scheduler.repository.ts +8 -0
  30. package/sidecar/src/scheduler/scheduler.service.ts +91 -25
  31. package/sidecar/src/validation/schemas.ts +27 -0
@@ -3,8 +3,10 @@
3
3
  import * as Table from '$lib/components/ui/table';
4
4
  import { Badge } from '$lib/components/ui/badge';
5
5
  import { Button } from '$lib/components/ui/button';
6
- import { type EventRoute, createRoute, deleteRoute } from '$lib/api';
7
- import { relativeAge, formatDuration } from '$lib/utils';
6
+ import { Modal } from '$lib/components/ui/modal';
7
+ import LogsPanel from '$lib/LogsPanel.svelte';
8
+ import { type EventRoute, createRoute, deleteRoute, updateRoute } from '$lib/api';
9
+ import { relativeAge, formatDuration, isValidAgentSubject } from '$lib/utils';
8
10
 
9
11
  interface Props {
10
12
  routes: EventRoute[];
@@ -13,6 +15,7 @@
13
15
 
14
16
  let { routes, onRefresh }: Props = $props();
15
17
 
18
+ // Create form state
16
19
  let showForm: boolean = $state(false);
17
20
  let formPattern: string = $state('agent.events.');
18
21
  let formTarget: string = $state('main');
@@ -21,6 +24,15 @@
21
24
  let actionError: string | null = $state(null);
22
25
  let loading: boolean = $state(false);
23
26
 
27
+ // Modal state
28
+ let selectedRoute: EventRoute | null = $state(null);
29
+ let activeTab: 'details' | 'logs' = $state('details');
30
+ let editTarget: string = $state('');
31
+ let editPriority: number = $state(5);
32
+ let editEnabled: boolean = $state(true);
33
+ let editError: string | null = $state(null);
34
+ let showDeleteConfirm: boolean = $state(false);
35
+
24
36
  function priorityVariant(p: number): 'default' | 'secondary' | 'destructive' {
25
37
  if (p >= 8) return 'destructive';
26
38
  if (p >= 5) return 'default';
@@ -37,8 +49,8 @@
37
49
 
38
50
  async function handleCreate() {
39
51
  formError = null;
40
- if (!formPattern.startsWith('agent.events.')) {
41
- formError = 'Pattern must start with "agent.events."';
52
+ if (!isValidAgentSubject(formPattern)) {
53
+ formError = 'Pattern must start with "agent.events." followed by at least one token and must not end with "."';
42
54
  return;
43
55
  }
44
56
  try {
@@ -57,20 +69,156 @@
57
69
  }
58
70
  }
59
71
 
60
- async function handleDelete(id: string) {
72
+ function openRouteModal(route: EventRoute) {
73
+ selectedRoute = route;
74
+ activeTab = 'details';
75
+ editTarget = route.target;
76
+ editPriority = route.priority;
77
+ editEnabled = route.enabled;
78
+ editError = null;
79
+ showDeleteConfirm = false;
80
+ }
81
+
82
+ function closeModal() {
83
+ selectedRoute = null;
84
+ editError = null;
85
+ showDeleteConfirm = false;
86
+ }
87
+
88
+ async function handleSave() {
89
+ if (!selectedRoute) return;
90
+ try {
91
+ editError = null;
92
+ loading = true;
93
+ await updateRoute(selectedRoute.id, {
94
+ target: editTarget,
95
+ priority: editPriority,
96
+ enabled: editEnabled,
97
+ });
98
+ closeModal();
99
+ onRefresh();
100
+ } catch (e: any) {
101
+ editError = e.message;
102
+ } finally {
103
+ loading = false;
104
+ }
105
+ }
106
+
107
+ async function handleDelete() {
108
+ if (!selectedRoute) return;
61
109
  try {
62
- actionError = null;
110
+ editError = null;
63
111
  loading = true;
64
- await deleteRoute(id);
112
+ await deleteRoute(selectedRoute.id);
113
+ closeModal();
65
114
  onRefresh();
66
115
  } catch (e: any) {
67
- actionError = e.message;
116
+ editError = e.message;
68
117
  } finally {
69
118
  loading = false;
70
119
  }
71
120
  }
72
121
  </script>
73
122
 
123
+ {#if selectedRoute}
124
+ <Modal
125
+ open={true}
126
+ title="Route: {selectedRoute.pattern}"
127
+ onClose={closeModal}
128
+ >
129
+ {#snippet children()}
130
+ <!-- Tabs -->
131
+ <div class="flex gap-1 border-b mb-4">
132
+ <button
133
+ class="px-3 py-1.5 text-sm font-medium border-b-2 transition-colors {activeTab === 'details' ? 'border-primary text-foreground' : 'border-transparent text-muted-foreground hover:text-foreground'}"
134
+ onclick={() => (activeTab = 'details')}
135
+ >Details</button>
136
+ <button
137
+ class="px-3 py-1.5 text-sm font-medium border-b-2 transition-colors {activeTab === 'logs' ? 'border-primary text-foreground' : 'border-transparent text-muted-foreground hover:text-foreground'}"
138
+ onclick={() => (activeTab = 'logs')}
139
+ >Logs</button>
140
+ </div>
141
+
142
+ {#if activeTab === 'details'}
143
+ {#if editError}
144
+ <div class="rounded-md bg-destructive/10 p-2 text-xs text-destructive mb-3">{editError}</div>
145
+ {/if}
146
+
147
+ {#if showDeleteConfirm}
148
+ <div class="rounded-md border border-destructive/50 bg-destructive/5 p-4 space-y-3">
149
+ <p class="text-sm">Are you sure you want to delete route <span class="font-mono font-semibold">{selectedRoute.pattern}</span>?</p>
150
+ <p class="text-xs text-muted-foreground">This action cannot be undone.</p>
151
+ <div class="flex gap-2">
152
+ <Button variant="destructive" size="sm" onclick={handleDelete} disabled={loading}>
153
+ {loading ? 'Deleting...' : 'Confirm Delete'}
154
+ </Button>
155
+ <Button variant="outline" size="sm" onclick={() => (showDeleteConfirm = false)}>Cancel</Button>
156
+ </div>
157
+ </div>
158
+ {:else}
159
+ <div class="space-y-3">
160
+ <div class="space-y-1">
161
+ <label class="text-xs text-muted-foreground" for="edit-target">Target</label>
162
+ <input
163
+ id="edit-target"
164
+ type="text"
165
+ bind:value={editTarget}
166
+ class="w-full rounded-md border border-input bg-background px-3 py-1.5 text-sm"
167
+ />
168
+ </div>
169
+ <div class="grid grid-cols-2 gap-3">
170
+ <div class="space-y-1">
171
+ <label class="text-xs text-muted-foreground" for="edit-priority">Priority</label>
172
+ <input
173
+ id="edit-priority"
174
+ type="number"
175
+ min="1"
176
+ max="10"
177
+ bind:value={editPriority}
178
+ class="w-full rounded-md border border-input bg-background px-3 py-1.5 text-sm"
179
+ />
180
+ </div>
181
+ <div class="space-y-1">
182
+ <label for="edit-enabled" class="text-xs text-muted-foreground">Enabled</label>
183
+ <div class="flex items-center gap-2 pt-1">
184
+ <input id="edit-enabled" type="checkbox" bind:checked={editEnabled} />
185
+ <label for="edit-enabled" class="text-sm">{editEnabled ? 'Active' : 'Disabled'}</label>
186
+ </div>
187
+ </div>
188
+ </div>
189
+
190
+ <div class="text-xs text-muted-foreground pt-2 space-y-1">
191
+ <div>Deliveries: <span class="font-medium text-foreground">{selectedRoute.deliveryCount}</span></div>
192
+ <div>Last delivered: <span class="font-medium text-foreground">{selectedRoute.lastDeliveredAt ? relativeAge(new Date(selectedRoute.lastDeliveredAt).getTime()) : '\u2014'}</span></div>
193
+ {#if selectedRoute.lastEventSubject}
194
+ <div>Last subject: <span class="font-mono font-medium text-foreground">{selectedRoute.lastEventSubject}</span></div>
195
+ {/if}
196
+ </div>
197
+ </div>
198
+ {/if}
199
+ {:else}
200
+ <LogsPanel entityType="route" entityId={selectedRoute.id} />
201
+ {/if}
202
+ {/snippet}
203
+
204
+ {#snippet actions()}
205
+ {#if activeTab === 'details' && !showDeleteConfirm}
206
+ <div class="flex w-full justify-between">
207
+ <Button variant="ghost" size="sm" class="text-destructive" onclick={() => (showDeleteConfirm = true)}>
208
+ Delete
209
+ </Button>
210
+ <div class="flex gap-2">
211
+ <Button variant="outline" size="sm" onclick={closeModal}>Cancel</Button>
212
+ <Button size="sm" onclick={handleSave} disabled={loading}>
213
+ {loading ? 'Saving...' : 'Save'}
214
+ </Button>
215
+ </div>
216
+ </div>
217
+ {/if}
218
+ {/snippet}
219
+ </Modal>
220
+ {/if}
221
+
74
222
  <Card.Root>
75
223
  <Card.Header class="pb-2 flex flex-row items-center justify-between">
76
224
  <Card.Title class="text-sm font-medium">Routes</Card.Title>
@@ -143,12 +291,11 @@
143
291
  <Table.Head>Deliveries</Table.Head>
144
292
  <Table.Head>Last Delivered</Table.Head>
145
293
  <Table.Head>Lag</Table.Head>
146
- <Table.Head class="w-16"></Table.Head>
147
294
  </Table.Row>
148
295
  </Table.Header>
149
296
  <Table.Body>
150
297
  {#each routes as route}
151
- <Table.Row>
298
+ <Table.Row class="cursor-pointer hover:bg-muted/50" onclick={() => openRouteModal(route)}>
152
299
  <Table.Cell class="font-mono text-xs">{route.pattern}</Table.Cell>
153
300
  <Table.Cell>{route.target}</Table.Cell>
154
301
  <Table.Cell>
@@ -166,9 +313,6 @@
166
313
  <Table.Cell class="text-xs text-muted-foreground">
167
314
  {formatDuration(route.lagMs)}
168
315
  </Table.Cell>
169
- <Table.Cell>
170
- <Button variant="ghost" size="sm" onclick={() => handleDelete(route.id)}>Delete</Button>
171
- </Table.Cell>
172
316
  </Table.Row>
173
317
  {/each}
174
318
  </Table.Body>
@@ -76,6 +76,20 @@ export async function createRoute(body: {
76
76
  });
77
77
  }
78
78
 
79
+ export interface UpdateRouteBody {
80
+ target?: string;
81
+ priority?: number;
82
+ enabled?: boolean;
83
+ }
84
+
85
+ export async function updateRoute(id: string, body: UpdateRouteBody): Promise<EventRoute> {
86
+ return fetchJSON(`/routes/${encodeURIComponent(id)}`, {
87
+ method: 'PATCH',
88
+ headers: { 'Content-Type': 'application/json' },
89
+ body: JSON.stringify(body),
90
+ });
91
+ }
92
+
79
93
  export async function deleteRoute(id: string): Promise<void> {
80
94
  await fetchJSON(`/routes/${encodeURIComponent(id)}`, { method: 'DELETE' });
81
95
  }
@@ -114,6 +128,22 @@ export async function createCronJob(body: {
114
128
  });
115
129
  }
116
130
 
131
+ export interface UpdateCronBody {
132
+ cron?: string;
133
+ subject?: string;
134
+ payload?: unknown;
135
+ timezone?: string;
136
+ enabled?: boolean;
137
+ }
138
+
139
+ export async function updateCronJob(name: string, body: UpdateCronBody): Promise<CronJob> {
140
+ return fetchJSON(`/cron/${encodeURIComponent(name)}`, {
141
+ method: 'PATCH',
142
+ headers: { 'Content-Type': 'application/json' },
143
+ body: JSON.stringify(body),
144
+ });
145
+ }
146
+
117
147
  export async function deleteCronJob(name: string): Promise<void> {
118
148
  await fetchJSON(`/cron/${encodeURIComponent(name)}`, { method: 'DELETE' });
119
149
  }
@@ -139,3 +169,50 @@ export interface SubjectMetric {
139
169
  export async function getMetrics(): Promise<SubjectMetric[]> {
140
170
  return fetchJSON('/metrics');
141
171
  }
172
+
173
+ // ── Execution Logs ──────────────────────────────────────────────────
174
+
175
+ export interface ExecutionLog {
176
+ id: string;
177
+ entityType: string;
178
+ entityId: string;
179
+ action: string;
180
+ subject: string;
181
+ detail: string | null;
182
+ success: boolean;
183
+ createdAt: number;
184
+ }
185
+
186
+ export interface LogFilters {
187
+ success?: boolean;
188
+ action?: string;
189
+ subject?: string;
190
+ }
191
+
192
+ export interface LogsResult {
193
+ items: ExecutionLog[];
194
+ total: number;
195
+ }
196
+
197
+ export async function getLogs(
198
+ entityType: string,
199
+ entityId: string,
200
+ limit: number = 50,
201
+ offset: number = 0,
202
+ filters?: LogFilters,
203
+ ): Promise<LogsResult> {
204
+ const params = new URLSearchParams({
205
+ entityType,
206
+ entityId,
207
+ limit: String(limit),
208
+ offset: String(offset),
209
+ });
210
+ if (filters?.success !== undefined) params.set('success', String(filters.success));
211
+ if (filters?.action) params.set('action', filters.action);
212
+ if (filters?.subject) params.set('subject', filters.subject);
213
+ return fetchJSON(`/logs?${params}`);
214
+ }
215
+
216
+ export async function getRecentLogs(limit: number = 20): Promise<ExecutionLog[]> {
217
+ return fetchJSON(`/logs/recent?limit=${limit}`);
218
+ }
@@ -0,0 +1 @@
1
+ export { default as Modal } from './modal.svelte';
@@ -0,0 +1,49 @@
1
+ <script lang="ts">
2
+ import { Button } from '$lib/components/ui/button';
3
+ import type { Snippet } from 'svelte';
4
+
5
+ interface Props {
6
+ open: boolean;
7
+ title: string;
8
+ onClose: () => void;
9
+ children: Snippet;
10
+ actions?: Snippet;
11
+ }
12
+
13
+ let {
14
+ open,
15
+ title,
16
+ onClose,
17
+ children,
18
+ actions,
19
+ }: Props = $props();
20
+
21
+ function handleKeydown(e: KeyboardEvent) {
22
+ if (e.key === 'Escape') onClose();
23
+ }
24
+ </script>
25
+
26
+ {#if open}
27
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
28
+ <div
29
+ class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
30
+ onkeydown={handleKeydown}
31
+ >
32
+ <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
33
+ <div class="fixed inset-0" onclick={onClose}></div>
34
+ <div class="relative z-10 w-full max-w-lg rounded-lg border bg-background p-6 shadow-lg">
35
+ <div class="flex items-center justify-between mb-4">
36
+ <h3 class="text-lg font-semibold">{title}</h3>
37
+ <Button variant="ghost" size="icon-sm" onclick={onClose}>
38
+ <span class="text-lg leading-none">&times;</span>
39
+ </Button>
40
+ </div>
41
+ {@render children()}
42
+ {#if actions}
43
+ <div class="mt-4 flex justify-end gap-2">
44
+ {@render actions()}
45
+ </div>
46
+ {/if}
47
+ </div>
48
+ </div>
49
+ {/if}
@@ -27,3 +27,11 @@ export function formatDuration(ms: number | null): string {
27
27
  if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
28
28
  return `${Math.floor(seconds / 3600)}h`;
29
29
  }
30
+
31
+ export function isValidAgentSubject(subject: string): boolean {
32
+ if (!subject.startsWith('agent.events.')) return false;
33
+ const rest = subject.slice('agent.events.'.length);
34
+ if (rest.length === 0) return false;
35
+ if (rest.endsWith('.')) return false;
36
+ return true;
37
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnixal/openclaw-nats-plugin",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "NATS JetStream event-driven plugin for OpenClaw",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/sidecar/bun.lock CHANGED
@@ -5,7 +5,7 @@
5
5
  "": {
6
6
  "name": "@ai-entrepreneur/nats-sidecar",
7
7
  "dependencies": {
8
- "@onebun/core": "^0.2.14",
8
+ "@onebun/core": "^0.2.15",
9
9
  "@onebun/drizzle": "^0.2.4",
10
10
  "@onebun/envs": "^0.2.2",
11
11
  "@onebun/logger": "^0.2.1",
@@ -104,7 +104,7 @@
104
104
 
105
105
  "@nats-io/transport-node": ["@nats-io/transport-node@3.3.1", "", { "dependencies": { "@nats-io/nats-core": "3.3.1", "@nats-io/nkeys": "2.0.3", "@nats-io/nuid": "2.0.3" } }, "sha512-GBvY0VcvyQEILgy5bjpqU1GpDYmSF06bW59I7cewZuNGS9u3AoV/gf+a+3ep45T/Z+UC661atq/b7x+QV12w+Q=="],
106
106
 
107
- "@onebun/core": ["@onebun/core@0.2.14", "", { "dependencies": { "@onebun/envs": "^0.2.2", "@onebun/logger": "^0.2.1", "@onebun/metrics": "^0.2.2", "@onebun/requests": "^0.2.1", "@onebun/trace": "^0.2.1", "arktype": "^2.0.0", "effect": "^3.13.10" }, "peerDependencies": { "testcontainers": ">=10.0.0" }, "optionalPeers": ["testcontainers"] }, "sha512-uClu4Oez18y6BldubdE6R/I02Brrk5eXCoxxPatgRJ9qYRM+jKSTNmeZVoqqo7ai/rMYM9lJ1WuF9d7p6/RtDA=="],
107
+ "@onebun/core": ["@onebun/core@0.2.15", "", { "dependencies": { "@onebun/envs": "^0.2.2", "@onebun/logger": "^0.2.1", "@onebun/metrics": "^0.2.2", "@onebun/requests": "^0.2.1", "@onebun/trace": "^0.2.1", "arktype": "^2.0.0", "effect": "^3.13.10" }, "peerDependencies": { "testcontainers": ">=10.0.0" }, "optionalPeers": ["testcontainers"] }, "sha512-oofGNjLhnG2EVErl4erxrX6u8x/FANLM/NMg5OpMA6jvdWyXsWwt5Hk6Pe11WD8EA69zV8MLC/1tiYyG/bERlQ=="],
108
108
 
109
109
  "@onebun/drizzle": ["@onebun/drizzle@0.2.4", "", { "dependencies": { "@onebun/envs": "^0.2.1", "@onebun/logger": "^0.2.1", "arktype": "^2.0.0", "drizzle-arktype": "^0.1.3", "drizzle-kit": "^0.31.6", "drizzle-orm": "^0.44.7", "effect": "^3.13.10" }, "peerDependencies": { "@onebun/core": ">=0.2.0" }, "bin": { "onebun-drizzle": "bin/drizzle-kit.ts" } }, "sha512-LbkW2hU9pTKZU/rlrHNdwhI4jYoMl+v22c3G2zc0L0aW77nW7ZCfp5YqOJYufWJbfOTSWEnNOVZQXMueYhBxsA=="],
110
110
 
@@ -14,7 +14,7 @@
14
14
  "db:studio": "bunx onebun-drizzle studio",
15
15
  },
16
16
  "dependencies": {
17
- "@onebun/core": "^0.2.14",
17
+ "@onebun/core": "^0.2.15",
18
18
  "@onebun/drizzle": "^0.2.4",
19
19
  "@onebun/envs": "^0.2.2",
20
20
  "@onebun/logger": "^0.2.1",
@@ -11,6 +11,7 @@ import { HealthModule } from './health/health.module';
11
11
  import { RouterModule } from './router/router.module';
12
12
  import { SchedulerModule } from './scheduler/scheduler.module';
13
13
  import { MetricsModule } from './metrics/metrics.module';
14
+ import { LogModule } from './logs/log.module';
14
15
 
15
16
  const config = getConfig<AppConfig>(envSchema);
16
17
 
@@ -35,6 +36,7 @@ const config = getConfig<AppConfig>(envSchema);
35
36
  RouterModule,
36
37
  SchedulerModule,
37
38
  MetricsModule,
39
+ LogModule,
38
40
  ],
39
41
  })
40
42
  export class AppModule {}
@@ -4,6 +4,7 @@ import { GatewayClientService } from '../gateway/gateway-client.service';
4
4
  import { PendingService } from '../pending/pending.service';
5
5
  import { RouterService } from '../router/router.service';
6
6
  import { MetricsService } from '../metrics/metrics.service';
7
+ import { LogService } from '../logs/log.service';
7
8
  import type { NatsEventEnvelope } from '../publisher/envelope';
8
9
 
9
10
  @Controller('/consumer')
@@ -14,6 +15,7 @@ export class ConsumerController extends BaseController {
14
15
  private pendingService: PendingService,
15
16
  private routerService: RouterService,
16
17
  private metrics: MetricsService,
18
+ private logService: LogService,
17
19
  ) {
18
20
  super();
19
21
  }
@@ -49,18 +51,24 @@ export class ConsumerController extends BaseController {
49
51
  // Deliver to each matching target
50
52
  if (this.gatewayClient.isAlive()) {
51
53
  for (const route of routes) {
52
- await this.gatewayClient.inject({
53
- target: route.target,
54
- message: this.formatMessage(envelope),
55
- metadata: {
56
- source: 'nats',
57
- eventId: envelope.id,
58
- subject: envelope.subject,
59
- priority: (ctx.enrichments['priority'] as number) ?? envelope.meta?.priority ?? 5,
60
- },
61
- });
62
- await this.routerService.recordDelivery(route.id, envelope.subject);
63
- this.metrics.recordConsume(envelope.subject);
54
+ try {
55
+ await this.gatewayClient.inject({
56
+ target: route.target,
57
+ message: this.formatMessage(envelope),
58
+ metadata: {
59
+ source: 'nats',
60
+ eventId: envelope.id,
61
+ subject: envelope.subject,
62
+ priority: (ctx.enrichments['priority'] as number) ?? envelope.meta?.priority ?? 5,
63
+ },
64
+ });
65
+ await this.routerService.recordDelivery(route.id, envelope.subject);
66
+ this.metrics.recordConsume(envelope.subject);
67
+ await this.logService.logDelivery(route.id, envelope.subject, JSON.stringify({ eventId: envelope.id, target: route.target }));
68
+ } catch (routeErr) {
69
+ await this.logService.logError('route', route.id, envelope.subject, routeErr);
70
+ throw routeErr;
71
+ }
64
72
  }
65
73
  await message.ack();
66
74
  } else {
@@ -4,9 +4,10 @@ import { PreHandlersModule } from '../pre-handlers/pre-handlers.module';
4
4
  import { PendingModule } from '../pending/pending.module';
5
5
  import { RouterModule } from '../router/router.module';
6
6
  import { MetricsModule } from '../metrics/metrics.module';
7
+ import { LogModule } from '../logs/log.module';
7
8
 
8
9
  @Module({
9
- imports: [PreHandlersModule, PendingModule, RouterModule, MetricsModule],
10
+ imports: [PreHandlersModule, PendingModule, RouterModule, MetricsModule, LogModule],
10
11
  controllers: [ConsumerController],
11
12
  })
12
13
  export class ConsumerModule {}
@@ -0,0 +1,13 @@
1
+ CREATE TABLE `execution_logs` (
2
+ `id` text PRIMARY KEY NOT NULL,
3
+ `entity_type` text NOT NULL,
4
+ `entity_id` text NOT NULL,
5
+ `action` text NOT NULL,
6
+ `subject` text NOT NULL,
7
+ `detail` text,
8
+ `success` integer DEFAULT true NOT NULL,
9
+ `created_at` integer NOT NULL
10
+ );
11
+ --> statement-breakpoint
12
+ CREATE INDEX `execution_logs_entity_idx` ON `execution_logs` (`entity_type`,`entity_id`);--> statement-breakpoint
13
+ CREATE INDEX `execution_logs_created_at_idx` ON `execution_logs` (`created_at`);