@guckdev/core 0.1.0
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/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +178 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/redact.d.ts +3 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +68 -0
- package/dist/redact.js.map +1 -0
- package/dist/schema.d.ts +113 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +2 -0
- package/dist/schema.js.map +1 -0
- package/dist/store/backends/cloudwatch.d.ts +4 -0
- package/dist/store/backends/cloudwatch.d.ts.map +1 -0
- package/dist/store/backends/cloudwatch.js +302 -0
- package/dist/store/backends/cloudwatch.js.map +1 -0
- package/dist/store/backends/k8s.d.ts +4 -0
- package/dist/store/backends/k8s.d.ts.map +1 -0
- package/dist/store/backends/k8s.js +307 -0
- package/dist/store/backends/k8s.js.map +1 -0
- package/dist/store/backends/local.d.ts +10 -0
- package/dist/store/backends/local.d.ts.map +1 -0
- package/dist/store/backends/local.js +31 -0
- package/dist/store/backends/local.js.map +1 -0
- package/dist/store/backends/types.d.ts +25 -0
- package/dist/store/backends/types.d.ts.map +1 -0
- package/dist/store/backends/types.js +2 -0
- package/dist/store/backends/types.js.map +1 -0
- package/dist/store/file-store.d.ts +21 -0
- package/dist/store/file-store.d.ts.map +1 -0
- package/dist/store/file-store.js +169 -0
- package/dist/store/file-store.js.map +1 -0
- package/dist/store/filters.d.ts +4 -0
- package/dist/store/filters.d.ts.map +1 -0
- package/dist/store/filters.js +73 -0
- package/dist/store/filters.js.map +1 -0
- package/dist/store/read-store.d.ts +35 -0
- package/dist/store/read-store.d.ts.map +1 -0
- package/dist/store/read-store.js +256 -0
- package/dist/store/read-store.js.map +1 -0
- package/dist/store/time.d.ts +4 -0
- package/dist/store/time.d.ts.map +1 -0
- package/dist/store/time.js +47 -0
- package/dist/store/time.js.map +1 -0
- package/package.json +38 -0
- package/src/config.ts +210 -0
- package/src/index.ts +6 -0
- package/src/redact.ts +83 -0
- package/src/schema.ts +130 -0
- package/src/store/backends/cloudwatch.ts +373 -0
- package/src/store/backends/k8s.ts +400 -0
- package/src/store/backends/local.ts +47 -0
- package/src/store/backends/types.ts +18 -0
- package/src/store/file-store.ts +217 -0
- package/src/store/filters.ts +83 -0
- package/src/store/read-store.ts +340 -0
- package/src/store/time.ts +54 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import {
|
|
4
|
+
GuckCloudWatchReadBackendConfig,
|
|
5
|
+
GuckEvent,
|
|
6
|
+
GuckSearchParams,
|
|
7
|
+
GuckSessionsParams,
|
|
8
|
+
GuckStatsParams,
|
|
9
|
+
} from "../../schema.js";
|
|
10
|
+
import { eventMatches, normalizeLevel } from "../filters.js";
|
|
11
|
+
import { normalizeTimestamp, parseTimeInput } from "../time.js";
|
|
12
|
+
import { ReadBackend, SearchResult, SessionsResult, StatsResult } from "./types.js";
|
|
13
|
+
|
|
14
|
+
type AwsClient = {
|
|
15
|
+
send: (command: unknown) => Promise<{
|
|
16
|
+
events?: Array<{
|
|
17
|
+
eventId?: string;
|
|
18
|
+
timestamp?: number;
|
|
19
|
+
message?: string;
|
|
20
|
+
logStreamName?: string;
|
|
21
|
+
}>;
|
|
22
|
+
nextToken?: string;
|
|
23
|
+
}>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const inferLevel = (message?: string): "fatal" | "error" | "warn" | "info" | "debug" | "trace" => {
|
|
27
|
+
const text = message?.toLowerCase() ?? "";
|
|
28
|
+
if (text.includes("fatal")) {
|
|
29
|
+
return "fatal";
|
|
30
|
+
}
|
|
31
|
+
if (text.includes("error")) {
|
|
32
|
+
return "error";
|
|
33
|
+
}
|
|
34
|
+
if (text.includes("warn")) {
|
|
35
|
+
return "warn";
|
|
36
|
+
}
|
|
37
|
+
if (text.includes("debug")) {
|
|
38
|
+
return "debug";
|
|
39
|
+
}
|
|
40
|
+
if (text.includes("trace")) {
|
|
41
|
+
return "trace";
|
|
42
|
+
}
|
|
43
|
+
return "info";
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const deriveService = (logGroup: string): string => {
|
|
47
|
+
const trimmed = logGroup.trim();
|
|
48
|
+
const parts = trimmed.split("/");
|
|
49
|
+
const last = parts[parts.length - 1];
|
|
50
|
+
return last && last.length > 0 ? last : trimmed;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const isGuckLikeObject = (value: Record<string, unknown>): boolean => {
|
|
54
|
+
const keys = [
|
|
55
|
+
"id",
|
|
56
|
+
"ts",
|
|
57
|
+
"level",
|
|
58
|
+
"type",
|
|
59
|
+
"service",
|
|
60
|
+
"run_id",
|
|
61
|
+
"session_id",
|
|
62
|
+
"message",
|
|
63
|
+
"data",
|
|
64
|
+
"tags",
|
|
65
|
+
"trace_id",
|
|
66
|
+
"span_id",
|
|
67
|
+
"source",
|
|
68
|
+
];
|
|
69
|
+
return keys.some((key) => key in value);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const extractTags = (value: unknown): Record<string, string> | undefined => {
|
|
73
|
+
if (!value || typeof value !== "object") {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const record: Record<string, string> = {};
|
|
77
|
+
for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
|
|
78
|
+
if (typeof entry === "string") {
|
|
79
|
+
record[key] = entry;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return Object.keys(record).length > 0 ? record : undefined;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const extractData = (
|
|
86
|
+
value: Record<string, unknown>,
|
|
87
|
+
fallbackMessage: string | undefined,
|
|
88
|
+
): Record<string, unknown> | undefined => {
|
|
89
|
+
if (value.data && typeof value.data === "object" && value.data !== null) {
|
|
90
|
+
return value.data as Record<string, unknown>;
|
|
91
|
+
}
|
|
92
|
+
const knownKeys = new Set([
|
|
93
|
+
"id",
|
|
94
|
+
"ts",
|
|
95
|
+
"level",
|
|
96
|
+
"type",
|
|
97
|
+
"service",
|
|
98
|
+
"run_id",
|
|
99
|
+
"session_id",
|
|
100
|
+
"message",
|
|
101
|
+
"data",
|
|
102
|
+
"tags",
|
|
103
|
+
"trace_id",
|
|
104
|
+
"span_id",
|
|
105
|
+
"source",
|
|
106
|
+
]);
|
|
107
|
+
const extra: Record<string, unknown> = {};
|
|
108
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
109
|
+
if (!knownKeys.has(key)) {
|
|
110
|
+
extra[key] = entry;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (Object.keys(extra).length === 0) {
|
|
114
|
+
return fallbackMessage ? { raw_message: fallbackMessage } : undefined;
|
|
115
|
+
}
|
|
116
|
+
return extra;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const normalizeSource = (
|
|
120
|
+
source: unknown,
|
|
121
|
+
backendId?: string,
|
|
122
|
+
): { kind: "mcp"; backend: "cloudwatch"; backend_id?: string } & Record<string, unknown> => {
|
|
123
|
+
const base =
|
|
124
|
+
source && typeof source === "object" ? (source as Record<string, unknown>) : {};
|
|
125
|
+
return {
|
|
126
|
+
...base,
|
|
127
|
+
kind: "mcp",
|
|
128
|
+
backend: "cloudwatch",
|
|
129
|
+
backend_id: backendId ?? (base.backend_id as string | undefined),
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const toEvent = (
|
|
134
|
+
config: GuckCloudWatchReadBackendConfig,
|
|
135
|
+
logEvent: { eventId?: string; timestamp?: number; message?: string; logStreamName?: string },
|
|
136
|
+
): GuckEvent => {
|
|
137
|
+
const rawMessage = logEvent.message ?? "";
|
|
138
|
+
const trimmed = rawMessage.trim();
|
|
139
|
+
const parsedTs = logEvent.timestamp ? new Date(logEvent.timestamp).toISOString() : new Date().toISOString();
|
|
140
|
+
const id = logEvent.eventId ?? randomUUID();
|
|
141
|
+
const service = config.service ?? deriveService(config.logGroup);
|
|
142
|
+
const runId = logEvent.logStreamName ?? logEvent.eventId ?? id;
|
|
143
|
+
const fallback = {
|
|
144
|
+
id,
|
|
145
|
+
ts: parsedTs,
|
|
146
|
+
level: inferLevel(rawMessage),
|
|
147
|
+
type: "log",
|
|
148
|
+
service,
|
|
149
|
+
run_id: runId,
|
|
150
|
+
message: rawMessage,
|
|
151
|
+
data: { logStreamName: logEvent.logStreamName, raw_message: rawMessage },
|
|
152
|
+
source: normalizeSource(undefined, config.id),
|
|
153
|
+
} satisfies GuckEvent;
|
|
154
|
+
|
|
155
|
+
if (!trimmed.startsWith("{")) {
|
|
156
|
+
return fallback;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const parsed = JSON.parse(trimmed);
|
|
161
|
+
if (!parsed || typeof parsed !== "object") {
|
|
162
|
+
return fallback;
|
|
163
|
+
}
|
|
164
|
+
const record = parsed as Record<string, unknown>;
|
|
165
|
+
if (!isGuckLikeObject(record)) {
|
|
166
|
+
return fallback;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const level = normalizeLevel(record.level as string | undefined) ?? inferLevel(rawMessage);
|
|
170
|
+
const message =
|
|
171
|
+
typeof record.message === "string" ? record.message : rawMessage || undefined;
|
|
172
|
+
const source = normalizeSource(record.source, config.id);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
id: typeof record.id === "string" ? record.id : id,
|
|
176
|
+
ts: typeof record.ts === "string" ? record.ts : parsedTs,
|
|
177
|
+
level,
|
|
178
|
+
type: typeof record.type === "string" ? record.type : "log",
|
|
179
|
+
service: typeof record.service === "string" ? record.service : service,
|
|
180
|
+
run_id: typeof record.run_id === "string" ? record.run_id : runId,
|
|
181
|
+
session_id: typeof record.session_id === "string" ? record.session_id : undefined,
|
|
182
|
+
message,
|
|
183
|
+
data: extractData(record, rawMessage),
|
|
184
|
+
tags: extractTags(record.tags),
|
|
185
|
+
trace_id: typeof record.trace_id === "string" ? record.trace_id : undefined,
|
|
186
|
+
span_id: typeof record.span_id === "string" ? record.span_id : undefined,
|
|
187
|
+
source,
|
|
188
|
+
};
|
|
189
|
+
} catch {
|
|
190
|
+
return fallback;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const requireModule = createRequire(import.meta.url);
|
|
195
|
+
|
|
196
|
+
const loadSdk = (): {
|
|
197
|
+
CloudWatchLogsClient: new (config: { region: string; credentials?: unknown }) => AwsClient;
|
|
198
|
+
FilterLogEventsCommand: new (input: {
|
|
199
|
+
logGroupName: string;
|
|
200
|
+
startTime?: number;
|
|
201
|
+
endTime?: number;
|
|
202
|
+
nextToken?: string;
|
|
203
|
+
limit?: number;
|
|
204
|
+
}) => unknown;
|
|
205
|
+
} => {
|
|
206
|
+
try {
|
|
207
|
+
return requireModule("@aws-sdk/client-cloudwatch-logs");
|
|
208
|
+
} catch {
|
|
209
|
+
throw new Error(
|
|
210
|
+
"CloudWatch backend requires @aws-sdk/client-cloudwatch-logs. Install it to enable this backend.",
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const loadCredentialProviders = (): { fromIni: (input: { profile: string }) => unknown } => {
|
|
216
|
+
try {
|
|
217
|
+
return requireModule("@aws-sdk/credential-providers");
|
|
218
|
+
} catch {
|
|
219
|
+
throw new Error(
|
|
220
|
+
"CloudWatch backend requires @aws-sdk/credential-providers when using profile override.",
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export const createCloudWatchBackend = (
|
|
226
|
+
config: GuckCloudWatchReadBackendConfig,
|
|
227
|
+
): ReadBackend => {
|
|
228
|
+
let client: AwsClient | null = null;
|
|
229
|
+
let FilterLogEventsCommand: ReturnType<typeof loadSdk>["FilterLogEventsCommand"] | null = null;
|
|
230
|
+
const getClient = (): { client: AwsClient; FilterLogEventsCommand: ReturnType<typeof loadSdk>["FilterLogEventsCommand"] } => {
|
|
231
|
+
if (client && FilterLogEventsCommand) {
|
|
232
|
+
return { client, FilterLogEventsCommand };
|
|
233
|
+
}
|
|
234
|
+
const sdk = loadSdk();
|
|
235
|
+
FilterLogEventsCommand = sdk.FilterLogEventsCommand;
|
|
236
|
+
const credentials = config.profile
|
|
237
|
+
? loadCredentialProviders().fromIni({ profile: config.profile })
|
|
238
|
+
: undefined;
|
|
239
|
+
const sdkClient = new sdk.CloudWatchLogsClient({
|
|
240
|
+
region: config.region,
|
|
241
|
+
credentials,
|
|
242
|
+
});
|
|
243
|
+
client = {
|
|
244
|
+
send: (command) => sdkClient.send(command as unknown),
|
|
245
|
+
};
|
|
246
|
+
return { client, FilterLogEventsCommand };
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const fetchEvents = async (
|
|
250
|
+
params: GuckSearchParams,
|
|
251
|
+
): Promise<SearchResult> => {
|
|
252
|
+
const { client, FilterLogEventsCommand } = getClient();
|
|
253
|
+
const limit = params.limit ?? 200;
|
|
254
|
+
const sinceMs = params.since ? parseTimeInput(params.since) : undefined;
|
|
255
|
+
const untilMs = params.until ? parseTimeInput(params.until) : undefined;
|
|
256
|
+
const events: GuckEvent[] = [];
|
|
257
|
+
let truncated = false;
|
|
258
|
+
let nextToken: string | undefined = undefined;
|
|
259
|
+
|
|
260
|
+
do {
|
|
261
|
+
const remaining = Math.max(limit - events.length, 1);
|
|
262
|
+
const response = await client.send(
|
|
263
|
+
new FilterLogEventsCommand({
|
|
264
|
+
logGroupName: config.logGroup,
|
|
265
|
+
startTime: sinceMs,
|
|
266
|
+
endTime: untilMs,
|
|
267
|
+
nextToken,
|
|
268
|
+
limit: Math.min(remaining, 10000),
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
const batch = response.events ?? [];
|
|
272
|
+
for (const logEvent of batch) {
|
|
273
|
+
const event = toEvent(config, logEvent);
|
|
274
|
+
if (!eventMatches(event, params, sinceMs, untilMs)) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
events.push(event);
|
|
278
|
+
if (events.length >= limit) {
|
|
279
|
+
truncated = true;
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const receivedToken = response.nextToken;
|
|
284
|
+
if (receivedToken && receivedToken === nextToken) {
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
nextToken = receivedToken;
|
|
288
|
+
} while (nextToken && !truncated);
|
|
289
|
+
|
|
290
|
+
if (nextToken) {
|
|
291
|
+
truncated = true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return { events, truncated };
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const stats = async (params: GuckStatsParams): Promise<StatsResult> => {
|
|
298
|
+
const searchParams: GuckSearchParams = {
|
|
299
|
+
service: params.service,
|
|
300
|
+
session_id: params.session_id,
|
|
301
|
+
since: params.since,
|
|
302
|
+
until: params.until,
|
|
303
|
+
limit: params.limit ?? 1000,
|
|
304
|
+
};
|
|
305
|
+
const result = await fetchEvents(searchParams);
|
|
306
|
+
const buckets = new Map<string, number>();
|
|
307
|
+
for (const event of result.events) {
|
|
308
|
+
let key = "unknown";
|
|
309
|
+
if (params.group_by === "type") {
|
|
310
|
+
key = event.type;
|
|
311
|
+
} else if (params.group_by === "level") {
|
|
312
|
+
key = event.level;
|
|
313
|
+
} else if (params.group_by === "stage") {
|
|
314
|
+
const stage = (event.data as Record<string, unknown> | undefined)?.stage;
|
|
315
|
+
key = typeof stage === "string" ? stage : "unknown";
|
|
316
|
+
}
|
|
317
|
+
buckets.set(key, (buckets.get(key) ?? 0) + 1);
|
|
318
|
+
}
|
|
319
|
+
const limit = params.limit ?? 200;
|
|
320
|
+
const sorted = [...buckets.entries()]
|
|
321
|
+
.sort((a, b) => b[1] - a[1])
|
|
322
|
+
.slice(0, limit)
|
|
323
|
+
.map(([key, count]) => ({ key, count }));
|
|
324
|
+
return { buckets: sorted };
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const sessions = async (params: GuckSessionsParams): Promise<SessionsResult> => {
|
|
328
|
+
const searchParams: GuckSearchParams = {
|
|
329
|
+
service: params.service,
|
|
330
|
+
since: params.since,
|
|
331
|
+
limit: params.limit ?? 1000,
|
|
332
|
+
};
|
|
333
|
+
const result = await fetchEvents(searchParams);
|
|
334
|
+
const sessions = new Map<
|
|
335
|
+
string,
|
|
336
|
+
{ session_id: string; last_ts: string; event_count: number; error_count: number }
|
|
337
|
+
>();
|
|
338
|
+
for (const event of result.events) {
|
|
339
|
+
if (!event.session_id) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const existing = sessions.get(event.session_id) ?? {
|
|
343
|
+
session_id: event.session_id,
|
|
344
|
+
last_ts: event.ts,
|
|
345
|
+
event_count: 0,
|
|
346
|
+
error_count: 0,
|
|
347
|
+
};
|
|
348
|
+
existing.event_count += 1;
|
|
349
|
+
if (event.level === "error" || event.level === "fatal") {
|
|
350
|
+
existing.error_count += 1;
|
|
351
|
+
}
|
|
352
|
+
const existingTs = normalizeTimestamp(existing.last_ts) ?? 0;
|
|
353
|
+
const eventTs = normalizeTimestamp(event.ts) ?? 0;
|
|
354
|
+
if (eventTs > existingTs) {
|
|
355
|
+
existing.last_ts = event.ts;
|
|
356
|
+
}
|
|
357
|
+
sessions.set(event.session_id, existing);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const limit = params.limit ?? 200;
|
|
361
|
+
const sorted = [...sessions.values()]
|
|
362
|
+
.sort((a, b) => (normalizeTimestamp(b.last_ts) ?? 0) - (normalizeTimestamp(a.last_ts) ?? 0))
|
|
363
|
+
.slice(0, limit);
|
|
364
|
+
|
|
365
|
+
return { sessions: sorted };
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
search: fetchEvents,
|
|
370
|
+
stats,
|
|
371
|
+
sessions,
|
|
372
|
+
};
|
|
373
|
+
};
|