Introduction

Welcome back to "Getting Started with NGINX Web Server"! You've reached lesson four, and you're making remarkable progress. So far, we've learned to serve static content, configure reverse proxies, and manage multiple applications through virtual hosting. Now, we're going to explore a critical aspect of production web servers: logging.

Every request that flows through NGINX generates valuable data. Who accessed your application? Which endpoints are most popular? How long did requests take? Are there errors we should investigate? Without proper logging, these questions remain unanswered, leaving us blind to both problems and opportunities.

In this lesson, we'll configure NGINX to capture detailed information about every request and error. We'll create custom log formats that include timing metrics, set up separate log files for each application, and learn how to analyze logs to understand traffic patterns and troubleshoot issues. By the end, you'll know how to make NGINX tell you exactly what's happening on your server.

Why Logging Matters

Imagine running a web application without logs. When users report slow page loads, you have no data to investigate. When an API endpoint suddenly fails, you can't determine what went wrong. When planning infrastructure upgrades, you lack metrics about actual usage patterns.

Logs serve three essential purposes. First, they enable debugging: when something breaks, logs reveal what happened, when it happened, and often why. Second, they provide monitoring: by analyzing access patterns, we can identify performance bottlenecks, unusual traffic spikes, or security threats. Third, they support analytics: understanding which features users access most helps guide development priorities.

However, logging comes with trade-offs. Detailed logs consume disk space and can impact performance if not configured carefully. We need to balance the value of information against resource costs, capturing what matters without overwhelming our systems.

NGINX Logging Architecture

NGINX maintains two distinct types of logs, each serving a different purpose. The access log records every client request, capturing details like the requested URL, response status, bytes sent, and request duration. This log helps us understand normal traffic patterns and identify anomalies.

The error log captures internal problems, configuration issues, and unexpected conditions. When NGINX can't find a file, when an upstream server is unavailable, or when something goes wrong during request processing, it writes to the error log. These entries are categorized by severity levels, ranging from informational messages to critical failures.

Both log types can be configured globally or per application, giving us fine-grained control over what gets logged and where. This flexibility is essential when managing multiple applications, as we saw in the previous lesson.

The log_format Directive

Before NGINX can write access logs, it needs to know what format to use. The log_format directive defines a template for log entries, specifying which pieces of information to capture and how to structure them. We place these definitions in the http block, making them available to all server blocks.

Each log format has a name (here, main) followed by a template string. The dollar-sign variables like $remote_addr and $status are placeholders that NGINX replaces with actual values for each request. The spaces, hyphens, and brackets are literal text that appears in the log output, making entries readable and parseable.

This format is similar to the Apache Combined Log Format, which many log analysis tools recognize. It captures the client's IP address, authenticated username if any, timestamp, request line, HTTP status code, response size, referrer, and user agent.

Adding Detailed Timing Metrics

For performance monitoring and debugging, we often need more than basic request information. Let's define a second, more detailed format:

This format includes everything from main plus four timing variables:

  • $request_time: Total time NGINX spent processing the request, from reading the first bytes to sending the last
  • $upstream_connect_time: Time spent establishing a connection to the backend server
  • $upstream_header_time: Time until NGINX received the first byte of the response header from the backend
  • $upstream_response_time: Total time the backend took to send the complete response

These metrics are invaluable for identifying slow backends, network latency issues, or requests that take unexpectedly long to process.

Configuring Global Logs

With our formats defined, we can now configure where NGINX writes logs at the global level:

The access_log directive specifies the file path and which format to use. Every request to any server block will be logged to logs/access.log using the main format we defined earlier. The path is relative to NGINX's prefix directory (typically where you started NGINX from or where it's installed).

The error_log directive sets the error log file and minimum severity level. By specifying warn, we're telling NGINX to log warnings and anything more severe (errors, critical failures, alerts, and emergencies), but to skip informational and debug messages. This keeps error logs focused on actual problems without excessive noise.

Application-Specific Access Logs

While global logs capture everything, they become difficult to parse when managing multiple applications. Let's configure separate access logs for each application in their respective server blocks:

Notice we're using the detailed format here, which includes our timing metrics. This allows us to monitor app1's performance characteristics separately. Because we defined a global access_log, requests to app1.local appear in both the global access.log (using the main format) and app1_access.log (with detailed timing information). If we removed the global access_log directive, only the app-specific log would receive entries.

The error_log directive without a level defaults to error severity, which is slightly less verbose than our global warn level. This means we'll capture actual errors specific to this application in its own file.

Configuring the Second Application's Logs

Similarly, we set up logging for the second application:

App2 gets its own dedicated log files. This separation is extremely useful when troubleshooting: if users report issues with app2, we can examine app2_access.log and app2_error.log without wading through unrelated traffic from app1.

Both applications use the detailed format because we want timing metrics for both. However, we could easily use different formats for different applications if their needs differ.

Selective Logging for Health Checks

Many monitoring systems continuously poll health check endpoints. Logging these can quickly overwhelm access logs with noise. Let's exclude them:

The access_log off directive disables access logging for this specific location. Health checks return immediately without generating log entries, keeping our logs focused on actual user traffic. Note that error logs are still active; if something goes wrong with the health check endpoint, we'll still be notified.

Understanding Log Output

Let's examine what entries in our detailed access log actually look like:

Reading from left to right: the client's IP is 192.168.1.50, no authentication was used (the dashes), and the request occurred at 14:23:15 on December 4, 2024. The request was GET /api/users HTTP/1.1, which returned status 200 and sent 1,247 bytes. The referrer was the app1 homepage, and the user agent identifies a Firefox browser.

The timing metrics tell an important story: total request time (rt) was 125 milliseconds. Connecting to the upstream took just 3 ms (uct), receiving the response headers took 45 ms (uht), and the complete response came back in 122 ms (urt). This suggests the backend itself is reasonably fast, but there might be some processing time we could optimize.

Analyzing Error Logs

Error logs follow a different format, structured by NGINX itself:

This entry shows a timestamp, severity level (error), process information, and a detailed error message. The backend at 127.0.0.1:5000 timed out while processing /api/slow, requested by client 192.168.1.50. This level of detail makes debugging much easier than a simple "something went wrong" message.

Error log entries appear only when problems occur, so a healthy server might go hours or days without new error log entries, while access logs continuously grow with every request.

Log Rotation and Management

As your server handles traffic, log files grow continuously. Without rotation, they can consume all available disk space. Most production systems use tools like logrotate to compress old logs and limit disk usage.

A typical rotation strategy keeps the last week of logs immediately accessible, older logs compressed, and deletes logs after a certain age. You might rotate logs daily at midnight, compressing yesterday's logs and deleting anything older than 30 days.

NGINX supports log rotation gracefully. When you move or rename a log file, send NGINX a USR1 signal, and it will reopen its log files, creating fresh ones without dropping requests.

Conclusion and Next Steps

Excellent work! You've now learned how to configure comprehensive logging in NGINX, from defining custom log formats with timing metrics to setting up per-application logs and selectively disabling logs where appropriate. You understand how access logs capture request patterns, how error logs help debug problems, and how to interpret the information both types provide.

Logging is essential for maintaining production systems. The configuration skills you've gained will serve you well, whether you're debugging a single application or monitoring a complex multi-service environment. Remember that good logging strikes a balance: capture enough information to understand what's happening without overwhelming your storage or analysis capabilities.

Now it's time to apply these concepts hands-on! Get ready to configure your own logging setup, experiment with different log formats, and practice analyzing log output to understand server behavior. Let's put your new logging knowledge into action!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal