Apache and Nginx Logs: Analysis and Rotation

Web server logs are invaluable sources of information for monitoring application health, diagnosing issues, analyzing traffic patterns, detecting security threats, and understanding user behavior. Both Apache and Nginx generate detailed access and error logs that, when properly managed and analyzed, provide critical insights into your web infrastructure. This comprehensive guide covers log configuration, analysis techniques, rotation strategies, and best practices for both Apache and Nginx web servers.

Effective log management is essential for maintaining operational visibility, troubleshooting performance issues, ensuring security compliance, and making data-driven decisions about your infrastructure. Whether you're tracking down a mysterious 500 error, analyzing traffic patterns for capacity planning, or investigating potential security incidents, mastering web server log analysis and management is a fundamental skill for system administrators and DevOps engineers.

Table of Contents

Prerequisites

Before working with web server logs, ensure you have:

  • Operating System: Ubuntu 20.04/22.04, Debian 10/11, CentOS 8/Rocky Linux 8, or similar
  • Web Server: Apache 2.4+ or Nginx 1.18+ installed and running
  • Root or sudo access: Required for configuring log settings and rotation
  • Basic knowledge: Understanding of HTTP, web servers, and command-line tools
  • Disk space: Adequate storage for log retention (logs can grow quickly)
  • Text editor: nano, vim, or similar for editing configuration files
  • Log analysis tools: Basic familiarity with grep, awk, sed recommended

Understanding Web Server Logs

Types of Web Server Logs

Access Logs: Record all requests processed by the web server

  • Client IP addresses
  • Request timestamps
  • HTTP methods and URLs
  • Response status codes
  • Bytes transferred
  • User agents and referrers

Error Logs: Record server errors, warnings, and diagnostic information

  • Application errors (500 errors)
  • Configuration issues
  • Missing files (404 errors)
  • Permission problems
  • Module warnings

Log Formats

Combined Log Format (most common):

LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined

Example entry:

192.168.1.100 - - [10/Jan/2026:10:30:45 +0000] "GET /index.html HTTP/1.1" 200 2326 "https://google.com" "Mozilla/5.0..."

Fields explained:

  • %h: Client IP address (192.168.1.100)
  • %l: Identity (usually -)
  • %u: Authenticated user (or -)
  • %t: Timestamp
  • %r: Request line
  • %>s: Status code (200)
  • %b: Size in bytes (2326)
  • Referer: Where visitor came from
  • User-Agent: Browser information

Log Locations

Apache:

  • Ubuntu/Debian: /var/log/apache2/
    • access.log
    • error.log
  • CentOS/Rocky: /var/log/httpd/
    • access_log
    • error_log

Nginx:

  • All distributions: /var/log/nginx/
    • access.log
    • error.log

Apache Log Configuration

Custom Log Formats

Define custom log formats in Apache configuration:

sudo nano /etc/apache2/apache2.conf
# Standard Combined format
LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined

# Common format
LogFormat "%h %l %u %t \"%r\" %>s %O" common

# Virtual host combined format
LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined

# Custom format with response time
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %D" custom_timing

# JSON format for easy parsing
LogFormat "{ \"time\":\"%t\", \"remote_addr\":\"%a\", \"status\":\"%>s\", \"request\":\"%U%q\", \"method\":\"%m\", \"bytes\":\"%B\", \"referer\":\"%{Referer}i\", \"agent\":\"%{User-Agent}i\", \"duration\":\"%D\" }" json_format

# Detailed format with SSL information
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %{SSL_PROTOCOL}x %{SSL_CIPHER}x" ssl_combined

Virtual Host Log Configuration

Configure per-site logging:

sudo nano /etc/apache2/sites-available/example.com.conf
<VirtualHost *:80>
    ServerName example.com
    DocumentRoot /var/www/example.com

    # Separate access log for this site
    CustomLog ${APACHE_LOG_DIR}/example.com-access.log combined

    # Separate error log for this site
    ErrorLog ${APACHE_LOG_DIR}/example.com-error.log

    # Set error log level (debug, info, notice, warn, error, crit, alert, emerg)
    LogLevel warn

    # Conditional logging (skip logging for specific paths)
    SetEnvIf Request_URI "^/health-check$" dontlog
    CustomLog ${APACHE_LOG_DIR}/example.com-access.log combined env=!dontlog
</VirtualHost>

Error Log Levels

Configure Apache error log verbosity:

# Global log level
LogLevel warn

# Module-specific log levels
LogLevel core:info ssl:warn rewrite:trace3

# Per-directory log level
<Directory /var/www/html/api>
    LogLevel debug
</Directory>

Log levels (most to least verbose):

  • trace8 - trace1: Trace messages
  • debug: Debugging messages
  • info: Informational messages
  • notice: Normal but significant conditions
  • warn: Warning conditions
  • error: Error conditions
  • crit: Critical conditions
  • alert: Action must be taken immediately
  • emerg: System is unusable

Conditional Logging

Skip logging for specific requests:

# Don't log health checks and monitoring
SetEnvIf Request_URI "^/health$" dontlog
SetEnvIf Request_URI "^/status$" dontlog
SetEnvIf Request_URI "^/ping$" dontlog

# Don't log static assets (optional)
SetEnvIf Request_URI "\.(jpg|gif|png|css|js|ico)$" dontlog

# Apply conditional logging
CustomLog ${APACHE_LOG_DIR}/access.log combined env=!dontlog

# Log only errors for specific IPs
SetEnvIf Remote_Addr "192\.168\.1\." internalip
CustomLog ${APACHE_LOG_DIR}/internal-access.log combined env=internalip

Nginx Log Configuration

Custom Log Formats

Define custom log formats in Nginx:

sudo nano /etc/nginx/nginx.conf
http {
    # Default combined format
    log_format combined '$remote_addr - $remote_user [$time_local] '
                       '"$request" $status $body_bytes_sent '
                       '"$http_referer" "$http_user_agent"';

    # Extended format with timing information
    log_format detailed '$remote_addr - $remote_user [$time_local] '
                       '"$request" $status $body_bytes_sent '
                       '"$http_referer" "$http_user_agent" '
                       'rt=$request_time uct="$upstream_connect_time" '
                       'uht="$upstream_header_time" urt="$upstream_response_time"';

    # JSON format for structured logging
    log_format json_combined escape=json
    '{'
        '"time_local":"$time_local",'
        '"remote_addr":"$remote_addr",'
        '"remote_user":"$remote_user",'
        '"request":"$request",'
        '"status": "$status",'
        '"body_bytes_sent":"$body_bytes_sent",'
        '"request_time":"$request_time",'
        '"http_referrer":"$http_referer",'
        '"http_user_agent":"$http_user_agent",'
        '"http_x_forwarded_for":"$http_x_forwarded_for"'
    '}';

    # Performance monitoring format
    log_format performance '$remote_addr - $remote_user [$time_local] '
                          '"$request" $status $body_bytes_sent '
                          'rt=$request_time '
                          'uct=$upstream_connect_time '
                          'uht=$upstream_header_time '
                          'urt=$upstream_response_time '
                          'cs=$upstream_cache_status';

    # SSL/TLS format
    log_format ssl_combined '$remote_addr - $remote_user [$time_local] '
                           '"$request" $status $body_bytes_sent '
                           '"$http_referer" "$http_user_agent" '
                           '$ssl_protocol $ssl_cipher';

    # Default access log
    access_log /var/log/nginx/access.log combined;

    # Error log with level
    error_log /var/log/nginx/error.log warn;
}

Server Block Log Configuration

Configure per-site logging:

sudo nano /etc/nginx/sites-available/example.com
server {
    listen 80;
    server_name example.com www.example.com;

    root /var/www/html;

    # Access log with custom format
    access_log /var/log/nginx/example.com-access.log detailed;

    # Error log with level
    error_log /var/log/nginx/example.com-error.log warn;

    # Conditional logging - skip health checks
    location = /health {
        access_log off;
        return 200 "OK\n";
    }

    # Disable logging for static files (optional)
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2)$ {
        access_log off;
        expires 30d;
    }

    # Main location
    location / {
        try_files $uri $uri/ =404;
    }
}

Error Log Levels

Configure Nginx error log verbosity:

# Available levels: debug, info, notice, warn, error, crit, alert, emerg

# Global error log
error_log /var/log/nginx/error.log warn;

# Server-specific error log
server {
    error_log /var/log/nginx/example.com-error.log error;
}

# Debug logging for specific location
location /api {
    error_log /var/log/nginx/api-debug.log debug;
}

Buffered Logging

Improve I/O performance with buffered logging:

# Buffer logs to reduce disk I/O
access_log /var/log/nginx/access.log combined buffer=32k flush=5s;

# Disable access logging for high-performance scenarios
access_log off;

Log Analysis Techniques

Basic Log Analysis Commands

View recent log entries:

# Apache
sudo tail -f /var/log/apache2/access.log
sudo tail -f /var/log/apache2/error.log

# Nginx
sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/nginx/error.log

# View last 100 lines
sudo tail -100 /var/log/apache2/access.log

# View logs from specific time
sudo grep "10/Jan/2026" /var/log/nginx/access.log

Analyze Status Codes

# Count status codes
awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -nr

# Count 404 errors
grep " 404 " /var/log/apache2/access.log | wc -l

# List unique 404 URLs
grep " 404 " /var/log/nginx/access.log | awk '{print $7}' | sort | uniq -c | sort -nr | head -20

# Count 500 errors by hour
grep " 500 " /var/log/apache2/access.log | cut -d[ -f2 | cut -d] -f1 | awk -F: '{print $2":00"}' | sort -n | uniq -c

Analyze Traffic Sources

# Top IP addresses
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head -20

# Top user agents
awk -F'"' '{print $6}' /var/log/apache2/access.log | sort | uniq -c | sort -nr | head -10

# Top referrers
awk -F'"' '{print $4}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head -20

# Requests by country (requires GeoIP)
# Install: sudo apt install geoip-bin geoip-database

Analyze Request Patterns

# Most requested URLs
awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head -20

# Requests by HTTP method
awk '{print $6}' /var/log/apache2/access.log | sort | uniq -c | sort -nr

# Requests per hour
cut -d[ -f2 /var/log/nginx/access.log | cut -d] -f1 | awk -F: '{print $2":00"}' | sort -n | uniq -c

# Bandwidth usage by IP
awk '{bytes+=$10} END {print bytes/1024/1024 " MB"}' /var/log/nginx/access.log

Detect Security Issues

# Detect SQL injection attempts
grep -i "select.*from\|union.*select\|insert.*into\|delete.*from" /var/log/apache2/access.log

# Detect path traversal attempts
grep -E "\.\./|\.\.%2F" /var/log/nginx/access.log

# Detect brute force login attempts
grep "POST /login" /var/log/apache2/access.log | awk '{print $1}' | sort | uniq -c | sort -nr

# Detect suspicious user agents
grep -i "sqlmap\|nikto\|nessus\|nmap" /var/log/nginx/access.log

# Find requests from specific IP
grep "192.168.1.100" /var/log/apache2/access.log

Performance Analysis

# Slowest requests (Nginx with timing in log format)
awk '{print $NF, $7}' /var/log/nginx/access.log | sort -nr | head -20

# Average response time
awk '{sum+=$NF; count++} END {print sum/count}' /var/log/nginx/access.log

# Requests taking longer than 1 second
awk '$NF > 1.0 {print $7, $NF}' /var/log/nginx/access.log

# Traffic by hour
awk '{print $4}' /var/log/apache2/access.log | cut -d: -f2 | sort -n | uniq -c

Using GoAccess for Real-Time Analysis

Install and use GoAccess for interactive log analysis:

# Install GoAccess
sudo apt install goaccess -y

# Real-time Apache analysis
sudo goaccess /var/log/apache2/access.log --log-format=COMBINED -o /var/www/html/report.html --real-time-html

# Real-time Nginx analysis
sudo goaccess /var/log/nginx/access.log --log-format=COMBINED -o /var/www/html/report.html --real-time-html

# Terminal-based interface
sudo goaccess /var/log/nginx/access.log --log-format=COMBINED

# Generate static HTML report
sudo goaccess /var/log/apache2/access.log --log-format=COMBINED -o /tmp/report.html

Log Rotation

Logrotate Configuration

Logrotate automatically rotates, compresses, and manages logs:

Apache logrotate configuration:

sudo nano /etc/logrotate.d/apache2
/var/log/apache2/*.log {
    # Rotate daily
    daily

    # Keep 14 days of logs
    rotate 14

    # Compress old logs
    compress

    # Delay compression until next rotation
    delaycompress

    # Don't error if log file is missing
    missingok

    # Don't rotate if log is empty
    notifempty

    # Create new log with specific permissions
    create 640 root adm

    # Share log files between multiple processes
    sharedscripts

    # Reload Apache after rotation
    postrotate
        if /etc/init.d/apache2 status > /dev/null 2>&1; then \
            /etc/init.d/apache2 reload > /dev/null 2>&1; \
        fi;
    endscript

    # Alternatively, use systemd
    # postrotate
    #     systemctl reload apache2 > /dev/null 2>&1 || true
    # endscript
}

Nginx logrotate configuration:

sudo nano /etc/logrotate.d/nginx
/var/log/nginx/*.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    create 0640 www-data adm
    sharedscripts

    # Signal Nginx to reopen log files
    postrotate
        if [ -f /var/run/nginx.pid ]; then
            kill -USR1 $(cat /var/run/nginx.pid)
        fi
    endscript
}

Custom Rotation Schedules

Weekly rotation with 8 weeks retention:

/var/log/nginx/example.com-access.log {
    weekly
    rotate 8
    compress
    delaycompress
    notifempty
    create 0640 www-data adm
    sharedscripts
    postrotate
        kill -USR1 $(cat /var/run/nginx.pid)
    endscript
}

Size-based rotation:

/var/log/apache2/high-traffic-access.log {
    # Rotate when log reaches 100MB
    size 100M

    # Keep 50 rotated logs
    rotate 50

    compress
    delaycompress
    notifempty
    create 640 root adm
    sharedscripts
    postrotate
        systemctl reload apache2
    endscript
}

Manual Log Rotation

# Force log rotation
sudo logrotate -f /etc/logrotate.d/nginx

# Test logrotate configuration
sudo logrotate -d /etc/logrotate.d/apache2

# Check logrotate status
cat /var/lib/logrotate/status

Alternative: Rotation with Systemd Timer

Create custom rotation with systemd:

# Create rotation script
sudo nano /usr/local/bin/rotate-nginx-logs.sh
#!/bin/bash
LOG_DIR="/var/log/nginx"
ARCHIVE_DIR="/var/log/nginx/archive"
DATE=$(date +%Y%m%d-%H%M%S)

mkdir -p "$ARCHIVE_DIR"

# Move current log
mv "$LOG_DIR/access.log" "$ARCHIVE_DIR/access-$DATE.log"
mv "$LOG_DIR/error.log" "$ARCHIVE_DIR/error-$DATE.log"

# Signal Nginx to create new logs
kill -USR1 $(cat /var/run/nginx.pid)

# Compress old logs
gzip "$ARCHIVE_DIR/access-$DATE.log"
gzip "$ARCHIVE_DIR/error-$DATE.log"

# Delete logs older than 30 days
find "$ARCHIVE_DIR" -name "*.log.gz" -mtime +30 -delete
# Make executable
sudo chmod +x /usr/local/bin/rotate-nginx-logs.sh

# Create systemd service
sudo nano /etc/systemd/system/nginx-log-rotation.service
[Unit]
Description=Nginx Log Rotation

[Service]
Type=oneshot
ExecStart=/usr/local/bin/rotate-nginx-logs.sh
# Create systemd timer
sudo nano /etc/systemd/system/nginx-log-rotation.timer
[Unit]
Description=Nginx Log Rotation Timer

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target
# Enable and start timer
sudo systemctl enable nginx-log-rotation.timer
sudo systemctl start nginx-log-rotation.timer

# Check timer status
sudo systemctl list-timers

Centralized Logging

rsyslog Configuration

Send logs to centralized syslog server:

Configure Apache to use syslog:

# Add to Apache configuration
CustomLog "|/usr/bin/logger -t apache -p local6.info" combined
ErrorLog syslog:local6

Configure Nginx to use syslog:

access_log syslog:server=192.168.1.100:514,facility=local6,tag=nginx,severity=info combined;
error_log syslog:server=192.168.1.100:514,facility=local6,tag=nginx,severity=error;

ELK Stack Integration

Send logs to Elasticsearch/Logstash/Kibana:

Install Filebeat:

# Add Elastic repository
wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
echo "deb https://artifacts.elastic.co/packages/8.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-8.x.list

# Install Filebeat
sudo apt update
sudo apt install filebeat -y

# Configure Filebeat
sudo nano /etc/filebeat/filebeat.yml
filebeat.inputs:
- type: log
  enabled: true
  paths:
    - /var/log/nginx/access.log
  fields:
    log_type: nginx_access

- type: log
  enabled: true
  paths:
    - /var/log/nginx/error.log
  fields:
    log_type: nginx_error

output.elasticsearch:
  hosts: ["localhost:9200"]

setup.kibana:
  host: "localhost:5601"
# Enable and start Filebeat
sudo systemctl enable filebeat
sudo systemctl start filebeat

Security and Compliance

Secure Log Files

# Set appropriate permissions
sudo chmod 640 /var/log/nginx/access.log
sudo chmod 640 /var/log/apache2/error.log

# Set ownership
sudo chown root:adm /var/log/nginx/*.log
sudo chown root:adm /var/log/apache2/*.log

# Restrict directory access
sudo chmod 750 /var/log/nginx
sudo chmod 750 /var/log/apache2

Log Anonymization

Remove sensitive data from logs:

Apache anonymization:

# Hash IP addresses
LogFormat "%{c}a %l %u %t \"%r\" %>s %b" anonymized
SetEnvIf Remote_Addr "(\d+\.\d+\.\d+)\.\d+" ANONYMIZED_IP=$1.0
CustomLog ${APACHE_LOG_DIR}/access.log "%{ANONYMIZED_IP}e %l %u %t \"%r\" %>s %b"

Nginx anonymization:

# Hash or truncate IP addresses
map $remote_addr $anonymized_ip {
    ~(?P<ip>\d+\.\d+\.\d+)\.\d+ $ip.0;
    ~(?P<ip>[^:]+:[^:]+): $ip::;
    default 0.0.0.0;
}

log_format anonymized '$anonymized_ip - $remote_user [$time_local] '
                      '"$request" $status $body_bytes_sent '
                      '"$http_referer" "$http_user_agent"';

access_log /var/log/nginx/access.log anonymized;

Compliance Considerations

GDPR compliance:

  • Anonymize or pseudonymize IP addresses
  • Implement data retention policies
  • Secure log storage and access
  • Document log processing activities

PCI DSS compliance:

  • Log all access to cardholder data
  • Implement log retention (minimum 1 year)
  • Secure log storage with access controls
  • Regular log reviews and monitoring

Troubleshooting

Logs Not Being Created

Check permissions:

# Verify log directory permissions
ls -la /var/log/nginx
ls -la /var/log/apache2

# Fix permissions
sudo mkdir -p /var/log/nginx
sudo chown www-data:adm /var/log/nginx
sudo chmod 755 /var/log/nginx

Check configuration:

# Test Apache configuration
sudo apache2ctl configtest

# Test Nginx configuration
sudo nginx -t

# Check if logs are defined
sudo apache2ctl -S | grep -i log
sudo nginx -T | grep -i log

Disk Space Issues

Monitor disk usage:

# Check disk usage
df -h

# Check log directory size
du -sh /var/log/nginx
du -sh /var/log/apache2

# Find largest log files
find /var/log -type f -size +100M -exec ls -lh {} \;

Clean up old logs:

# Manually delete old compressed logs
sudo find /var/log/nginx -name "*.gz" -mtime +30 -delete
sudo find /var/log/apache2 -name "*.gz" -mtime +30 -delete

# Truncate current log (use with caution)
sudo truncate -s 0 /var/log/nginx/access.log

Log Rotation Not Working

Debug logrotate:

# Run logrotate in debug mode
sudo logrotate -d /etc/logrotate.d/nginx

# Force rotation
sudo logrotate -f /etc/logrotate.d/nginx

# Check logrotate status
cat /var/lib/logrotate/status

# Check for errors
sudo journalctl -u logrotate

Best Practices

  1. Implement log rotation: Prevent disk space exhaustion
  2. Monitor log sizes: Set up alerts for unusual growth
  3. Use structured logging: JSON format for easier parsing
  4. Centralize logs: Send to dedicated log management system
  5. Set appropriate retention: Balance compliance and storage costs
  6. Secure log access: Restrict permissions and implement access controls
  7. Regular analysis: Review logs for security and performance issues
  8. Automate alerting: Set up notifications for critical errors
  9. Document log formats: Maintain documentation of custom formats
  10. Test log configuration: Verify logging after configuration changes

Conclusion

Effective web server log management is crucial for maintaining operational visibility, troubleshooting issues, ensuring security, and meeting compliance requirements. By properly configuring Apache and Nginx logging, implementing robust log rotation strategies, and utilizing powerful analysis tools, you can extract valuable insights from your web server logs while managing storage efficiently.

Key takeaways:

  • Configure appropriately: Customize log formats for your specific needs
  • Rotate regularly: Implement automated log rotation to manage disk space
  • Analyze systematically: Use command-line tools and specialized software for log analysis
  • Centralize when possible: Send logs to centralized systems for better visibility
  • Secure logs: Implement proper permissions and anonymization where required
  • Monitor continuously: Set up automated monitoring and alerting

Logs are your primary source of truth for understanding web server behavior, diagnosing issues, and detecting security threats. Invest time in proper log configuration and analysis—it will pay dividends when troubleshooting production issues or investigating security incidents.