Mastering resty request log for Nginx Debugging
The digital arteries of our modern world pulse with data, ferried across complex networks by robust web servers and sophisticated API Gateways. At the heart of much of this infrastructure lies Nginx, a venerable workhorse renowned for its performance, stability, and versatility. Yet, for all its power, Nginx, especially when augmented by the dynamic capabilities of OpenResty and Lua scripting, can present a formidable challenge when issues inevitably arise. Debugging in such a dynamic, high-performance environment demands more than just superficial glances at generic log files. It calls for a mastery of custom, granular logging – a technique we broadly refer to as "resty request log" within the OpenResty ecosystem – to illuminate the intricate dance of requests and responses.
This comprehensive guide will embark on a journey to demystify advanced Nginx debugging using the incredible flexibility offered by OpenResty's Lua scripting, specifically focusing on how to craft and leverage detailed request-specific logs. We will transcend the limitations of traditional Nginx logging, diving deep into the log_by_lua* phases to capture every salient detail of an HTTP interaction. From request headers and bodies to internal processing states and upstream responses, understanding how to effectively log this information is paramount for maintaining system stability, identifying performance bottlenecks, and swiftly resolving the often-elusive bugs that plague complex API architectures. Whether you're operating a high-traffic api gateway, developing intricate api services, or simply aiming to enhance the observability of your Nginx gateway, mastering these logging techniques will undoubtedly elevate your debugging prowess.
The Nginx Ecosystem and the Intricate Dance of Debugging
Nginx has cemented its position as an indispensable component of virtually every modern web architecture. It serves as a static web server, a high-performance reverse proxy, a resilient load balancer, and increasingly, a powerful foundation for API Gateways. Its asynchronous, event-driven architecture allows it to handle an astonishing number of concurrent connections with minimal resource consumption, making it the preferred choice for mission-critical applications globally. However, this very efficiency and flexibility also introduce layers of complexity that can make debugging a daunting task.
In a typical modern setup, Nginx rarely operates in isolation. It often sits at the edge of a microservices architecture, routing requests, applying security policies, and sometimes even transforming data before forwarding it to upstream services. When OpenResty enters the picture, integrating the LuaJIT runtime directly into Nginx, the capabilities expand dramatically. Developers can now program Nginx's request processing lifecycle with Lua, implementing custom authentication, sophisticated routing logic, data caching, dynamic content generation, and intricate API gateway functionalities. This programmability, while immensely powerful, creates a black box effect where standard Nginx logs often fall short of providing the necessary visibility into the internal workings of a request's journey.
Traditional Nginx logging, primarily through access_log and error_log, offers a foundational level of insight. The access_log records basic information about client requests and server responses, such as client IP, request method, URI, status code, and bytes sent. While invaluable for traffic analysis and general monitoring, it rarely captures the rich, context-specific details required to troubleshoot intricate application logic or identify subtle issues within an API gateway's processing chain. For instance, if a custom Lua script within Nginx modifies a request header before proxying, or if an internal variable influences routing decisions, the access_log remains oblivious to these internal machinations. Similarly, error_log provides system-level errors and warnings from Nginx itself, along with any output from ngx.log calls in Lua, but it lacks the structured, per-request context often needed for application-level debugging. To peer into this black box and understand the full lifecycle of a request, from its arrival at the gateway to its eventual dispatch and response, we need a more surgical approach to logging, specifically tailored for the dynamic environment of OpenResty.
Introducing OpenResty and Lua for Enhanced Nginx Capabilities
OpenResty is not merely a collection of Nginx modules; it's a powerful web platform that seamlessly integrates the standard Nginx core with the LuaJIT virtual machine. This fusion transforms Nginx from a primarily configuration-driven server into a programmable gateway capable of executing high-performance Lua code directly within its request processing phases. The significance of this integration for debugging cannot be overstated, as it provides an unparalleled opportunity to inject custom logic and, crucially, custom logging at almost any point in the request's lifecycle.
The power of Lua scripting within Nginx is exposed through the lua-nginx-module, which defines various "phases" where Lua code can be executed. Understanding these phases is fundamental to mastering custom logging:
init_by_lua*: Runs once when Nginx starts. Ideal for initializing global Lua modules or shared dictionaries.init_worker_by_lua*: Runs once per worker process during startup. Suitable for worker-specific initialization.set_by_lua*: Used to set Nginx variables dynamically.rewrite_by_lua*: Executes before Nginx's rewrite phase. Can modify request URI, headers, or internal variables.access_by_lua*: Runs during the access control phase. Perfect for implementing custom authentication, authorization, and rate limiting for your API gateway.content_by_lua*: Generates the response content directly. Turns Nginx into an application server.header_filter_by_lua*: Modifies response headers before they are sent to the client.body_filter_by_lua*: Processes response body chunks. More complex due to streaming nature.log_by_lua*: Executes after the request has been processed and the response has been sent (or at least completely buffered). This is the most critical phase for comprehensive custom request logging, as it has access to both request and response data, and its execution typically doesn't delay the client's response.
By leveraging these phases, Nginx transitions from a rigid configuration file interpreter to a dynamic, programmable api gateway that can make real-time decisions, interact with external services, and, most importantly for our discussion, emit highly detailed, context-rich logs. This ability to inject Lua code at specific points allows developers to capture the internal state of Nginx and the custom logic, providing the visibility needed to debug even the most elusive issues in complex api architectures.
Deep Dive into Nginx Logging Mechanisms
Before we delve into the sophisticated world of Lua-based custom logging, it's essential to understand the foundational logging mechanisms provided by Nginx itself. These logs, while often insufficient on their own for deep debugging, remain crucial for initial triage and system-level monitoring.
The access_log Directive
The access_log directive is Nginx's primary tool for recording information about client requests. Every request that Nginx processes can be logged to a specified file or syslog server. Its basic syntax is straightforward:
access_log /path/to/access.log;
However, the real power of access_log lies in its ability to be customized using the log_format directive. This allows you to define exactly what information gets logged for each request using a rich set of built-in Nginx variables.
http {
log_format custom_combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'$request_time $upstream_response_time "$http_x_forwarded_for"';
server {
listen 80;
server_name example.com;
access_log /var/log/nginx/example.com_access.log custom_combined;
# ... other configurations
}
}
In this example, custom_combined extends the traditional combined log format by adding $request_time (total request processing time), $upstream_response_time (time spent waiting for a response from an upstream server), and $http_x_forwarded_for (useful for identifying the original client IP behind proxies). These additional variables are invaluable for initial performance analysis and tracing requests through multiple proxies, a common scenario for an api gateway.
Despite its flexibility with log_format, the access_log still has inherent limitations for deep debugging. It can only log Nginx's pre-defined variables and any variables you explicitly set. It cannot directly capture the internal state of a Lua script, the intermediate values of complex calculations, or the exact details of a request body or a large response body without significant, often impractical, workarounds. For instance, while you can log $request_body, Nginx might buffer large bodies to disk, making direct logging inefficient or incomplete within the access_log context. This constraint becomes particularly restrictive when debugging dynamic logic implemented in an api gateway where Lua scripts are constantly transforming, validating, and interacting with request data.
The error_log Directive
The error_log directive is where Nginx reports its own internal errors, warnings, and diagnostic messages. It is also the default destination for output from Lua's ngx.log function.
error_log /path/to/error.log info;
The second parameter, info in this case, specifies the logging level. Nginx supports several levels, ordered by severity: debug, info, notice, warn, error, crit, alert, and emerg. Setting it to debug provides the most verbose output, including internal Nginx processing steps, which can be overwhelming but occasionally invaluable for low-level Nginx module debugging. For most day-to-day operations and Lua debugging, info or warn is often sufficient.
error_log is crucial for identifying system-level problems, configuration issues, or unhandled exceptions within Lua scripts. When a Lua script encounters an error (e.g., trying to access a nil value), the traceback and error message will typically appear in the error_log. However, unlike access_log, error_log is not structured per request in a consistent manner. Messages from different requests can be interleaved, and correlating an error message to a specific client request can be challenging without additional context, such as a unique request ID. This lack of request-specific context makes error_log less ideal for debugging the logical flow of a single api call through a complex gateway and more suitable for diagnosing general system health.
Limitations of Standard Logs for Deep Debugging
While access_log and error_log are indispensable, their limitations become glaringly apparent when dealing with the intricacies of an OpenResty-powered api gateway:
- Lack of Internal State Visibility: They cannot easily reveal the values of Lua variables, the outcome of conditional logic, or the results of external API calls made from within Lua scripts.
- Request Body/Response Body Logging: Capturing the full request or response body, especially when large or binary, is difficult or inefficient with standard directives.
- Correlation Challenges: Tying disparate log entries from different Nginx worker processes or even different log files back to a single client request can be a headache, particularly in high-concurrency environments.
- No Contextual Richness: Standard logs often lack the application-specific context needed to understand why something happened, such as which user was involved, what specific parameters were passed to an internal function, or the exact version of an api being called.
These limitations underscore the necessity of a more sophisticated logging strategy that can leverage the programmability of OpenResty. This is where the concept of "resty request log" – a custom, Lua-driven approach to request-specific logging – truly shines.
Unveiling Custom Lua Logging: The Power of log_by_lua*
The term "resty request log" isn't a single, predefined OpenResty function or module. Instead, it refers to the powerful pattern of implementing custom, highly detailed, request-specific logging within OpenResty using Lua. The primary tool for achieving this is the log_by_lua* directive, which allows developers to execute Lua code at the very end of the request processing lifecycle, after the response has been sent to the client. This phase is ideal because logging operations, which can be I/O intensive, will not delay the client's perceived response time, thus minimizing performance impact.
The log_by_lua* Phase: Your Debugging Command Center
The log_by_lua_block (or log_by_lua_file) directive provides a safe and efficient sandbox to gather and emit comprehensive logs. At this point, most Nginx and Lua variables related to the request and response are available.
http {
# ... other configurations ...
server {
listen 80;
server_name example.com;
location /api/v1/user {
# ... processing logic (e.g., access_by_lua_block, proxy_pass) ...
log_by_lua_block {
-- Your custom Lua logging script goes here
}
}
}
}
Inside the log_by_lua_block, you have access to a rich set of OpenResty APIs to inspect the request and response details.
Using ngx.log for Custom Logging
The most straightforward way to emit custom logs from Lua is by using ngx.log. This function writes messages directly to Nginx's error_log file, respecting the configured error_log level.
-- Inside log_by_lua_block { ... }
ngx.log(ngx.INFO, "Custom log message: Request processed successfully.")
ngx.log takes a log level (e.g., ngx.DEBUG, ngx.INFO, ngx.WARN, ngx.ERR) and a message string. Using appropriate log levels allows you to filter messages in the error_log file, making it easier to focus on specific types of events during debugging. For instance, you might use ngx.DEBUG for highly verbose, temporary debugging information, and ngx.INFO for important events you always want to track.
Capturing Request Details
With Lua, you can capture virtually any detail of the incoming request:
- Nginx Variables: Access standard Nginx variables using
ngx.var.<variable_name>.lua local client_ip = ngx.var.remote_addr local request_uri = ngx.var.uri local request_method = ngx.var.request_method local http_host = ngx.var.host -- Host header local response_status = ngx.var.status -- Final HTTP status code local upstream_addr = ngx.var.upstream_addr -- Upstream server address local request_time = ngx.var.request_time -- Total request time - Request Headers: Retrieve all request headers as a Lua table using
ngx.req.get_headers().lua local headers = ngx.req.get_headers() ngx.log(ngx.INFO, "Request Headers: ", cjson.encode(headers)) local user_agent = headers["User-Agent"] local authorization = headers["Authorization"] - Request Body: Capturing the request body requires a bit more care. You must call
ngx.req.read_body()(in an earlier phase likeaccess_by_lua*) to read the body into memory, and thenngx.req.get_body_data()to retrieve it. If the body is large, it might be spooled to a file, in which casengx.req.get_body_file()would provide the path. For logging, it's generally safer to logget_body_data()for smaller bodies. For an api gateway, often only the first few KB of the body are relevant for debugging, so truncating large bodies is a good strategy. ```lua -- In an earlier phase like access_by_lua_block: ngx.req.read_body() -- Read the request body-- In log_by_lua_block: local body_data = ngx.req.get_body_data() if body_data then ngx.log(ngx.INFO, "Request Body: ", string.sub(body_data, 1, 2048), " (truncated)") end* **Query Parameters**:lua local args = ngx.req.get_uri_args() -- Returns a table of query parameters ngx.log(ngx.INFO, "Query Parameters: ", cjson.encode(args)) ```
Capturing Response Details
After the upstream service has responded and Nginx has processed the response, you can log its details:
- Response Headers: Retrieve all response headers using
ngx.resp.get_headers().lua local response_headers = ngx.resp.get_headers() ngx.log(ngx.INFO, "Response Headers: ", cjson.encode(response_headers)) local content_type = response_headers["Content-Type"] - Response Body: Capturing the full response body in the
log_by_lua*phase is more complex than the request body. It typically requires using thebody_filter_by_lua*phase to intercept and buffer chunks of the response body as they are streamed. This can significantly impact performance and memory usage, so it should be used judiciously, perhaps only for specific debugging scenarios or smaller responses. For most debugging, logging response headers and the status code is sufficient.
Logging Internal Lua Variables and States
This is where log_by_lua* truly shines beyond access_log. Any Lua variable, table, or state that was part of your rewrite_by_lua*, access_by_lua*, or content_by_lua* scripts can be logged, provided it's passed around or accessible within the log_by_lua* context. Often, this is achieved by storing data in ngx.ctx (the request context table) which is available across all Lua phases for a given request.
-- In access_by_lua_block:
ngx.ctx.user_id = "user123"
ngx.ctx.auth_success = true
ngx.ctx.upstream_service = "users_api"
-- In log_by_lua_block:
local user_id = ngx.ctx.user_id
local auth_success = ngx.ctx.auth_success
local upstream_service = ngx.ctx.upstream_service
ngx.log(ngx.INFO, "User ID: ", user_id, ", Auth Success: ", tostring(auth_success), ", Upstream: ", upstream_service)
Structuring Log Data: JSON vs. Plain Text
While plain text logs (ngx.log(ngx.INFO, "Key: Value")) are readable for simple debugging, they quickly become unmanageable in complex systems. Structured logging, especially using JSON, is a best practice for modern observability. JSON logs are machine-readable, making them effortless to parse, index, and query with log aggregation tools (e.g., ELK Stack, Splunk, Loki).
To enable JSON encoding in OpenResty, you'll need the lua-cjson library.
http {
lua_package_path "/path/to/lua-cjson/?.lua;;"; # Point to your cjson installation
server {
# ...
location /api/v1/product {
# ...
log_by_lua_block {
local cjson = require "cjson"
local log_data = {}
log_data.timestamp = ngx.var.time_iso8601
log_data.request_id = ngx.var.request_id -- We'll discuss this soon
log_data.client_ip = ngx.var.remote_addr
log_data.method = ngx.var.request_method
log_data.uri = ngx.var.uri
log_data.status = ngx.var.status
log_data.request_time = tonumber(ngx.var.request_time)
-- Example of logging custom context
if ngx.ctx.auth_result then
log_data.auth_result = ngx.ctx.auth_result
end
if ngx.ctx.route_target then
log_data.route_target = ngx.ctx.route_target
end
-- Log request headers (selective or all)
local req_headers = ngx.req.get_headers()
log_data.request_headers = {
["User-Agent"] = req_headers["User-Agent"],
["Referer"] = req_headers["Referer"],
["X-Forwarded-For"] = req_headers["X-Forwarded-For"],
}
-- Log truncated request body for relevant methods
local request_body_data = ngx.req.get_body_data()
if request_body_data then
log_data.request_body_snippet = string.sub(request_body_data, 1, 1024) -- Log first 1KB
end
ngx.log(ngx.INFO, cjson.encode(log_data))
}
}
}
}
This JSON output provides a single, self-contained record for each request, making it incredibly powerful for debugging and analysis, especially for an api gateway processing a multitude of diverse api calls.
Example Scenarios for Lua-based Debugging
Let's explore how custom Lua logging can be applied to common debugging challenges in an API gateway context:
- Debugging Authentication/Authorization Failures: If your
access_by_lua*script handles JWT validation or API key checks, you can log the input token, the decoded payload, the user ID extracted, and the final authorization decision (e.g.,ngx.ctx.is_authorized = true/false). This immediately pinpoints where an authorization failure occurred. - Tracing Request/Response Transformations: For an api gateway that modifies headers, rewrites URIs, or transforms request/response bodies,
log_by_lua*can capture the state before and after each transformation. For instance, log the original URI, the URI afterrewrite_by_lua*, and the final URI sent to the upstream. - Performance Profiling: By logging
ngx.now()at various points within your Lua scripts and storing the deltas inngx.ctx, you can accurately measure the execution time of specific Lua blocks, identifying performance bottlenecks within your api gateway logic. - Identifying Upstream Service Issues: Log the exact upstream URL, the HTTP status code returned by the upstream, and any specific error messages from the upstream service. This helps determine if the issue lies within your gateway or the backend api service.
APIPark is a high-performance AI gateway that allows you to securely access the most comprehensive LLM APIs globally on the APIPark platform, including OpenAI, Anthropic, Mistral, Llama2, Google Gemini, and more.Try APIPark now! 👇👇👇
Advanced Logging Techniques and Best Practices
While log_by_lua* sending to error_log is a great start, scaling your logging strategy for production environments, especially for a high-traffic api gateway, demands more advanced techniques and adherence to best practices.
External Logging Services and Asynchronous Logging
Sending all logs to the local filesystem's error_log can become an I/O bottleneck and makes centralized log management difficult. External logging services provide a robust solution.
- Sending Logs to Syslog: Nginx natively supports sending
error_logandaccess_logto a syslog server.nginx error_log syslog:server=127.0.0.1:514,facility=local7,tag=nginx_api_gateway info;For Lua-specific logs, you can uselua-resty-logger-socketto send data directly to syslog or a custom UDP receiver. This module allows non-blocking logging over UDP, minimizing latency impact.
HTTP/HTTPS Logging: For maximum flexibility, especially when integrating with centralized log management platforms like ELK Stack, Splunk, or Grafana Loki, you can POST your structured JSON logs directly over HTTP/HTTPS. lua-resty-http or lua-resty-string combined with ngx.timer.at for asynchronous execution are excellent tools for this. ```lua -- Example using ngx.timer.at for asynchronous HTTP logging -- This assumes a logging endpoint that accepts JSON POST requests-- In log_by_lua_block: local cjson = require "cjson" local http = require "resty.http"local log_data = { -- Populate your JSON log data here timestamp = ngx.var.time_iso8601, request_id = ngx.var.request_id, uri = ngx.var.uri, status = ngx.var.status, -- ... }local json_log_string = cjson.encode(log_data)-- Use ngx.timer.at to send the log asynchronously ngx.timer.at(0, function() local httpc = http.new() local ok, err = httpc:connect("log-aggregator.example.com", 8080) if not ok then ngx.log(ngx.ERR, "Failed to connect to log aggregator: ", err) return end
local res, err = httpc:request({
method = "POST",
path = "/logs",
headers = {
["Content-Type"] = "application/json"
},
body = json_log_string
})
if not res then
ngx.log(ngx.ERR, "Failed to send log to aggregator: ", err)
elseif res.status ~= 200 then
ngx.log(ngx.WARN, "Log aggregator returned non-200 status: ", res.status, " body: ", res.body)
end
httpc:close()
end) `` Asynchronous logging withngx.timer.at(0, ...)` is crucial. It schedules the log-sending task to run immediately in the background without blocking the current request processing, ensuring your logging doesn't become a bottleneck for your api gateway's performance.
Correlation IDs for End-to-End Tracing
In a microservices architecture, a single user request can traverse multiple services, queues, and databases. Debugging such a distributed flow without a mechanism to tie all related log entries together is incredibly difficult. This is where Correlation IDs (also known as Request IDs) become indispensable.
The principle is simple: 1. Generate: When a request first hits your API gateway, generate a unique ID (e.g., using ngx.var.request_id or lua-resty-string's UUID functions). 2. Propagate: Inject this ID into a standardized HTTP header (e.g., X-Request-ID, X-Correlation-ID) and ensure it's passed along to all upstream services the gateway communicates with. 3. Log: Include this correlation ID in every log message generated by Nginx, Lua scripts, and all backend services.
http {
# ...
server {
# ...
location /api/(.*) {
# Generate if not present, otherwise use existing
set $request_id "";
access_by_lua_block {
if not ngx.var.http_x_request_id then
ngx.var.request_id = ngx.var.time_local .. "-" .. string.sub(ngx.uuid(), 1, 8) -- Simple unique ID
else
ngx.var.request_id = ngx.var.http_x_request_id
end
}
proxy_set_header X-Request-ID $request_id; # Pass to upstream
proxy_pass http://upstream_backend;
log_by_lua_block {
local cjson = require "cjson"
local log_data = {
timestamp = ngx.var.time_iso8601,
request_id = ngx.var.request_id, -- Include in logs
-- ... other details ...
}
ngx.log(ngx.INFO, cjson.encode(log_data))
}
}
}
}
By consistently logging the request_id, you can easily search your centralized log management system for all events related to a specific user request, offering an "X-ray" view of its journey through your entire infrastructure. This dramatically simplifies debugging for api calls that traverse complex gateway and microservice landscapes.
Conditional Logging and Sampling
In high-traffic environments, logging every single detail of every request can generate an overwhelming volume of data and consume significant resources. Conditional logging and sampling offer intelligent ways to manage this:
- Log Based on Conditions: Log detailed information only for requests that meet specific criteria:
- Error Status Codes:
if tonumber(ngx.var.status) >= 400 then ... log detailed error ... end - Specific URLs/Paths:
if ngx.var.uri:match("^/debug/api") then ... log verbosely ... end - Specific Headers:
if ngx.req.get_headers()["X-Debug-Mode"] == "true" then ... end - Slow Requests:
if tonumber(ngx.var.request_time) > 1.0 then ... end
- Error Status Codes:
- Log Sampling: Log only a fraction of requests (e.g., 1 out of 100). This provides statistical insights without the full data load.
lua -- In log_by_lua_block if math.random(1, 100) == 1 then -- Log 1% of requests -- ... detailed logging logic ... endSampling is useful for performance monitoring or general health checks, but less effective for debugging specific, hard-to-reproduce issues.
Sensitive Data Masking
Security is paramount, especially for an api gateway handling sensitive user data. You must ensure that PII (Personally Identifiable Information), authentication tokens (like Authorization headers or JWTs), and other confidential data are never written to logs.
- Selective Header Logging: Instead of
cjson.encode(ngx.req.get_headers()), manually construct a table with only non-sensitive headers. - Regex Replacement: Use Lua's
string.gsubto find and replace sensitive patterns in request bodies or query strings (e.g., credit card numbers, email addresses, passwords). - Field Exclusion: If logging a JSON object, explicitly omit fields known to contain sensitive data.
Performance Considerations
Extensive logging, especially synchronous logging to disk or network, can impact the performance of your Nginx gateway.
- Prioritize Asynchronous Logging: Always favor
ngx.timer.atorlua-resty-logger-socket(UDP) for external logging to prevent blocking the request cycle. - Buffer Logs: For local file logging, increase Nginx's
access_logorerror_logbuffer sizes if necessary. - Minimize CPU Overhead: Avoid complex string manipulations or computationally expensive operations within your
log_by_lua*blocks, especially for every request. - Resource Limits: Be mindful of memory usage if you're buffering large request/response bodies in
ngx.ctxor Lua variables for logging.
Log Rotation and Retention
Regardless of where your logs go, proper log rotation and retention policies are critical. For local file logs, logrotate is the standard Unix utility. Ensure Nginx is configured to gracefully reopen log files after rotation (e.g., kill -USR1 <Nginx master process PID>). For external log aggregators, retention policies are typically configured within the logging service itself.
Practical Implementation Examples
Let's consolidate these concepts into practical Nginx and Lua configurations for common debugging scenarios.
Simple JSON Logger in log_by_lua_block
This example demonstrates a robust, structured logger that captures essential request and response details, perfect for a general api gateway debugging baseline.
http {
# Ensure lua-cjson is available
# If installed via luarocks, it might be in /usr/local/share/lua/5.1 or similar
lua_package_path "/usr/local/share/lua/5.1/?.lua;;";
# Define a shared dictionary for potential future use (e.g., rate limits, shared data)
lua_shared_dict my_cache 10m;
server {
listen 80;
server_name api.example.com;
# Set a correlation ID early in the request lifecycle
# Use existing X-Request-ID if present, otherwise generate a simple one
set $req_id "";
access_by_lua_block {
if ngx.req.get_headers()["x-request-id"] then
ngx.var.req_id = ngx.req.get_headers()["x-request-id"]
else
-- A simple ID for local use. For production, consider UUIDs.
ngx.var.req_id = ngx.var.time_local .. "-" .. string.sub(ngx.md5(ngx.var.remote_addr .. ngx.var.remote_port .. ngx.now()), 1, 8)
end
ngx.ctx.request_start_time = ngx.now() -- Capture start time
}
location / {
# For demonstration, a simple proxy pass
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Request-ID $req_id; # Pass correlation ID upstream
# Read request body in an earlier phase if you want to log it
# This must be done *before* proxy_pass if you intend to modify body or access it for logging.
# In general, if you only need it for logging, log_by_lua_block can access it if 'proxy_request_buffering off;' is not used.
# However, for guaranteed access and to avoid conflicts, it's safer to read it in access_by_lua_block.
access_by_lua_block {
ngx.req.read_body() -- Read body into memory
if ngx.req.get_body_data() then
ngx.ctx.request_body = ngx.req.get_body_data()
end
-- Append any additional custom context here
ngx.ctx.api_version = "v1.0"
}
log_by_lua_block {
local cjson = require "cjson"
local log_data = {}
log_data.timestamp = ngx.var.time_iso8601
log_data.request_id = ngx.var.req_id
log_data.client_ip = ngx.var.remote_addr
log_data.method = ngx.var.request_method
log_data.uri = ngx.var.uri
log_data.status = tonumber(ngx.var.status)
log_data.request_time_sec = tonumber(ngx.var.request_time)
log_data.upstream_response_time_sec = tonumber(ngx.var.upstream_response_time or 0)
log_data.upstream_addr = ngx.var.upstream_addr
-- Log selected request headers (avoid sensitive ones)
local req_headers = ngx.req.get_headers()
log_data.request_headers = {
["User-Agent"] = req_headers["User-Agent"],
["Accept"] = req_headers["Accept"],
["Content-Type"] = req_headers["Content-Type"],
["X-Forwarded-For"] = req_headers["X-Forwarded-For"],
}
-- Log truncated request body
if ngx.ctx.request_body then
log_data.request_body_snippet = string.sub(ngx.ctx.request_body, 1, 1024) -- Truncate to 1KB
if #ngx.ctx.request_body > 1024 then
log_data.request_body_truncated = true
end
end
-- Log selected response headers
local resp_headers = ngx.resp.get_headers()
log_data.response_headers = {
["Content-Type"] = resp_headers["Content-Type"],
["Cache-Control"] = resp_headers["Cache-Control"],
}
-- Log custom context from ngx.ctx
log_data.api_version = ngx.ctx.api_version
if ngx.ctx.request_start_time then
log_data.lua_processing_duration_ms = (ngx.now() - ngx.ctx.request_start_time) * 1000
end
ngx.log(ngx.INFO, cjson.encode(log_data))
}
}
}
}
This configuration provides a comprehensive JSON log for each request. The request_id ensures traceability, and the inclusion of custom context (api_version, lua_processing_duration_ms) offers deeper insights into the gateway's internal logic.
Debugging an API Gateway Route with Advanced Logic
Consider an API gateway scenario where Nginx uses Lua to dynamically route requests based on an API key, performs rate limiting, and transforms the request before proxying. Debugging failures in such a chain requires granular logging at each step.
http {
lua_package_path "/usr/local/share/lua/5.1/?.lua;;";
lua_shared_dict rate_limit_store 10m; # For rate limiting
server {
listen 80;
server_name my-api-gateway.com;
# Global request ID generation and start time capture
set $req_id "";
access_by_lua_block {
if ngx.req.get_headers()["x-request-id"] then
ngx.var.req_id = ngx.req.get_headers()["x-request-id"]
else
ngx.var.req_id = ngx.var.time_local .. "-" .. string.sub(ngx.md5(ngx.var.remote_addr .. ngx.var.remote_port .. ngx.now()), 1, 8)
end
ngx.ctx.request_start_time = ngx.now()
ngx.ctx.log_entries = {} -- Store debug messages throughout the request
table.insert(ngx.ctx.log_entries, { timestamp = ngx.now(), message = "Request received by gateway." })
-- Read body early if needed for auth/routing logic
ngx.req.read_body()
ngx.ctx.request_body = ngx.req.get_body_data()
}
location ~ ^/v1/(.*) {
set $upstream_target ""; # To be set by Lua for dynamic routing
set $api_key_valid "false"; # To be set by Lua
set $rate_limit_exceeded "false"; # To be set by Lua
access_by_lua_block {
local api_key = ngx.req.get_headers()["x-api-key"]
ngx.ctx.api_key = api_key
table.insert(ngx.ctx.log_entries, { timestamp = ngx.now(), message = "Checking API Key." })
if not api_key or # Check API key validity (simplified for example)
(api_key ~= "my-secret-key-123" and api_key ~= "another-key") then
ngx.var.api_key_valid = "false"
table.insert(ngx.ctx.log_entries, { timestamp = ngx.now(), message = "API Key Invalid.", api_key = api_key })
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
ngx.var.api_key_valid = "true"
table.insert(ngx.ctx.log_entries, { timestamp = ngx.now(), message = "API Key Valid." })
-- Dynamic Routing based on path and API key
if ngx.var[1] == "users" then -- Assumes location regex captured "users"
ngx.var.upstream_target = "http://users-service:8081"
ngx.ctx.route = "/v1/users"
elseif ngx.var[1] == "products" and api_key == "my-secret-key-123" then
ngx.var.upstream_target = "http://products-service:8082"
ngx.ctx.route = "/v1/products"
else
table.insert(ngx.ctx.log_entries, { timestamp = ngx.now(), message = "No matching route found.", uri_part = ngx.var[1] })
return ngx.exit(ngx.HTTP_NOT_FOUND)
end
table.insert(ngx.ctx.log_entries, { timestamp = ngx.now(), message = "Route determined.", target = ngx.var.upstream_target })
-- Basic Rate Limiting Example
local limit_store = ngx.shared.rate_limit_store
local key = "rate_limit:" .. api_key
local count = limit_store:get(key)
count = (count or 0) + 1
limit_store:set(key, count, 60) -- 60 seconds window
if count > 100 then -- 100 requests per minute limit
ngx.var.rate_limit_exceeded = "true"
table.insert(ngx.ctx.log_entries, { timestamp = ngx.now(), message = "Rate limit exceeded.", current_count = count })
return ngx.exit(ngx.HTTP_TOO_MANY_REQUESTS)
end
table.insert(ngx.ctx.log_entries, { timestamp = ngx.now(), message = "Rate limit check passed.", current_count = count })
}
proxy_pass $upstream_target;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Request-ID $req_id;
proxy_set_header X-API-Key $api_key; # Pass API key upstream if needed
log_by_lua_block {
local cjson = require "cjson"
local log_data = {}
log_data.timestamp = ngx.var.time_iso8601
log_data.request_id = ngx.var.req_id
log_data.client_ip = ngx.var.remote_addr
log_data.method = ngx.var.request_method
log_data.uri = ngx.var.uri
log_data.status = tonumber(ngx.var.status)
log_data.request_time_sec = tonumber(ngx.var.request_time)
log_data.upstream_response_time_sec = tonumber(ngx.var.upstream_response_time or 0)
log_data.upstream_addr = ngx.var.upstream_addr
log_data.api_key_valid = ngx.var.api_key_valid == "true"
log_data.rate_limit_exceeded = ngx.var.rate_limit_exceeded == "true"
log_data.upstream_target_route = ngx.var.upstream_target
log_data.route_path = ngx.ctx.route
-- Log internal debug entries
log_data.internal_debug_trace = ngx.ctx.log_entries
-- Log truncated request body
if ngx.ctx.request_body then
log_data.request_body_snippet = string.sub(ngx.ctx.request_body, 1, 1024)
if #ngx.ctx.request_body > 1024 then
log_data.request_body_truncated = true
end
end
-- Log selected request headers
local req_headers = ngx.req.get_headers()
log_data.request_headers = {
["User-Agent"] = req_headers["User-Agent"],
["Content-Type"] = req_headers["Content-Type"],
["X-Forwarded-For"] = req_headers["X-Forwarded-For"],
}
-- Only log response body for error cases for efficiency
if log_data.status >= 400 then
-- To log response body, you'd generally need body_filter_by_lua_block
-- For simplicity here, we assume it's not feasible in log_by_lua_block for large bodies.
-- You could log 'proxy_intercept_errors on;' and then capture error_page content.
end
ngx.log(ngx.INFO, cjson.encode(log_data))
}
}
}
}
This more complex example illustrates how to build a powerful "resty request log" that traces the entire logic flow within an api gateway. It captures authentication outcomes, dynamic routing decisions, rate limiting results, and even a chronological trace of internal debugging messages (ngx.ctx.log_entries). This level of detail is invaluable for diagnosing why an api call might be unauthorized, routed incorrectly, or blocked by rate limits.
APIPark Integration Note: While manually crafting resty request log implementations provides immense flexibility and control, it's worth noting that dedicated API Management Platforms and AI Gateways like APIPark streamline this process significantly. APIPark, for instance, offers comprehensive logging capabilities that record every detail of each API call, enabling businesses to quickly trace and troubleshoot issues without needing to write custom Lua scripts for every logging scenario. This can save considerable development and debugging time, allowing teams to focus on core business logic rather than infrastructure concerns. APIPark's detailed API call logging ensures system stability and data security out of the box, offering a powerful api gateway solution that automatically handles much of the logging complexity we've been discussing.
Table: Comparison of Nginx Logging Methods
| Feature | access_log |
error_log (Nginx native) |
ngx.log (Lua in log_by_lua*) |
External Logger (Lua + ngx.timer.at) |
APIPark (Managed Gateway) |
|---|---|---|---|---|---|
| Data Scope | Basic request/response vars | Nginx internal errors/info | Full request/response, Lua state | Full request/response, Lua state | Full lifecycle, AI model interactions, cost tracking |
| Output Format | Customizable text | Plain text | Plain text | Highly customizable (e.g., JSON) | Structured JSON, visual dashboards |
| Output Destination | File, Syslog | File, Syslog | error_log file |
HTTP endpoint, UDP (Syslog) | Internal storage, customizable export |
| Performance Impact | Low | Low to Medium | Low to Medium (to local file) | Low (asynchronous to external) | Minimized by platform design, high TPS |
| Contextual Detail | Limited | Very limited per request | High (Lua variables, custom logic) | High (Lua variables, custom logic) | Extremely High (application-specific) |
| Correlation ID Support | Manual $request_id |
None | Manual ngx.var.request_id |
Manual ngx.var.request_id |
Built-in, automatic tracing |
| Setup Complexity | Low | Low | Medium | High (Lua scripting, external service) | Low (single command deployment) |
| Use Case | Traffic analysis, basic monitoring | System health, Nginx errors | Deep request-specific debugging | Centralized, structured logging | Comprehensive API management & observability |
Integrating with Log Analysis Tools
The ultimate goal of structured, detailed logging is to make it actionable. Raw logs, no matter how comprehensive, are cumbersome to analyze manually. This is where modern log analysis tools come into play, transforming vast streams of data into actionable insights.
- ELK Stack (Elasticsearch, Logstash, Kibana): This popular open-source suite is designed for collecting, processing, storing, and visualizing logs.
- Logstash: Can ingest logs from various sources (files, syslog, HTTP), parse them (especially JSON), enrich them, and forward them to Elasticsearch.
- Elasticsearch: A distributed search and analytics engine that stores and indexes the parsed log data.
- Kibana: A powerful visualization frontend for Elasticsearch, allowing you to create dashboards, search logs with a rich query language, and set up alerts. By consistently logging JSON from your Nginx gateway, Logstash can effortlessly parse the data, making it immediately available for powerful searching and visualization in Kibana. You can quickly filter for all requests from a specific
client_ip, look forstatuscodes above 400, or analyzerequest_time_sectrends over time.
- Splunk: A commercial equivalent to the ELK Stack, offering similar capabilities with enterprise-grade features, support, and a powerful search processing language (SPL). Splunk excels at indexing unstructured and structured data and providing rich analytical tools. Many large enterprises leverage Splunk for their centralized log management and security information and event management (SIEM).
- Grafana Loki: A newer, open-source logging system designed specifically for Prometheus, Loki treats logs as streams of labels. It’s particularly appealing for its low operational cost and its ability to integrate seamlessly with Grafana for visualization. Unlike Elasticsearch, Loki indexes metadata about logs (labels) rather than the full log content, making it very efficient for large volumes. You would configure a
promtailagent to scrape your Nginx JSON logs and send them to Loki, where they can then be queried and visualized in Grafana.
Why Structured Logs (JSON) are Essential: All these tools thrive on structured data. When your Nginx gateway emits logs in JSON format, each field (e.g., request_id, status, upstream_addr, api_key_valid) becomes a distinct, searchable, and filterable attribute. This transforms log analysis from tedious text parsing into efficient data querying, significantly accelerating the debugging process. You can quickly pinpoint performance regressions, identify specific api endpoint failures, or trace complex user journeys across your distributed system with ease.
Conclusion
Mastering custom request logging with OpenResty's Lua capabilities – what we've broadly termed "resty request log" – is an indispensable skill for anyone operating and debugging Nginx in complex, high-performance environments, particularly those functioning as an api gateway. We've journeyed from the foundational limitations of traditional Nginx logs to the expansive possibilities offered by Lua scripting within the log_by_lua* phase.
By embracing structured logging, leveraging correlation IDs, and thoughtfully integrating with external log analysis tools, developers and operations teams can transform their Nginx gateway from a potential black box into a transparent, observable component of their infrastructure. This granular visibility into the full lifecycle of an api call, from its initial reception to its final response, empowers teams to swiftly diagnose performance issues, pinpoint application errors, bolster security, and ultimately ensure the stability and reliability of their critical services. While bespoke solutions offer unparalleled customization, platforms like APIPark highlight the growing trend towards integrated solutions that deliver comprehensive logging and API management out of the box, striking a balance between control and convenience. In an increasingly complex digital landscape, the ability to effectively "see" and understand your traffic is not merely a luxury, but a fundamental requirement for success.
Frequently Asked Questions (FAQs)
- What is "resty request log" and how does it differ from standard Nginx
access_log? "Resty request log" is not a specific OpenResty module but rather a common pattern of implementing highly detailed, custom request-specific logging using Lua within thelog_by_lua*phase of OpenResty. It differs from standardaccess_login its ability to capture intricate internal states of Lua scripts, specific request/response body contents, and dynamic variables that are not exposed by default Nginx logging, providing much deeper context for debugging complex api gateway logic. - Why is
log_by_lua*the preferred phase for custom logging in OpenResty? Thelog_by_lua*phase executes at the very end of the Nginx request processing cycle, after the response has been sent to the client. This means that any potentially I/O-intensive logging operations performed in this phase will not delay the client's response time, thus minimizing performance impact on the user experience. It also has access to almost all request and response variables, making it a comprehensive point for data collection. - How can I avoid logging sensitive data (like API keys or PII) when using custom Lua logging? To prevent sensitive data from appearing in logs, you should:
- Selectively log headers: Instead of logging all request headers, create a whitelist of non-sensitive headers to include.
- Mask data: Use Lua's string manipulation functions (e.g.,
string.gsub) to replace sensitive patterns in request bodies or query parameters with placeholders (e.g.,[REDACTED]). - Exclude fields: If logging JSON, explicitly omit or nullify fields that are known to contain sensitive information. Prioritize security by designing your logging strategy with data privacy in mind from the outset.
- What are the performance implications of extensive custom Lua logging in OpenResty? While
log_by_lua*is designed to be non-blocking with respect to the client response, extensive logging can still consume CPU, memory, and I/O resources on your Nginx gateway.- CPU: Complex Lua scripts for log formatting or data manipulation.
- Memory: Buffering large request/response bodies or extensive data in
ngx.ctx. - I/O: Writing large volumes of logs to disk or sending them synchronously over the network. To mitigate this, use asynchronous logging with
ngx.timer.atfor external log shippers, employ conditional logging or sampling for less critical data, and ensure your Lua code is optimized for efficiency.
- How do platforms like APIPark simplify Nginx debugging and API management? Platforms like APIPark address many of the complexities discussed by providing an integrated solution. They offer out-of-the-box comprehensive API call logging, often with structured data and correlation IDs, eliminating the need to write intricate custom Lua scripts for basic observability. APIPark, as an AI gateway and API management platform, centralizes features like API lifecycle management, authentication, traffic control, and detailed analytics, drastically reducing the operational overhead and accelerating the debugging process for api services by giving a holistic view of the gateway's operations and AI model interactions.
🚀You can securely and efficiently call the OpenAI API on APIPark in just two steps:
Step 1: Deploy the APIPark AI gateway in 5 minutes.
APIPark is developed based on Golang, offering strong product performance and low development and maintenance costs. You can deploy APIPark with a single command line.
curl -sSO https://download.apipark.com/install/quick-start.sh; bash quick-start.sh

In my experience, you can see the successful deployment interface within 5 to 10 minutes. Then, you can log in to APIPark using your account.

Step 2: Call the OpenAI API.
