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:36:21 +07:00
|
|
|
import { configureSync, getConfig, getConsoleSink, withFilter, getLogger } from '@logtape/logtape';
|
2026-07-02 16:54:25 +07:00
|
|
|
import type { LogLevel } from '@logtape/logtape';
|
2026-07-01 17:08:46 +07:00
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
2026-07-01 17:36:21 +07:00
|
|
|
// LogTape configuration (idempotent — uses getConfig() not a local flag)
|
2026-07-01 17:08:46 +07:00
|
|
|
// ---------------------------------------------------------------------------
|
2026-06-19 17:27:28 +07:00
|
|
|
|
2026-07-02 16:54:25 +07:00
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-01 17:08:46 +07:00
|
|
|
function ensureConfigured() {
|
2026-07-01 17:36:21 +07:00
|
|
|
// 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;
|
2026-07-01 17:08:46 +07:00
|
|
|
configureSync({
|
|
|
|
|
sinks: {
|
|
|
|
|
console: getConsoleSink(),
|
|
|
|
|
errorsOnly: withFilter(getConsoleSink(), 'error'),
|
|
|
|
|
},
|
|
|
|
|
loggers: [
|
|
|
|
|
{
|
|
|
|
|
category: ['supra-app'],
|
|
|
|
|
sinks: ['console'],
|
2026-07-02 16:54:25 +07:00
|
|
|
lowestLevel: resolveLogLevel(),
|
2026-07-01 17:08:46 +07:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
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);
|
|
|
|
|
},
|
|
|
|
|
};
|