Add comprehensive structured logging system, then stop development: - Create Logger module with JSON output and configurable log levels - Implement hash-based request ID generation for request tracing - Add performance timing and client IP detection to HTTP server - Enhance startup logging with module loading and configuration checks STOPPED: Feature violates Low-Tech principles - 200+ lines logging vs 100 lines business logic (code bloat) - JSON serialization overhead reduces performance - No current production need for structured monitoring - Simple print() statements sufficient for current scale Branch parked for future consideration when monitoring requirements actually emerge. Issue #54 deferred to v0.2.x milestone.
200 lines
5 KiB
Lua
200 lines
5 KiB
Lua
-- src/logger.lua
|
|
-- Structured JSON Logger for Furt API-Gateway
|
|
-- Dragons@Work Digital Sovereignty Project
|
|
|
|
local found_cjson, cjson = pcall(require, 'cjson')
|
|
if not found_cjson then
|
|
cjson = require('dkjson')
|
|
end
|
|
|
|
local config = require("config.server")
|
|
|
|
-- Hash-based request ID generator for collision resistance
|
|
local function generate_request_id()
|
|
local data = string.format("%d-%d-%d-%d",
|
|
os.time(),
|
|
math.random(1000000, 9999999),
|
|
math.random(1000000, 9999999),
|
|
os.clock() * 1000000)
|
|
|
|
-- Simple hash function (Lua-native, no dependencies)
|
|
local hash = 0
|
|
for i = 1, #data do
|
|
hash = (hash * 31 + string.byte(data, i)) % 2147483647
|
|
end
|
|
|
|
return string.format("req-%x", hash)
|
|
end
|
|
|
|
-- Export request ID generator
|
|
Logger = {}
|
|
Logger.generate_request_id = generate_request_id
|
|
|
|
local Logger = {}
|
|
|
|
-- Log levels with numeric values for filtering
|
|
local LOG_LEVELS = {
|
|
debug = 1,
|
|
info = 2,
|
|
warn = 3,
|
|
error = 4
|
|
}
|
|
|
|
-- Current log level from config
|
|
local current_log_level = LOG_LEVELS[config.log_level] or LOG_LEVELS.info
|
|
|
|
-- Service identification
|
|
local SERVICE_NAME = "furt-lua"
|
|
|
|
-- Generate timestamp in ISO format
|
|
local function get_timestamp()
|
|
return os.date("!%Y-%m-%dT%H:%M:%SZ")
|
|
end
|
|
|
|
-- Core logging function
|
|
local function log_structured(level, message, context)
|
|
-- Skip if log level is below threshold
|
|
if LOG_LEVELS[level] < current_log_level then
|
|
return
|
|
end
|
|
|
|
-- Build log entry
|
|
local log_entry = {
|
|
timestamp = get_timestamp(),
|
|
level = level,
|
|
service = SERVICE_NAME,
|
|
message = message
|
|
}
|
|
|
|
-- Add context data if provided
|
|
if context then
|
|
for key, value in pairs(context) do
|
|
log_entry[key] = value
|
|
end
|
|
end
|
|
|
|
-- Output as JSON
|
|
local json_output = cjson.encode(log_entry)
|
|
print(json_output)
|
|
end
|
|
|
|
-- Public logging functions
|
|
function Logger.debug(message, context)
|
|
log_structured("debug", message, context)
|
|
end
|
|
|
|
function Logger.info(message, context)
|
|
log_structured("info", message, context)
|
|
end
|
|
|
|
function Logger.warn(message, context)
|
|
log_structured("warn", message, context)
|
|
end
|
|
|
|
function Logger.error(message, context)
|
|
log_structured("error", message, context)
|
|
end
|
|
|
|
-- Request logging with performance metrics
|
|
function Logger.log_request(method, path, status, duration_ms, client_ip, request_id)
|
|
if not config.log_requests then
|
|
return
|
|
end
|
|
|
|
local context = {
|
|
method = method,
|
|
path = path,
|
|
status = status,
|
|
duration_ms = duration_ms,
|
|
client_ip = client_ip
|
|
}
|
|
|
|
-- Add request_id if provided
|
|
if request_id then
|
|
context.request_id = request_id
|
|
end
|
|
|
|
log_structured("info", "HTTP request", context)
|
|
end
|
|
|
|
-- Service startup logging
|
|
function Logger.log_startup(host, port, version_info)
|
|
Logger.info("Furt HTTP-Server starting", {
|
|
host = host,
|
|
port = port,
|
|
version = version_info.version,
|
|
content_hash = version_info.content_hash,
|
|
vcs_hash = version_info.vcs_info and version_info.vcs_info.hash
|
|
})
|
|
end
|
|
|
|
-- Service health logging
|
|
function Logger.log_health_check(status, details)
|
|
local level = status == "healthy" and "info" or "warn"
|
|
log_structured(level, "Health check", {
|
|
health_status = status,
|
|
details = details
|
|
})
|
|
end
|
|
|
|
-- Error logging with stack trace support
|
|
function Logger.log_error(error_message, context, stack_trace)
|
|
local error_context = context or {}
|
|
if stack_trace then
|
|
error_context.stack_trace = stack_trace
|
|
end
|
|
log_structured("error", error_message, error_context)
|
|
end
|
|
|
|
-- Configuration logging
|
|
function Logger.log_config_summary(summary)
|
|
Logger.info("Configuration loaded", summary)
|
|
end
|
|
|
|
-- Rate limiting events
|
|
function Logger.log_rate_limit(api_key, client_ip, limit_type)
|
|
Logger.warn("Rate limit exceeded", {
|
|
api_key = api_key and "***masked***" or nil,
|
|
client_ip = client_ip,
|
|
limit_type = limit_type
|
|
})
|
|
end
|
|
|
|
-- SMTP/Mail logging
|
|
function Logger.log_mail_event(event_type, recipient, success, error_message)
|
|
local level = success and "info" or "error"
|
|
log_structured(level, "Mail event", {
|
|
event_type = event_type,
|
|
recipient = recipient,
|
|
success = success,
|
|
error = error_message
|
|
})
|
|
end
|
|
|
|
-- Set log level dynamically (useful for debugging)
|
|
function Logger.set_log_level(level)
|
|
if LOG_LEVELS[level] then
|
|
current_log_level = LOG_LEVELS[level]
|
|
Logger.info("Log level changed", { new_level = level })
|
|
else
|
|
Logger.error("Invalid log level", { attempted_level = level })
|
|
end
|
|
end
|
|
|
|
-- Get current log level
|
|
function Logger.get_log_level()
|
|
for level, value in pairs(LOG_LEVELS) do
|
|
if value == current_log_level then
|
|
return level
|
|
end
|
|
end
|
|
return "unknown"
|
|
end
|
|
|
|
-- Check if a log level would be output
|
|
function Logger.would_log(level)
|
|
return LOG_LEVELS[level] and LOG_LEVELS[level] >= current_log_level
|
|
end
|
|
|
|
return Logger
|
|
|