Structured logging
Logging and error metrics pattern for observability.
What it demonstrates:
- Custom
Loggerimplementation for JSON output - Error tracking by phase and topic
- Integration with
onErrorcallback
Logger extra keys
The engine passes a structured extra object to every logger call:
| Log message | Extra keys |
|---|---|
starting / started / stopped | topic |
subscribed | topic |
applied snapshot | topic, revision |
snapshot skipped (not newer) | topic, snapshotRevision, localRevision |
invalidation triggered refresh | topic, eventRevision |
invalidation skipped (not newer) | topic, eventRevision, localRevision |
invalidation skipped (shouldRefresh) | topic, event |
refresh coalesced (in-flight) | topic |
Error (via onError) | phase, topic, error, localRevision, eventRevision?, snapshotRevision?, sourceId?, attempt?, willRetry?, nextDelayMs? |
Full example
typescript
import type { Logger, SyncErrorContext, SyncPhase } from '@statesync/core';
import { createRevisionSync } from '@statesync/core';
// --- Structured Logger ---
function createStructuredLogger(topic: string): Logger {
return {
debug(msg, extra) {
console.log(JSON.stringify({ level: 'debug', topic, msg, ...normalize(extra) }));
},
warn(msg, extra) {
console.warn(JSON.stringify({ level: 'warn', topic, msg, ...normalize(extra) }));
},
error(msg, extra) {
console.error(JSON.stringify({ level: 'error', topic, msg, ...normalize(extra) }));
},
};
}
function normalize(extra: unknown): Record<string, unknown> {
if (extra && typeof extra === 'object' && !Array.isArray(extra)) {
return extra as Record<string, unknown>;
}
return extra !== undefined ? { value: extra } : {};
}
// --- Error Metrics ---
const errorCounts = new Map<string, number>();
function trackError(ctx: SyncErrorContext): void {
const key = `${ctx.topic ?? 'unknown'}:${ctx.phase}`;
errorCounts.set(key, (errorCounts.get(key) ?? 0) + 1);
console.error(
JSON.stringify({
level: 'error',
event: 'sync_error',
topic: ctx.topic,
phase: ctx.phase,
error: ctx.error instanceof Error ? ctx.error.message : String(ctx.error),
totalForKey: errorCounts.get(key),
}),
);
}
function getErrorMetrics(): Record<string, number> {
return Object.fromEntries(errorCounts);
}
function getErrorCountByPhase(phase: SyncPhase): number {
let total = 0;
for (const [key, count] of errorCounts) {
if (key.endsWith(`:${phase}`)) total += count;
}
return total;
}
// --- Usage ---
const topic = 'settings';
const sync = createRevisionSync({
topic,
subscriber: { subscribe: async () => () => {} },
provider: { getSnapshot: async () => ({ revision: '1' as never, data: {} }) },
applier: { apply() {} },
logger: createStructuredLogger(topic),
onError: trackError,
});
// Start sync
await sync.start();
// After some time, inspect metrics:
console.log(getErrorMetrics());
// { "settings:getSnapshot": 2, "settings:apply": 1 }
console.log('getSnapshot errors:', getErrorCountByPhase('getSnapshot'));
console.log('apply errors:', getErrorCountByPhase('apply'));Example output
json
{"level":"debug","topic":"settings","msg":"[state-sync] subscribed"}
{"level":"debug","topic":"settings","msg":"[state-sync] applied snapshot","revision":"1"}
{"level":"error","event":"sync_error","topic":"settings","phase":"getSnapshot","error":"Network error","totalForKey":1}Quick start with built-in logger
For simpler cases, use the built-in createConsoleLogger:
typescript
import { createConsoleLogger, createRevisionSync } from '@statesync/core';
const sync = createRevisionSync({
topic: 'settings',
subscriber,
provider,
applier,
logger: createConsoleLogger({ debug: true }),
});Adding tags with tagLogger
Add context (window ID, user ID) to all log entries:
typescript
import { createConsoleLogger, tagLogger } from '@statesync/core';
const baseLogger = createConsoleLogger({ debug: true });
const logger = tagLogger(baseLogger, { windowId: 'main', userId: '123' });
// All logs will include: { windowId: 'main', userId: '123', ... }See also
- Troubleshooting — debug sync issues using phases
- Lifecycle contract — full error phases reference
- Error handling example — retry and graceful degradation
