Implement structured logging with ngx_log_error_core

Logging now uses nginx's native error log with proper levels:
- ERROR (level 3): Configuration/query/template failures
- WARN (level 4): Missing parameters
- INFO (level 6): Request processing, template resolution
- DEBUG (level 7): Detailed tracing

Log Format: [sqlite-serve:module] message

Example output:
- [info] [sqlite-serve:handler] Processing request for /books
- [info] [sqlite-serve:template] Resolved template: ./server_root/books/catalog.hbs
- [info] [sqlite-serve:params] Resolved 1 parameters
- [notice] [sqlite-serve:success] Rendered catalog.hbs with 1 params

Specialized logging functions:
- log_config_error(): Invalid configuration
- log_query_error(): SQL errors with query shown
- log_template_error(): Template failures with path
- log_param_error(): Parameter resolution issues
- log_request_success(): Successful processing info
- log_template_loading(): Template discovery

Uses ngx_log_error_core() C API directly for dynamic messages.

Test Coverage: 59 tests
Configuration: error_log set to debug level for visibility
This commit is contained in:
Edward Langley
2025-11-15 17:05:09 -08:00
parent 032a105b3f
commit 86b79c0a85
5 changed files with 225 additions and 21 deletions

View File

@ -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

View File

@ -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!(
"<html><body><h1>Error</h1><pre>{}</pre></body></html>",
r#"<!DOCTYPE html>
<html>
<head><title>Error - sqlite-serve</title></head>
<body style="font-family: monospace; max-width: 800px; margin: 2rem auto; padding: 0 1rem;">
<h1 style="color: #CC9393;">Request Processing Error</h1>
<p style="color: #A6A689;">An error occurred while processing your request.</p>
<details style="margin-top: 1rem; background: #1111; padding: 1rem; border-left: 3px solid #CC9393;">
<summary style="cursor: pointer; color: #DFAF8F; font-weight: bold;">Error Details</summary>
<pre style="margin-top: 1rem; color: #DCDCCC; overflow-x: auto;">{}</pre>
</details>
<p style="margin-top: 2rem;"><a href="/" style="color: #7CB8BB;">← Back to Home</a></p>
</body>
</html>"#,
e
)
})
}
}
}
#[cfg(test)]

View File

@ -4,6 +4,7 @@ mod adapters;
mod config;
mod domain;
mod handler_types;
mod logging;
mod nginx_helpers;
mod parsing;
mod query;

131
src/logging.rs Normal file
View File

@ -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);
}
}

View File

@ -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()
}