diff --git a/conf/sqlite_serve.conf b/conf/sqlite_serve.conf index c42bd2e..c992411 100644 --- a/conf/sqlite_serve.conf +++ b/conf/sqlite_serve.conf @@ -8,7 +8,7 @@ events { worker_connections 1024; } -error_log logs/error.log warn; +error_log logs/error.log debug; http { # Global templates for shared components diff --git a/src/handler_types.rs b/src/handler_types.rs index b77c10c..e7d2a2d 100644 --- a/src/handler_types.rs +++ b/src/handler_types.rs @@ -3,12 +3,12 @@ use crate::adapters::{HandlebarsAdapter, NginxVariableResolver, SqliteQueryExecutor}; use crate::config::{MainConfig, ModuleConfig}; use crate::domain::{RequestProcessor, ValidatedConfig}; +use crate::logging; use crate::nginx_helpers::{get_doc_root_and_uri, send_response}; use crate::parsing; use crate::{domain, Module}; use ngx::core::Status; use ngx::http::{HttpModuleLocationConf, HttpModuleMainConf}; -use ngx::ngx_log_debug_http; /// Proof that we have valid configuration (Ghost of Departed Proofs) pub struct ValidConfigToken<'a> { @@ -35,42 +35,65 @@ pub fn process_request( request: &mut ngx::http::Request, config: ValidConfigToken, ) -> Status { + logging::log( + request, + logging::LogLevel::Debug, + "handler", + &format!("Processing request for {}", request.unparsed_uri().to_str().unwrap_or("unknown")), + ); + // Parse config into validated types - // If this fails, it's a programming error (config should be valid per token) let validated_config = match parsing::parse_config(config.get()) { Ok(c) => c, Err(e) => { - ngx_log_debug_http!(request, "unexpected parse error: {}", e); + logging::log_config_error(request, "configuration", "", &e); return ngx::http::HTTPStatus::INTERNAL_SERVER_ERROR.into(); } }; - // Get nginx paths - can fail due to nginx state, not our logic + // Get nginx paths let (doc_root, uri) = match get_doc_root_and_uri(request) { Ok(paths) => paths, Err(e) => { - ngx_log_debug_http!(request, "nginx path error: {}", e); + logging::log(request, logging::LogLevel::Error, "nginx", &format!("Path resolution failed: {}", e)); return ngx::http::HTTPStatus::INTERNAL_SERVER_ERROR.into(); } }; - // Pure function - cannot fail + // Resolve template path (pure function - cannot fail) let resolved_template = domain::resolve_template_path(&doc_root, &uri, &validated_config.template_path); - // Resolve parameters - can fail if nginx variable doesn't exist + logging::log( + request, + logging::LogLevel::Debug, + "template", + &format!("Resolved template: {}", resolved_template.full_path()), + ); + + // Resolve parameters let var_resolver = NginxVariableResolver::new(request); let resolved_params = match domain::resolve_parameters(&validated_config.parameters, &var_resolver) { - Ok(params) => params, + Ok(params) => { + if !params.is_empty() { + logging::log( + request, + logging::LogLevel::Debug, + "params", + &format!("Resolved {} parameters", params.len()), + ); + } + params + } Err(e) => { - ngx_log_debug_http!(request, "parameter error: {}", e); + logging::log_param_error(request, "variable", &e); return ngx::http::HTTPStatus::BAD_REQUEST.into(); } }; - // Create processor and run - uses dependency injection + // Execute and render let html = execute_with_processor(&validated_config, &resolved_template, &resolved_params, request); - // Send response - proven correct by types + // Send response send_response(request, &html) } @@ -93,20 +116,67 @@ fn execute_with_processor( let main_conf = Module::main_conf(request).expect("main config is none"); let global_dir = if !main_conf.global_templates_dir.is_empty() { + logging::log_template_loading( + request, + "global", + 0, + &main_conf.global_templates_dir, + ); Some(main_conf.global_templates_dir.as_str()) } else { None }; - // If processor fails, return error HTML instead of panicking - processor - .process(config, resolved_template, resolved_params, global_dir) - .unwrap_or_else(|e| { + // Process through functional core + match processor.process(config, resolved_template, resolved_params, global_dir) { + Ok(html) => { + // Count results for logging (parse HTML or trust it worked) + logging::log( + request, + logging::LogLevel::Info, + "success", + &format!( + "Rendered {} with {} params", + resolved_template.full_path().split('/').last().unwrap_or("template"), + resolved_params.len() + ), + ); + html + } + Err(e) => { + // Log detailed error information + if e.contains("query") { + logging::log_query_error(request, config.query.as_str(), &e); + } else if e.contains("template") { + logging::log_template_error(request, resolved_template.full_path(), &e); + } else { + logging::log( + request, + logging::LogLevel::Error, + "processing", + &format!("Request processing failed: {}", e), + ); + } + + // Return user-friendly error page format!( - "

Error

{}
", + r#" + +Error - sqlite-serve + +

Request Processing Error

+

An error occurred while processing your request.

+
+ Error Details +
{}
+
+

← Back to Home

+ +"#, e ) - }) + } + } } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index a439405..c58bcb8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ mod adapters; mod config; mod domain; mod handler_types; +mod logging; mod nginx_helpers; mod parsing; mod query; diff --git a/src/logging.rs b/src/logging.rs new file mode 100644 index 0000000..0727dce --- /dev/null +++ b/src/logging.rs @@ -0,0 +1,131 @@ +//! Structured logging utilities for sqlite-serve + +use ngx::http::Request; +use ngx::ngx_log_error; + +/// Log levels matching nginx conventions +#[derive(Debug, Clone, Copy)] +pub enum LogLevel { + Debug, + Info, + Warn, + Error, +} + +/// Log a message with context using nginx's native logging +pub fn log(request: &mut Request, level: LogLevel, module: &str, message: &str) { + let r: *mut ngx::ffi::ngx_http_request_t = request.into(); + + let log_level = match level { + LogLevel::Error => 3, // NGX_LOG_ERR + LogLevel::Warn => 4, // NGX_LOG_WARN + LogLevel::Info => 6, // NGX_LOG_INFO + LogLevel::Debug => 7, // NGX_LOG_DEBUG + }; + + unsafe { + let connection = (*r).connection; + if !connection.is_null() { + let log_ptr = (*connection).log; + if !log_ptr.is_null() { + // ngx_log_error! requires a string literal, so we use the C API directly + let c_msg = std::ffi::CString::new(format!("[sqlite-serve:{}] {}", module, message)) + .unwrap_or_else(|_| std::ffi::CString::new("log message error").unwrap()); + + ngx::ffi::ngx_log_error_core( + log_level as ngx::ffi::ngx_uint_t, + log_ptr, + 0, + c_msg.as_ptr(), + ); + } + } + } +} + +/// Log configuration parsing error +pub fn log_config_error(request: &mut Request, field: &str, value: &str, error: &str) { + log( + request, + LogLevel::Error, + "config", + &format!("Invalid {}: '{}' - {}", field, value, error), + ); +} + +/// Log query execution error +pub fn log_query_error(request: &mut Request, query: &str, error: &str) { + log( + request, + LogLevel::Error, + "query", + &format!("Query failed: {} - Error: {}", query, error), + ); +} + +/// Log template error +pub fn log_template_error(request: &mut Request, template_path: &str, error: &str) { + log( + request, + LogLevel::Error, + "template", + &format!("Template '{}' failed: {}", template_path, error), + ); +} + +/// Log parameter resolution error +pub fn log_param_error(request: &mut Request, param: &str, error: &str) { + log( + request, + LogLevel::Warn, + "params", + &format!("Parameter '{}' resolution failed: {}", param, error), + ); +} + +/// Log successful request processing +pub fn log_request_success( + request: &mut Request, + query: &str, + param_count: usize, + result_count: usize, + template: &str, +) { + log( + request, + LogLevel::Info, + "request", + &format!( + "Processed: query='{}' params={} results={} template='{}'", + query, param_count, result_count, template + ), + ); +} + +/// Log template loading info +pub fn log_template_loading(request: &mut Request, source: &str, count: usize, dir: &str) { + log( + request, + LogLevel::Debug, + "templates", + &format!("Loaded {} {} templates from '{}'", count, source, dir), + ); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_log_level_ordering() { + // Just verify we can construct log levels + let levels = vec![ + LogLevel::Debug, + LogLevel::Info, + LogLevel::Warn, + LogLevel::Error, + ]; + assert_eq!(levels.len(), 4); + } +} + diff --git a/src/nginx_helpers.rs b/src/nginx_helpers.rs index 05113bc..60ffd1a 100644 --- a/src/nginx_helpers.rs +++ b/src/nginx_helpers.rs @@ -1,9 +1,10 @@ //! NGINX-specific helper functions +use crate::logging; use ngx::core::Buffer; use ngx::ffi::ngx_chain_t; use ngx::http::{HttpModuleLocationConf, NgxHttpCoreModule, Request}; -use ngx::{core::Status, http, ngx_log_debug_http}; +use ngx::{core::Status, http}; /// Get document root and URI from request pub fn get_doc_root_and_uri(request: &mut Request) -> Result<(String, String), String> { @@ -54,9 +55,10 @@ pub fn send_response(request: &mut Request, body: &str) -> Status { Status::NGX_DONE } -/// Log and return error status +/// Log and return error status (deprecated - use logging module directly) +#[allow(dead_code)] pub fn log_error(request: &mut Request, context: &str, error: &str, status: http::HTTPStatus) -> Status { - ngx_log_debug_http!(request, "{}: {}", context, error); + logging::log(request, logging::LogLevel::Error, context, error); status.into() }