382 lines
9.6 KiB
Lua
382 lines
9.6 KiB
Lua
|
-- 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
|