@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.
- package/PLUGIN.md +9 -6
- package/README.md +12 -1
- package/dashboard/src/lib/CronPanel.svelte +206 -27
- package/dashboard/src/lib/LogsPanel.svelte +211 -0
- package/dashboard/src/lib/RoutesPanel.svelte +157 -13
- package/dashboard/src/lib/api.ts +77 -0
- package/dashboard/src/lib/components/ui/modal/index.ts +1 -0
- package/dashboard/src/lib/components/ui/modal/modal.svelte +49 -0
- package/dashboard/src/lib/utils.ts +8 -0
- package/package.json +1 -1
- package/sidecar/bun.lock +2 -2
- package/sidecar/package.json +1 -1
- package/sidecar/src/app.module.ts +2 -0
- package/sidecar/src/consumer/consumer.controller.ts +20 -12
- package/sidecar/src/consumer/consumer.module.ts +2 -1
- package/sidecar/src/db/migrations/0005_strong_supernaut.sql +13 -0
- package/sidecar/src/db/migrations/meta/0005_snapshot.json +389 -0
- package/sidecar/src/db/migrations/meta/_journal.json +7 -0
- package/sidecar/src/db/schema.ts +17 -0
- package/sidecar/src/logs/log.controller.ts +50 -0
- package/sidecar/src/logs/log.module.ts +11 -0
- package/sidecar/src/logs/log.repository.ts +78 -0
- package/sidecar/src/logs/log.service.ts +116 -0
- package/sidecar/src/router/router.controller.ts +28 -6
- package/sidecar/src/router/router.repository.ts +8 -0
- package/sidecar/src/router/router.service.ts +4 -0
- package/sidecar/src/scheduler/scheduler.controller.ts +32 -3
- package/sidecar/src/scheduler/scheduler.module.ts +2 -1
- package/sidecar/src/scheduler/scheduler.repository.ts +8 -0
- package/sidecar/src/scheduler/scheduler.service.ts +91 -25
- 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 {
|
|
7
|
-
import
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>
|
package/dashboard/src/lib/api.ts
CHANGED
|
@@ -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">×</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
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.
|
|
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.
|
|
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
|
|
package/sidecar/package.json
CHANGED
|
@@ -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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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`);
|