Files
IoT_PreTester_V46_Demo/frontend/src/lib/logger.ts
T

93 lines
3.5 KiB
TypeScript

// Browser-side logger with levels, timestamps and a ring buffer.
//
// Level: NEXT_PUBLIC_LOG_LEVEL at build time, overridable per browser with
// localStorage.setItem("iotp_log_level", "debug"). The last 300 entries are
// kept in window.__iotpLogs and can be copied with window.__iotpDumpLogs().
export type LogLevel = "debug" | "info" | "warn" | "error"
const ORDER: Record<LogLevel, number> = { debug: 10, info: 20, warn: 30, error: 40 }
const BUFFER_SIZE = 300
interface LogEntry {
ts: string
level: LogLevel
scope: string
message: string
data?: unknown
}
declare global {
interface Window {
__iotpLogs?: LogEntry[]
__iotpDumpLogs?: () => string
}
}
function configuredLevel(): LogLevel {
let level = (process.env.NEXT_PUBLIC_LOG_LEVEL || (process.env.NODE_ENV === "production" ? "info" : "debug")) as LogLevel
if (typeof window !== "undefined") {
try {
const override = window.localStorage.getItem("iotp_log_level") as LogLevel | null
if (override && override in ORDER) level = override
} catch { /* storage may be unavailable */ }
}
return level in ORDER ? level : "info"
}
function buffer(): LogEntry[] {
if (typeof window === "undefined") return []
if (!window.__iotpLogs) {
window.__iotpLogs = []
window.__iotpDumpLogs = () =>
(window.__iotpLogs || [])
.map((e) => `${e.ts} ${e.level.toUpperCase().padEnd(5)} [${e.scope}] ${e.message}${e.data !== undefined ? " " + safeJson(e.data) : ""}`)
.join("\n")
}
return window.__iotpLogs
}
function safeJson(v: unknown): string {
try { return JSON.stringify(v) } catch { return String(v) }
}
function emit(level: LogLevel, scope: string, message: string, data?: unknown) {
if (ORDER[level] < ORDER[configuredLevel()]) return
const ts = new Date().toISOString()
const buf = buffer()
buf.push({ ts, level, scope, message, data })
if (buf.length > BUFFER_SIZE) buf.splice(0, buf.length - BUFFER_SIZE)
const line = `[iotp ${ts.slice(11, 23)}] [${scope}] ${message}`
const fn = level === "error" ? console.error : level === "warn" ? console.warn : level === "info" ? console.info : console.debug
if (data !== undefined) fn(line, data)
else fn(line)
}
export function getLogger(scope: string) {
return {
debug: (message: string, data?: unknown) => emit("debug", scope, message, data),
info: (message: string, data?: unknown) => emit("info", scope, message, data),
warn: (message: string, data?: unknown) => emit("warn", scope, message, data),
error: (message: string, data?: unknown) => emit("error", scope, message, data),
}
}
// Describe any thrown value for logs and user messages.
export function describeError(err: unknown): string {
if (err instanceof Error) return `${err.name}: ${err.message}`
if (typeof err === "string") return err
return safeJson(err)
}
// Global handlers so uncaught browser errors land in the buffer too.
export function installGlobalErrorLogging() {
if (typeof window === "undefined") return
const w = window as Window & { __iotpGlobalInstalled?: boolean }
if (w.__iotpGlobalInstalled) return
w.__iotpGlobalInstalled = true
const log = getLogger("window")
window.addEventListener("error", (ev) => log.error(`Uncaught: ${ev.message}`, { file: ev.filename, line: ev.lineno }))
window.addEventListener("unhandledrejection", (ev) => log.error(`Unhandled rejection: ${describeError(ev.reason)}`))
log.debug(`Logging active at level "${configuredLevel()}"; call window.__iotpDumpLogs() to copy the buffer`)
}