-- Centralized logging and error handling system local M = {} -- Log levels local LOG_LEVELS = { TRACE = 1, DEBUG = 2, INFO = 3, WARN = 4, ERROR = 5, FATAL = 6 } -- Current log level (can be configured) local current_log_level = LOG_LEVELS.INFO -- Log file configuration local log_config = { file_enabled = true, file_path = nil, -- Will be set to stdpath('data')/notex/notex.log max_file_size = 1024 * 1024, -- 1MB backup_count = 3, console_enabled = true } -- Error categories for better handling local ERROR_CATEGORIES = { DATABASE = "database", PARSING = "parsing", QUERY = "query", UI = "ui", FILESYSTEM = "filesystem", VALIDATION = "validation", CONFIGURATION = "configuration", NETWORK = "network", PERFORMANCE = "performance" } -- Error context stack for nested operations local error_context = {} -- Performance tracking local performance_metrics = { query_times = {}, index_times = {}, operation_counts = {}, error_counts = {} } -- Initialize logging system function M.init(config) config = config or {} -- Set log level if config.log_level then local level = LOG_LEVELS[config.log_level:upper()] if level then current_log_level = level end end -- Configure logging log_config = vim.tbl_deep_extend("force", log_config, config) -- Set default log file path if not log_config.file_path then log_config.file_path = vim.fn.stdpath('data') .. '/notex/notex.log' end -- Ensure log directory exists local log_dir = vim.fn.fnamemodify(log_config.file_path, ':h') vim.fn.mkdir(log_dir, 'p') -- Clean up old log files M.cleanup_log_files() M.log("INFO", "Logging system initialized", { log_level = M.get_log_level_name(), log_file = log_config.file_path }) return true, "Logging system initialized" end -- Core logging function function M.log(level, message, context) level = level:upper() local level_value = LOG_LEVELS[level] or LOG_LEVELS.INFO -- Skip if below current log level if level_value < current_log_level then return end local timestamp = os.date("%Y-%m-%d %H:%M:%S") local context_str = context and " | " .. vim.inspect(context) or "" local log_entry = string.format("[%s] %s: %s%s", timestamp, level, message, context_str) -- Console output if log_config.console_enabled then if level_value >= LOG_LEVELS.ERROR then vim.notify(message, vim.log.levels.ERROR) elseif level_value >= LOG_LEVELS.WARN then vim.notify(message, vim.log.levels.WARN) elseif level_value >= LOG_LEVELS.INFO then vim.notify(message, vim.log.levels.INFO) else -- Debug/trace go to message history but not notifications vim.schedule(function() vim.cmd('echomsg "' .. message:gsub('"', '\\"') .. '"') end) end end -- File output if log_config.file_enabled then M.write_to_file(log_entry) end end -- Write to log file with rotation function M.write_to_file(log_entry) -- Check file size and rotate if necessary local file_info = vim.fn.getfsize(log_config.file_path) if file_info > log_config.max_file_size then M.rotate_log_file() end -- Append to log file local file = io.open(log_config.file_path, "a") if file then file:write(log_entry .. "\n") file:close() end end -- Rotate log files function M.rotate_log_file() -- Remove oldest backup local oldest_backup = log_config.file_path .. "." .. log_config.backup_count if vim.fn.filereadable(oldest_backup) > 0 then os.remove(oldest_backup) end -- Rotate existing backups for i = log_config.backup_count - 1, 1, -1 do local current_backup = log_config.file_path .. "." .. i local next_backup = log_config.file_path .. "." .. (i + 1) if vim.fn.filereadable(current_backup) > 0 then os.rename(current_backup, next_backup) end end -- Move current log to backup if vim.fn.filereadable(log_config.file_path) > 0 then os.rename(log_config.file_path, log_config.file_path .. ".1") end end -- Clean up old log files function M.cleanup_log_files() for i = 1, log_config.backup_count do local backup_file = log_config.file_path .. "." .. i if vim.fn.filereadable(backup_file) > 0 then local file_time = vim.fn.getftime(backup_file) local age_days = (os.time() - file_time) / 86400 -- Remove backups older than 30 days if age_days > 30 then os.remove(backup_file) end end end end -- Convenience logging functions function M.trace(message, context) M.log("TRACE", message, context) end function M.debug(message, context) M.log("DEBUG", message, context) end function M.info(message, context) M.log("INFO", message, context) end function M.warn(message, context) M.log("WARN", message, context) end function M.error(message, context) M.log("ERROR", message, context) -- Track error metrics local category = context and context.category or "unknown" performance_metrics.error_counts[category] = (performance_metrics.error_counts[category] or 0) + 1 end function M.fatal(message, context) M.log("FATAL", message, context) -- Fatal errors should also be shown as error notifications vim.notify("FATAL: " .. message, vim.log.levels.ERROR) end -- Error handling with context function M.handle_error(error_msg, category, context) category = category or "unknown" context = context or {} local error_info = { message = error_msg, category = category, context = context, timestamp = os.time(), stack_trace = debug.traceback() } -- Add current error context if #error_context > 0 then error_info.nested_context = vim.deepcopy(error_context) end M.error("Error in " .. category, error_info) return error_info end -- Push error context (for nested operations) function M.push_context(operation, context) table.insert(error_context, { operation = operation, context = context or {}, timestamp = os.time() }) end -- Pop error context function M.pop_context() return table.remove(error_context) end -- Execute with error context function M.with_context(operation, context, func, ...) M.push_context(operation, context) local results = {pcall(func, ...)} local success = table.remove(results, 1) M.pop_context() if not success then local error_msg = results[1] M.handle_error(error_msg, context.category or "operation", context) return nil, error_msg end return unpack(results) end -- Performance tracking function M.start_timer(operation) return { operation = operation, start_time = vim.loop.hrtime() } end function M.end_timer(timer) local end_time = vim.loop.hrtime() local duration_ms = (end_time - timer.start_time) / 1000000 -- Store performance metrics if timer.operation:match("query") then table.insert(performance_metrics.query_times, duration_ms) -- Keep only last 100 measurements if #performance_metrics.query_times > 100 then table.remove(performance_metrics.query_times, 1) end elseif timer.operation:match("index") then table.insert(performance_metrics.index_times, duration_ms) if #performance_metrics.index_times > 100 then table.remove(performance_metrics.index_times, 1) end end -- Track operation counts performance_metrics.operation_counts[timer.operation] = (performance_metrics.operation_counts[timer.operation] or 0) + 1 M.debug("Operation completed", { operation = timer.operation, duration_ms = duration_ms }) return duration_ms end -- Timer utility function function M.timer(operation) return function() return M.end_timer(M.start_timer(operation)) end end -- Get performance statistics function M.get_performance_stats() local stats = { operations = vim.deepcopy(performance_metrics.operation_counts), errors = vim.deepcopy(performance_metrics.error_counts) } -- Calculate averages if #performance_metrics.query_times > 0 then local total = 0 for _, time in ipairs(performance_metrics.query_times) do total = total + time end stats.average_query_time = total / #performance_metrics.query_times stats.query_count = #performance_metrics.query_times end if #performance_metrics.index_times > 0 then local total = 0 for _, time in ipairs(performance_metrics.index_times) do total = total + time end stats.average_index_time = total / #performance_metrics.index_times stats.index_count = #performance_metrics.index_times end return stats end -- Get log level name function M.get_log_level_name() for name, level in pairs(LOG_LEVELS) do if level == current_log_level then return name end end return "UNKNOWN" end -- Set log level function M.set_log_level(level) local level_value = LOG_LEVELS[level:upper()] if level_value then current_log_level = level_value M.info("Log level changed to " .. level:upper()) return true end return false, "Invalid log level: " .. tostring(level) end -- Configuration validation function M.validate_config(config) local errors = {} -- Validate log level if config.log_level and not LOG_LEVELS[config.log_level:upper()] then table.insert(errors, "Invalid log level: " .. config.log_level) end -- Validate file size if config.max_file_size and (type(config.max_file_size) ~= "number" or config.max_file_size <= 0) then table.insert(errors, "max_file_size must be a positive number") end -- Validate backup count if config.backup_count and (type(config.backup_count) ~= "number" or config.backup_count < 1) then table.insert(errors, "backup_count must be a positive number") end return #errors == 0, errors end -- Export error categories for use in other modules M.ERROR_CATEGORIES = ERROR_CATEGORIES return M