Supra_App/src/lib/core/utils/logger.ts

226 lines
7.1 KiB
TypeScript
Raw Normal View History

/**
* 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<string, string | boolean | undefined> | 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<string, unknown> {
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<string, unknown> } {
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);
},
};