stop(logging): implement structured JSON logging then halt due to YAGNI

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.
This commit is contained in:
michael 2025-09-05 20:31:27 +02:00
parent ddbb232de2
commit 8fecb0188c
3 changed files with 418 additions and 33 deletions

View file

@ -1,5 +1,5 @@
-- src/http_server.lua
-- HTTP Server Core for Furt API-Gateway
-- HTTP Server Core for Furt API-Gateway with Structured Logging
-- Dragons@Work Digital Sovereignty Project
local socket = require("socket")
@ -10,6 +10,7 @@ end
local config = require("config.server")
local Auth = require("src.auth")
local Logger = require("src.logger")
-- HTTP-Server Module
local FurtServer = {}
@ -32,11 +33,22 @@ function FurtServer:add_route(method, path, handler)
self.routes[method] = {}
end
self.routes[method][path] = handler
Logger.debug("Route registered", {
method = method,
path = path
})
end
-- Add protected route (requires authentication)
function FurtServer:add_protected_route(method, path, required_permission, handler)
self:add_route(method, path, Auth.create_protected_route(required_permission, handler))
Logger.debug("Protected route registered", {
method = method,
path = path,
permission = required_permission
})
end
-- Parse HTTP request
@ -49,6 +61,7 @@ function FurtServer:parse_request(client)
-- Parse request line: "POST /v1/mail/send HTTP/1.1"
local method, path, protocol = request_line:match("(%w+) (%S+) (%S+)")
if not method then
Logger.warn("Invalid request line", { request_line = request_line })
return nil
end
@ -174,22 +187,62 @@ function FurtServer:get_status_text(status)
return status_texts[status] or "Unknown"
end
-- Get client IP address with X-Forwarded-For support
function FurtServer:get_client_ip(client, headers)
-- Check for X-Forwarded-For header (proxy support)
local forwarded_for = headers["x-forwarded-for"]
if forwarded_for then
-- Take first IP in case of multiple proxies
local first_ip = forwarded_for:match("([^,]+)")
if first_ip then
return first_ip:match("^%s*(.-)%s*$") -- trim whitespace
end
end
-- Check for X-Real-IP header
local real_ip = headers["x-real-ip"]
if real_ip then
return real_ip
end
-- Fallback to direct connection
local peer_ip = client:getpeername()
return peer_ip or "unknown"
end
-- Handle client request
function FurtServer:handle_client(client)
-- Generate request ID and start timing
local request_id = Logger.generate_request_id()
local start_time = socket.gettime()
Logger.debug("Request started", {
request_id = request_id
})
local request = self:parse_request(client)
if not request then
local duration_ms = math.floor((socket.gettime() - start_time) * 1000)
local response = self:create_response(400, {error = "Invalid request"}, nil, nil, nil)
client:send(response)
Logger.log_request("INVALID", "unknown", 400, duration_ms, "unknown", request_id)
return
end
print(string.format("[%s] %s %s", os.date("%Y-%m-%d %H:%M:%S"),
request.method, request.path))
-- Get client IP
local client_ip = self:get_client_ip(client, request.headers)
-- Add request_id to request context for handlers
request.request_id = request_id
-- Handle OPTIONS preflight requests (CORS)
if request.method == "OPTIONS" then
local duration_ms = math.floor((socket.gettime() - start_time) * 1000)
local response = self:create_response(204, "", "text/plain", nil, request)
client:send(response)
Logger.log_request("OPTIONS", request.path, 204, duration_ms, client_ip, request_id)
return
end
@ -199,49 +252,78 @@ function FurtServer:handle_client(client)
handler = self.routes[request.method][request.path]
end
local status = 404
if handler then
local success, result = pcall(handler, request, self)
if success then
client:send(result)
-- Extract status from response (rough parsing)
status = tonumber(result:match("HTTP/1%.1 (%d+)")) or 200
else
print("Handler error: " .. tostring(result))
local error_response = self:create_response(500, {error = "Internal server error"}, nil, nil, request)
Logger.log_error("Handler error", {
request_id = request_id,
method = request.method,
path = request.path,
client_ip = client_ip
}, tostring(result))
status = 500
local error_response = self:create_response(500, {
error = "Internal server error",
request_id = request_id
}, nil, nil, request)
client:send(error_response)
end
else
print("Route not found: " .. request.method .. " " .. request.path)
local response = self:create_response(404, {error = "Route not found", method = request.method, path = request.path}, nil, nil, request)
Logger.debug("Route not found", {
request_id = request_id,
method = request.method,
path = request.path
})
local response = self:create_response(404, {
error = "Route not found",
method = request.method,
path = request.path,
request_id = request_id
}, nil, nil, request)
client:send(response)
end
-- Log completed request with performance metrics
local duration_ms = math.floor((socket.gettime() - start_time) * 1000)
Logger.log_request(request.method, request.path, status, duration_ms, client_ip, request_id)
end
-- Start HTTP server
function FurtServer:start()
self.server = socket.bind(self.host, self.port)
if not self.server then
Logger.error("Failed to bind server", {
host = self.host,
port = self.port
})
error("Failed to bind to " .. self.host .. ":" .. self.port)
end
local HealthRoute = require("src.routes.health")
local version_info = HealthRoute.get_version_info()
print(string.format("Furt HTTP-Server started on %s:%d", self.host, self.port))
print("Version: " .. version_info.version .. " (merkwerk)")
print("Content-Hash: " .. (version_info.content_hash or "unknown"))
print("VCS: " .. (version_info.vcs_info and version_info.vcs_info.hash or "none"))
print("API-Key authentication: ENABLED")
-- Structured startup logging
Logger.log_startup(self.host, self.port, version_info)
-- Show actual configured rate limits
-- Log configuration details
local rate_limits = config.security and config.security.rate_limits
if rate_limits then
print(string.format("Rate limiting: ENABLED (%d req/hour per API key, %d req/hour per IP)",
rate_limits.api_key_max, rate_limits.ip_max))
else
print("Rate limiting: ENABLED (default values)")
end
Logger.log_config_summary({
cors_origins_count = #config.cors.allowed_origins,
rate_limiting_enabled = rate_limits ~= nil,
api_key_max = rate_limits and rate_limits.api_key_max,
ip_max = rate_limits and rate_limits.ip_max,
test_endpoint_enabled = config.security and config.security.enable_test_endpoint,
log_level = Logger.get_log_level()
})
print("CORS enabled for " .. (#config.cors.allowed_origins) .. " configured origins")
print("Press Ctrl+C to stop")
Logger.info("Furt server ready - Press Ctrl+C to stop")
while true do
local client = self.server:accept()