2026-06-19 17:27:28 +07:00
|
|
|
/**
|
2026-07-01 17:08:46 +07:00
|
|
|
* Structured logger powered by LogTape (@logtape/logtape).
|
2026-06-19 17:27:28 +07:00
|
|
|
*
|
2026-07-01 17:08:46 +07:00
|
|
|
* 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).
|
2026-06-19 17:27:28 +07:00
|
|
|
*
|
2026-07-01 17:08:46 +07:00
|
|
|
* API (100 % backward compatible):
|
|
|
|
|
* logger.info('message', arg1, …)
|
|
|
|
|
* logger.error('Failed:', err)
|
|
|
|
|
* inspector.error('auth failure', uid) // always visible
|
2026-06-19 17:27:28 +07:00
|
|
|
*/
|
2026-07-01 17:08:46 +07:00
|
|
|
import { configureSync, getConsoleSink, withFilter, getLogger } from '@logtape/logtape';
|
2026-06-19 17:27:28 +07:00
|
|
|
|
2026-07-01 17:08:46 +07:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Environment helpers
|
|
|
|
|
// ---------------------------------------------------------------------------
|
2026-06-19 17:27:28 +07:00
|
|
|
|
|
|
|
|
const IS_DEV = import.meta.env.DEV;
|
2026-07-01 17:08:46 +07:00
|
|
|
const ENV_LOG_LEVEL = (import.meta.env.PUBLIC_APP_LOG_LEVEL as string) || (IS_DEV ? 'trace' : 'info');
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// LogTape configuration (singleton guard for Vite HMR / config reload)
|
|
|
|
|
// ---------------------------------------------------------------------------
|
2026-06-19 17:27:28 +07:00
|
|
|
|
2026-07-01 17:08:46 +07:00
|
|
|
let _configured = false;
|
|
|
|
|
function ensureConfigured() {
|
|
|
|
|
if (_configured) return;
|
|
|
|
|
_configured = true;
|
|
|
|
|
configureSync({
|
|
|
|
|
sinks: {
|
|
|
|
|
console: getConsoleSink(),
|
|
|
|
|
errorsOnly: withFilter(getConsoleSink(), 'error'),
|
|
|
|
|
},
|
|
|
|
|
loggers: [
|
|
|
|
|
{
|
|
|
|
|
category: ['supra-app'],
|
|
|
|
|
sinks: ['console'],
|
|
|
|
|
lowestLevel: ENV_LOG_LEVEL as 'trace' | 'debug' | 'info' | 'warning' | 'error' | 'fatal',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
category: ['supra-app', 'inspector'],
|
|
|
|
|
sinks: ['errorsOnly'],
|
|
|
|
|
lowestLevel: 'error',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
category: ['logtape', 'meta'],
|
|
|
|
|
sinks: ['console'],
|
|
|
|
|
lowestLevel: 'error',
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|
2026-06-19 17:27:28 +07:00
|
|
|
}
|
|
|
|
|
|
2026-07-01 17:08:46 +07:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// 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);
|
2026-06-19 17:27:28 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-07-01 17:08:46 +07:00
|
|
|
* 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
|
2026-06-19 17:27:28 +07:00
|
|
|
*/
|
2026-07-01 17:08:46 +07:00
|
|
|
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}`;
|
2026-06-19 17:27:28 +07:00
|
|
|
try {
|
2026-07-01 17:08:46 +07:00
|
|
|
return JSON.stringify(v);
|
2026-06-19 17:27:28 +07:00
|
|
|
} catch {
|
2026-07-01 17:08:46 +07:00
|
|
|
return String(v);
|
2026-06-19 17:27:28 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-01 17:08:46 +07:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
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);
|
2026-06-19 17:27:28 +07:00
|
|
|
}
|
2026-07-01 17:08:46 +07:00
|
|
|
},
|
2026-06-19 17:27:28 +07:00
|
|
|
|
2026-07-01 17:08:46 +07:00
|
|
|
info(...args: unknown[]) {
|
|
|
|
|
const { message, properties } = normalize(args);
|
|
|
|
|
if (properties) {
|
|
|
|
|
getBaseLogger().info(message, properties);
|
|
|
|
|
} else {
|
|
|
|
|
getBaseLogger().info(message);
|
2026-06-19 17:27:28 +07:00
|
|
|
}
|
2026-07-01 17:08:46 +07:00
|
|
|
},
|
2026-06-19 17:27:28 +07:00
|
|
|
|
2026-07-01 17:08:46 +07:00
|
|
|
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);
|
2026-06-19 17:27:28 +07:00
|
|
|
}
|
2026-07-01 17:08:46 +07:00
|
|
|
},
|
2026-06-19 17:27:28 +07:00
|
|
|
|
2026-07-01 17:08:46 +07:00
|
|
|
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);
|
2026-06-19 17:27:28 +07:00
|
|
|
}
|
2026-07-01 17:08:46 +07:00
|
|
|
},
|
|
|
|
|
};
|
2026-06-19 17:27:28 +07:00
|
|
|
|
2026-07-01 17:08:46 +07:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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);
|
2026-06-19 17:27:28 +07:00
|
|
|
}
|
2026-07-01 17:08:46 +07:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-06-19 17:27:28 +07:00
|
|
|
|
2026-07-01 17:08:46 +07:00
|
|
|
info(...args: unknown[]) {
|
|
|
|
|
const { message } = normalize(args);
|
|
|
|
|
// promote to error so it passes the filter
|
|
|
|
|
if (message) getInspectorLogger().error(message);
|
|
|
|
|
},
|
|
|
|
|
};
|