/** * Structured logger powered by LogTape (@logtape/logtape). * * Two tiers of logging: * - **logger** — general-purpose logging (suppressed in production for security) * - Dev: `trace`, `debug`, `info`, `warn`, `error` (configurable via PUBLIC_APP_LOG_LEVEL) * - Prod: `info` by default (or what PUBLIC_APP_LOG_LEVEL says) * - **inspector** — error-only logging, ALWAYS active (never suppressed). * Use for security-critical events that must appear in the browser * developer console (e.g. auth failures, API errors). * * API (100 % backward compatible): * logger.info('message', arg1, …) * logger.error('Failed:', err) * inspector.error('auth failure', uid) // always visible */ import { configureSync, getConfig, getConsoleSink, withFilter, getLogger } from '@logtape/logtape'; import type { LogLevel } from '@logtape/logtape'; // --------------------------------------------------------------------------- // LogTape configuration (idempotent — uses getConfig() not a local flag) // --------------------------------------------------------------------------- /** * Resolve the log level lazily (not at module-eval time). * * import.meta.env is undefined when Vite loads vite.config.ts, so we * must not read it at the top level. Deferring to first use ensures * Vite has finished initialising by the time we read it. */ function resolveLogLevel(): LogLevel { const env = import.meta.env as Record | undefined; const isDev = env?.DEV ?? (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production'); const configured = env?.PUBLIC_APP_LOG_LEVEL as string | undefined; return (configured || (isDev ? 'trace' : 'info')) as LogLevel; } function ensureConfigured() { // Vite evaluates the config module twice during build (SSR + client). // A local `_configured` boolean resets between evaluations, so we // rely on LogTape's own global state instead. if (getConfig() !== null) return; configureSync({ sinks: { console: getConsoleSink(), errorsOnly: withFilter(getConsoleSink(), 'error'), }, loggers: [ { category: ['supra-app'], sinks: ['console'], lowestLevel: resolveLogLevel(), }, { category: ['supra-app', 'inspector'], sinks: ['errorsOnly'], lowestLevel: 'error', }, { category: ['logtape', 'meta'], sinks: ['console'], lowestLevel: 'error', }, ], }); } // --------------------------------------------------------------------------- // Internal LogTape loggers (lazy-init) // --------------------------------------------------------------------------- function getBaseLogger() { ensureConfigured(); return getLogger(['supra-app']); } function getInspectorLogger() { ensureConfigured(); return getLogger(['supra-app', 'inspector']); } // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- function isRecord(v: unknown): v is Record { return typeof v === 'object' && v !== null && !(v instanceof Error) && !Array.isArray(v); } /** * Convert variadic args into a flat message string + optional extras. * * Patterns handled: * logger.info('text') → message, no props * logger.info('text', obj) → message, obj as props if obj is a plain Record * logger.info('text', error) → error overload * logger.info('text', arg1, arg2, …) → concatenated message */ function normalize( args: unknown[], ): { message: string; error?: Error; properties?: Record } { if (args.length === 0) return { message: '' }; const first = args[0]; if (args.length === 1) { if (first instanceof Error) return { message: first.message, error: first }; return { message: String(first) }; } if (args.length === 2 && isRecord(args[1])) { return { message: String(first), properties: args[1] }; } // error overload: logger.warn(msg, error) / logger.error('msg', err) if (args.length === 2 && args[1] instanceof Error) { return { message: String(first), error: args[1] as Error }; } // multi-arg: concatenate return { message: args.map((a) => (typeof a === 'object' ? safeStringify(a) : String(a))).join(' '), }; } function safeStringify(v: unknown): string { if (v instanceof Error) return `${v.name}: ${v.message}`; try { return JSON.stringify(v); } catch { return String(v); } } // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- export const logger = { trace(...args: unknown[]) { const { message } = normalize(args); getBaseLogger().trace(message); }, debug(...args: unknown[]) { const { message, properties } = normalize(args); if (properties) { getBaseLogger().debug(message, properties); } else { getBaseLogger().debug(message); } }, info(...args: unknown[]) { const { message, properties } = normalize(args); if (properties) { getBaseLogger().info(message, properties); } else { getBaseLogger().info(message); } }, warn(...args: unknown[]) { const { message, error, properties } = normalize(args); if (properties) { getBaseLogger().warn(message, properties); } else if (error) { getBaseLogger().warn(message, error); } else { getBaseLogger().warn(message); } }, error(...args: unknown[]) { const { message, error, properties } = normalize(args); if (error && properties) { getBaseLogger().error(error, properties); } else if (error) { getBaseLogger().error(error); } else if (properties) { getBaseLogger().error(message, properties); } else { getBaseLogger().error(message); } }, }; // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- /** * Inspector logger — always active, error-only, never suppressed. * Messages are written via the same LogTape infrastructure but on a * separate category (supra-app → inspector) whose sink is hard-filtered to * `error` / `fatal` so security-sensitive events are always visible. */ export const inspector = { error(...args: unknown[]) { const { message, error, properties } = normalize(args); if (error && properties) { getInspectorLogger().error(error, properties); } else if (error) { getInspectorLogger().error(error); } else if (properties) { getInspectorLogger().error(message, properties); } else { getInspectorLogger().error(message); } }, warn(...args: unknown[]) { const { message, error } = normalize(args); // inspector only logs errors; escalate to error level if (error) { getInspectorLogger().error(error); } else if (message) { getInspectorLogger().error(message); } }, info(...args: unknown[]) { const { message } = normalize(args); // promote to error so it passes the filter if (message) getInspectorLogger().error(message); }, };