@mandujs/core 0.20.8 → 0.20.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/core",
3
- "version": "0.20.8",
3
+ "version": "0.20.9",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -45,6 +45,8 @@ export interface ManduConfig {
45
45
  dev?: {
46
46
  hmr?: boolean;
47
47
  watchDirs?: string[];
48
+ /** Observability SQLite 영구 저장 (기본: true) */
49
+ observability?: boolean;
48
50
  };
49
51
  fsRoutes?: {
50
52
  routesDir?: string;
@@ -15,6 +15,7 @@ import { FileAPI } from "./api/file-api";
15
15
  import { GuardDecisionManager } from "./api/guard-decisions";
16
16
  import { ContractPlaygroundAPI } from "./api/contract-api";
17
17
  import { renderKitchenHTML } from "./kitchen-ui";
18
+ import { eventBus } from "../observability/event-bus";
18
19
  import fs from "fs/promises";
19
20
  import path from "path";
20
21
 
@@ -77,6 +78,21 @@ export function getRecentRequests(): RequestEntry[] {
77
78
  return [...recentRequests].reverse();
78
79
  }
79
80
 
81
+ /** Parse a window string like "5m", "30s", "1h" into milliseconds. */
82
+ function parseWindow(input: string): number {
83
+ const match = /^(\d+)\s*(ms|s|m|h)?$/.exec(input.trim());
84
+ if (!match) return 5 * 60 * 1000;
85
+ const value = parseInt(match[1], 10);
86
+ const unit = match[2] || "m";
87
+ switch (unit) {
88
+ case "ms": return value;
89
+ case "s": return value * 1000;
90
+ case "m": return value * 60 * 1000;
91
+ case "h": return value * 60 * 60 * 1000;
92
+ default: return 5 * 60 * 1000;
93
+ }
94
+ }
95
+
80
96
  export class KitchenHandler {
81
97
  private sse: ActivitySSEBroadcaster;
82
98
  private guardAPI: GuardAPI;
@@ -222,19 +238,41 @@ export class KitchenHandler {
222
238
  return Response.json({ removed: true });
223
239
  }
224
240
 
225
- // Requests API — recent HTTP request log
241
+ // Requests API — recent HTTP events from eventBus (fallback: ring buffer)
226
242
  if (sub === "/api/requests" && req.method === "GET") {
227
- return Response.json({ requests: getRecentRequests() });
243
+ const url = new URL(req.url);
244
+ const limit = Math.min(parseInt(url.searchParams.get("limit") || "100", 10) || 100, 500);
245
+ const busEvents = eventBus.getRecent(limit, { type: "http" });
246
+ if (busEvents.length > 0) {
247
+ return Response.json({ requests: busEvents.slice().reverse() });
248
+ }
249
+ return Response.json({ requests: getRecentRequests().slice(0, limit) });
250
+ }
251
+
252
+ // Correlation API — all events linked to a correlationId
253
+ if (sub === "/api/correlation" && req.method === "GET") {
254
+ const url = new URL(req.url);
255
+ const cid = url.searchParams.get("id") || "";
256
+ if (!cid) return Response.json({ events: [] });
257
+ const all = eventBus.getRecent(500);
258
+ const events = all.filter((e) => e.correlationId === cid);
259
+ return Response.json({ events });
228
260
  }
229
261
 
230
- // Activity API — recent MCP tool calls from activity.jsonl
262
+ // Activity API — recent MCP events from eventBus (fallback: activity.jsonl)
231
263
  if (sub === "/api/activity" && req.method === "GET") {
264
+ const url = new URL(req.url);
265
+ const limit = Math.min(parseInt(url.searchParams.get("limit") || "100", 10) || 100, 500);
266
+ const busEvents = eventBus.getRecent(limit, { type: "mcp" });
267
+ if (busEvents.length > 0) {
268
+ return Response.json({ events: busEvents.slice().reverse() });
269
+ }
232
270
  const events = await this.readRecentActivity();
233
271
  return Response.json({ events });
234
272
  }
235
273
 
236
274
  // Cache API — cache store stats
237
- if (sub === "/api/cache" && req.method === "GET") {
275
+ if ((sub === "/api/cache" || sub === "/api/cache-stats") && req.method === "GET") {
238
276
  const store = getGlobalCache();
239
277
  return Response.json({
240
278
  enabled: !!store,
@@ -243,6 +281,45 @@ export class KitchenHandler {
243
281
  });
244
282
  }
245
283
 
284
+ // Metrics API — rolling-window eventBus stats
285
+ if (sub === "/api/metrics" && req.method === "GET") {
286
+ const url = new URL(req.url);
287
+ const windowParam = url.searchParams.get("window") || "5m";
288
+ const windowMs = parseWindow(windowParam);
289
+ const stats = eventBus.getStats(windowMs);
290
+ const httpEvents = eventBus.getRecent(500, { type: "http" })
291
+ .filter((e) => e.timestamp >= Date.now() - windowMs);
292
+ const durations = httpEvents
293
+ .map((e) => e.duration)
294
+ .filter((d): d is number => typeof d === "number")
295
+ .sort((a, b) => a - b);
296
+ const percentile = (p: number) => {
297
+ if (!durations.length) return 0;
298
+ const idx = Math.min(durations.length - 1, Math.floor((p / 100) * durations.length));
299
+ return durations[idx];
300
+ };
301
+ const totalEvents = Object.values(stats).reduce((sum, s) => sum + s.count, 0);
302
+ const totalErrors = Object.values(stats).reduce((sum, s) => sum + s.errors, 0);
303
+ return Response.json({
304
+ window: windowParam,
305
+ windowMs,
306
+ stats,
307
+ http: {
308
+ count: httpEvents.length,
309
+ p50: percentile(50),
310
+ p95: percentile(95),
311
+ p99: percentile(99),
312
+ },
313
+ mcp: {
314
+ count: stats.mcp.count,
315
+ errors: stats.mcp.errors,
316
+ avgDuration: stats.mcp.avgDuration,
317
+ },
318
+ errorRate: totalEvents > 0 ? totalErrors / totalEvents : 0,
319
+ totalEvents,
320
+ });
321
+ }
322
+
246
323
  // Error API (Kitchen → MCP bridge)
247
324
  if (sub === "/api/errors" && req.method === "POST") {
248
325
  try {