# 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. ## logger.nim ## Logging system for NIP MVP import std/[times, strformat, os, strutils] type LogLevel* = enum Debug, Info, Warning, Error, Fatal Logger* = ref object logFile*: string minLevel*: LogLevel enabled*: bool verbose*: bool var globalLogger*: Logger = nil proc newLogger*(logFile: string = "/var/log/nip.log", minLevel: LogLevel = Info, verbose: bool = false): Logger = ## Create a new logger result = Logger( logFile: logFile, minLevel: minLevel, enabled: true, verbose: verbose ) proc initGlobalLogger*(logFile: string = "/var/log/nip.log", minLevel: LogLevel = Info, verbose: bool = false) = ## Initialize the global logger globalLogger = newLogger(logFile, minLevel, verbose) proc levelToString(level: LogLevel): string = case level of Debug: "DEBUG" of Info: "INFO" of Warning: "WARN" of Error: "ERROR" of Fatal: "FATAL" proc log*(logger: Logger, level: LogLevel, message: string) = ## Log a message if not logger.enabled: return if level < logger.minLevel: return let timestamp = now().format("yyyy-MM-dd HH:mm:ss") let levelStr = levelToString(level) let logLine = fmt"[{timestamp}] [{levelStr}] {message}" # Print to console if verbose or error/fatal if logger.verbose or level >= Error: echo logLine # Write to log file try: let logDir = parentDir(logger.logFile) if not dirExists(logDir): try: createDir(logDir) except: # Can't create log directory, skip file logging return let file = open(logger.logFile, fmAppend) file.writeLine(logLine) file.close() except IOError, OSError: # Can't write to log file, just skip it discard # Convenience functions for global logger proc logDebug*(message: string) = if globalLogger != nil: globalLogger.log(Debug, message) proc logInfo*(message: string) = if globalLogger != nil: globalLogger.log(Info, message) proc logWarning*(message: string) = if globalLogger != nil: globalLogger.log(Warning, message) proc logError*(message: string) = if globalLogger != nil: globalLogger.log(Error, message) proc logFatal*(message: string) = if globalLogger != nil: globalLogger.log(Fatal, message) # Operation logging helpers proc logOperation*(operation: string, details: string = "") = let msg = if details != "": fmt"{operation}: {details}" else: operation logInfo(msg) proc logSuccess*(operation: string, details: string = "") = let msg = if details != "": fmt"✅ {operation}: {details}" else: fmt"✅ {operation}" logInfo(msg) proc logFailure*(operation: string, error: string) = logError(fmt"❌ {operation} failed: {error}") proc logException*(operation: string, e: ref Exception) = logError(fmt"Exception in {operation}: {e.msg}") if globalLogger != nil and globalLogger.verbose: logError(e.getStackTrace()) # Log rotation proc rotateLog*(logger: Logger, maxSize: int64 = 10_000_000) = ## Rotate log file if it exceeds maxSize (default 10MB) if not fileExists(logger.logFile): return try: let size = getFileSize(logger.logFile) if size > maxSize: # Rotate: nip.log -> nip.log.1, nip.log.1 -> nip.log.2, etc. for i in countdown(2, 1): let oldFile = logger.logFile & "." & $i let newFile = logger.logFile & "." & $(i + 1) if fileExists(oldFile): moveFile(oldFile, newFile) # Move current log to .1 moveFile(logger.logFile, logger.logFile & ".1") logInfo("Log file rotated") except: discard proc getLogPath*(): string = ## Get the current log file path if globalLogger != nil: return globalLogger.logFile else: return "/var/log/nip.log" proc setVerbose*(verbose: bool) = ## Set verbose mode for global logger if globalLogger != nil: globalLogger.verbose = verbose