mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-12 19:57:37 +00:00
feat(stack-activity): in-process metrics, structured diagnostic logs, docs (#1229)
Phase 3 + Phase 6 of the Stack Activity audit (PR 2 of 2): - StackActivityMetricsService: in-process counters and ring-buffered latency histogram (1000 samples per nodeId/op pair). Mirrors the FileExplorerMetricsService pattern shipped in #1216. No external export. Records (nodeId, op) where op is read or write, with success/error counts and p50/p95 latency on demand. - Admin endpoint GET /api/stack-activity-metrics returns the snapshot. Admin-only via requireAdmin, mounted next to the file-explorer metrics route. An operator debugging "why is the activity tab slow on this node?" can pull per-(nodeId, op) counts and latencies without scrolling logs. - Diagnostic logs: route handler emits a structured [StackActivity:diag] read entry per request (stackName, nodeId, limit, before, beforeId, returned, elapsedMs); dispatchAlert emits a [StackActivity:diag] write entry per persisted notification (category, stackName, nodeId, actor, messageLen). Both gated on developer_mode via isDebugEnabled. Same namespace so a single grep covers reads and writes on the timeline path. Per-request and per-event, never inside a poll loop. - Metric record points: the route's try/finally records a read metric with the outcome of the DB call; dispatchAlert records a write metric on both the success path and (before re-throwing) the failure path, so error rates from the insert path stay visible. - docs/features/stack-activity.mdx: refreshed to reflect PR 1's retention behavior (30 days plus per-(node, stack) 500-row cap, 1000 per-node unattached), composite (timestamp, id) cursor, error-vs- empty UI distinction, and "by username" vs "via Subsystem" actor rendering. Adds Troubleshooting entries for "Activity unavailable" (node disconnect or fetch failure), "expected event missing" (retention windows), and "same restart shows twice" (manual click vs Auto-Heal redeploy are distinct events). No tier, role, or capability gate touched. The admin metrics endpoint inherits the standard requireAdmin gate already used by /api/file- explorer-metrics and /api/stack-metrics.
This commit is contained in:
@@ -5,6 +5,7 @@ import { isDebugEnabled } from '../utils/debug';
|
||||
import { getErrorMessage } from '../utils/errors';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
import { sanitizeNotificationMessage } from '../utils/notificationMessage';
|
||||
import { StackActivityMetricsService } from './StackActivityMetricsService';
|
||||
|
||||
export type NotificationCategory =
|
||||
| 'deploy_success'
|
||||
@@ -115,25 +116,42 @@ export class NotificationService {
|
||||
message: string,
|
||||
options?: { stackName?: string; containerName?: string; actor?: string },
|
||||
) {
|
||||
const t0 = Date.now();
|
||||
const { stackName, containerName, actor } = options ?? {};
|
||||
// Internal writes use the middleware default so they share a row key
|
||||
// with user-initiated requests; otherwise the UI and monitors split
|
||||
// between different node_id buckets.
|
||||
const localNodeId = NodeRegistry.getInstance().getDefaultNodeId();
|
||||
// Use the full resolution chain (node.compose_dir → env → default)
|
||||
// Use the full resolution chain (node.compose_dir, env, default)
|
||||
// so messages mentioning a per-node compose override get collapsed.
|
||||
const sanitized = sanitizeNotificationMessage(message, {
|
||||
composeDir: NodeRegistry.getInstance().getComposeDir(localNodeId),
|
||||
});
|
||||
const notification = this.dbService.addNotificationHistory(localNodeId, {
|
||||
level,
|
||||
category,
|
||||
message: sanitized,
|
||||
timestamp: Date.now(),
|
||||
stack_name: stackName,
|
||||
container_name: containerName,
|
||||
actor_username: actor ?? null,
|
||||
});
|
||||
|
||||
let notification: NotificationHistory;
|
||||
try {
|
||||
notification = this.dbService.addNotificationHistory(localNodeId, {
|
||||
level,
|
||||
category,
|
||||
message: sanitized,
|
||||
timestamp: Date.now(),
|
||||
stack_name: stackName,
|
||||
container_name: containerName,
|
||||
actor_username: actor ?? null,
|
||||
});
|
||||
} catch (err) {
|
||||
StackActivityMetricsService.getInstance().record(localNodeId, 'write', Date.now() - t0, false);
|
||||
throw err;
|
||||
}
|
||||
StackActivityMetricsService.getInstance().record(localNodeId, 'write', Date.now() - t0, true);
|
||||
// Separate [StackActivity:diag] namespace from the [Notify:diag] lines
|
||||
// below so a single grep can pull every per-stack timeline write across
|
||||
// route reads and dispatch writes.
|
||||
if (isDebugEnabled()) {
|
||||
console.log('[StackActivity:diag] write', {
|
||||
category, stackName, nodeId: localNodeId, actor: actor ?? null, messageLen: sanitized.length,
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Push to connected browser clients via WebSocket
|
||||
this.broadcastToSubscribers(notification);
|
||||
|
||||
Reference in New Issue
Block a user