# SPDX-License-Identifier: LSL-1.0 # Copyright (c) 2026 Markus Maiwald # Stewardship: Self Sovereign Society Foundation # # This file is part of the Nexus Sovereign Core. # See legal/LICENSE_SOVEREIGN.md for license terms. ## NimPak Structured Logging ## ## Comprehensive logging system for the NimPak package manager. ## Provides structured logging with multiple output formats and levels. ## Task 38: Add comprehensive logging. import std/[times, json, strformat, strutils, os, terminal, tables, locks] type LogLevel* = enum Trace = 0, ## Detailed tracing (development only) Debug = 1, ## Debug information Info = 2, ## General information Warn = 3, ## Warnings (potential issues) Error = 4, ## Errors (operation failed) Fatal = 5, ## Fatal errors (application cannot continue) Audit = 6 ## Audit log (security-relevant events) LogOutput* = enum Console, ## Console (stdout/stderr) File, ## Log file Json, ## JSON structured log Syslog ## System log (future) LogEntry* = object timestamp*: DateTime level*: LogLevel component*: string message*: string context*: Table[string, string] duration*: float ## Operation duration in ms (if applicable) Logger* = object minLevel*: LogLevel outputs*: set[LogOutput] logFile*: string jsonFile*: string useColors*: bool component*: string fileHandle: File jsonHandle: File lock: Lock const LevelColors: array[LogLevel, ForegroundColor] = [ fgWhite, # Trace fgCyan, # Debug fgGreen, # Info fgYellow, # Warn fgRed, # Error fgMagenta, # Fatal fgBlue # Audit ] LevelNames: array[LogLevel, string] = [ "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL", "AUDIT" ] var globalLogger: Logger loggerInitialized = false # ############################################################################ # Logger Initialization # ############################################################################ proc initLogger*(component: string = "nip", minLevel: LogLevel = Info, outputs: set[LogOutput] = {Console}, logFile: string = "", jsonFile: string = "", useColors: bool = true): Logger = ## Initialize a new logger result = Logger( minLevel: minLevel, outputs: outputs, logFile: logFile, jsonFile: jsonFile, useColors: useColors and isatty(stdout), component: component ) initLock(result.lock) if logFile.len > 0 and File in outputs: createDir(logFile.parentDir) result.fileHandle = open(logFile, fmAppend) if jsonFile.len > 0 and Json in outputs: createDir(jsonFile.parentDir) result.jsonHandle = open(jsonFile, fmAppend) proc initGlobalLogger*(component: string = "nip", minLevel: LogLevel = Info, outputs: set[LogOutput] = {Console}, logFile: string = "", jsonFile: string = "") = ## Initialize the global logger globalLogger = initLogger(component, minLevel, outputs, logFile, jsonFile) loggerInitialized = true proc closeLogger*(logger: var Logger) = ## Close logger file handles if logger.logFile.len > 0 and Console in logger.outputs: logger.fileHandle.close() if logger.jsonFile.len > 0 and Json in logger.outputs: logger.jsonHandle.close() deinitLock(logger.lock) # ############################################################################ # Log Formatting # ############################################################################ proc formatConsole(entry: LogEntry, useColors: bool): string = ## Format log entry for console output let timeStr = entry.timestamp.format("HH:mm:ss") let levelStr = LevelNames[entry.level] if useColors: let colorCode = ord(LevelColors[entry.level]) result = fmt"[\e[90m{timeStr}\e[0m] \e[{colorCode}m{levelStr:5}\e[0m [{entry.component}] {entry.message}" else: result = fmt"[{timeStr}] {levelStr:5} [{entry.component}] {entry.message}" # Add context if present if entry.context.len > 0: result.add " {" var first = true for key, value in entry.context: if not first: result.add ", " result.add fmt"{key}={value}" first = false result.add "}" # Add duration if present if entry.duration > 0: result.add fmt" ({entry.duration:.2f}ms)" proc formatJson(entry: LogEntry): string = ## Format log entry as JSON let obj = %*{ "timestamp": entry.timestamp.format("yyyy-MM-dd'T'HH:mm:ss'Z'"), "level": $entry.level, "component": entry.component, "message": entry.message } if entry.context.len > 0: var ctx = newJObject() for key, value in entry.context: ctx[key] = %value obj["context"] = ctx if entry.duration > 0: obj["duration_ms"] = %entry.duration result = $obj proc formatFile(entry: LogEntry): string = ## Format log entry for file output (plain text with full timestamp) let timeStr = entry.timestamp.format("yyyy-MM-dd HH:mm:ss") let levelStr = LevelNames[entry.level] result = fmt"[{timeStr}] {levelStr:5} [{entry.component}] {entry.message}" if entry.context.len > 0: for key, value in entry.context: result.add fmt"\n {key}: {value}" # ############################################################################ # Logging Functions # ############################################################################ proc log*(logger: var Logger, level: LogLevel, message: string, context: Table[string, string] = initTable[string, string](), duration: float = 0.0) = ## Log a message with the given level if level < logger.minLevel: return let entry = LogEntry( timestamp: now(), level: level, component: logger.component, message: message, context: context, duration: duration ) acquire(logger.lock) defer: release(logger.lock) if Console in logger.outputs: let formatted = formatConsole(entry, logger.useColors) if level >= Error: stderr.writeLine(formatted) else: stdout.writeLine(formatted) if File in logger.outputs and logger.logFile.len > 0: logger.fileHandle.writeLine(formatFile(entry)) logger.fileHandle.flushFile() if Json in logger.outputs and logger.jsonFile.len > 0: logger.jsonHandle.writeLine(formatJson(entry)) logger.jsonHandle.flushFile() # Convenience functions for global logger proc log*(level: LogLevel, message: string, context: Table[string, string] = initTable[string, string]()) = if loggerInitialized: globalLogger.log(level, message, context) proc trace*(message: string, context: Table[string, string] = initTable[string, string]()) = log(Trace, message, context) proc debug*(message: string, context: Table[string, string] = initTable[string, string]()) = log(Debug, message, context) proc info*(message: string, context: Table[string, string] = initTable[string, string]()) = log(Info, message, context) proc warn*(message: string, context: Table[string, string] = initTable[string, string]()) = log(Warn, message, context) proc error*(message: string, context: Table[string, string] = initTable[string, string]()) = log(Error, message, context) proc fatal*(message: string, context: Table[string, string] = initTable[string, string]()) = log(Fatal, message, context) proc audit*(message: string, context: Table[string, string] = initTable[string, string]()) = log(Audit, message, context) # ############################################################################ # Performance Metrics Logging # ############################################################################ type Timer* = object startTime: float name: string logger: ptr Logger proc startTimer*(logger: var Logger, name: string): Timer = ## Start a performance timer result = Timer( startTime: epochTime(), name: name, logger: addr logger ) proc stop*(timer: Timer): float = ## Stop timer and log the duration result = (epochTime() - timer.startTime) * 1000.0 # Convert to ms var ctx = initTable[string, string]() ctx["operation"] = timer.name timer.logger[].log(Debug, fmt"Operation '{timer.name}' completed", ctx, result) template timed*(logger: var Logger, name: string, body: untyped) = ## Execute body and log duration let timer = logger.startTimer(name) body discard timer.stop() # ############################################################################ # Audit Logging # ############################################################################ proc auditEvent*(logger: var Logger, event: string, user: string = "", resource: string = "", action: string = "", success: bool = true) = ## Log an audit event var ctx = initTable[string, string]() if user.len > 0: ctx["user"] = user if resource.len > 0: ctx["resource"] = resource if action.len > 0: ctx["action"] = action ctx["success"] = $success logger.log(Audit, event, ctx) proc auditPackageOp*(logger: var Logger, operation: string, packageName: string, version: string = "", success: bool = true) = ## Log a package operation audit event var ctx = initTable[string, string]() ctx["package"] = packageName if version.len > 0: ctx["version"] = version ctx["operation"] = operation ctx["success"] = $success logger.log(Audit, fmt"Package {operation}: {packageName}", ctx) proc auditCasOp*(logger: var Logger, operation: string, hash: string, format: string = "", success: bool = true) = ## Log a CAS operation audit event var ctx = initTable[string, string]() ctx["hash"] = hash[0..min(20, hash.len-1)] if format.len > 0: ctx["format"] = format ctx["operation"] = operation ctx["success"] = $success logger.log(Audit, fmt"CAS {operation}", ctx) # ############################################################################ # Log Level Parsing # ############################################################################ proc parseLogLevel*(s: string): LogLevel = ## Parse log level from string case s.toLowerAscii(): of "trace": Trace of "debug": Debug of "info": Info of "warn", "warning": Warn of "error": Error of "fatal": Fatal of "audit": Audit else: Info proc setLogLevel*(logger: var Logger, level: LogLevel) = logger.minLevel = level proc setLogLevel*(level: LogLevel) = if loggerInitialized: globalLogger.minLevel = level