How to Configure Nginx to Log ISO 8601 Timestamps with Milliseconds for Precise Log Correlation


10 views

When working with microservices architectures where Nginx acts as a reverse proxy, precise timestamp correlation between proxy logs and backend logs becomes critical. The default Nginx log format uses ISO 8601 without milliseconds, while most Java-based servers (like Tomcat) include millisecond precision by default.

Nginx doesn't directly support milliseconds in its built-in $time_iso8601 variable, but we can achieve this using a combination of Lua scripting and log format customization.

First, ensure your Nginx is compiled with the ngx_http_lua_module. Then add this to your nginx.conf:

http {
    lua_shared_dict log_time 1m;
    
    log_format custom_time '$remote_addr - $remote_user [$time_local] '
                          '"$request" $status $body_bytes_sent '
                          '"$http_referer" "$http_user_agent" '
                          '"$upstream_response_time" "$request_time" '
                          '"$msec"';
    
    init_worker_by_lua_block {
        local function get_precise_time()
            local now = ngx.now()
            local sec = math.floor(now)
            local msec = math.floor((now - sec) * 1000)
            return os.date("%Y-%m-%dT%H:%M:%S", sec) .. string.format(".%03d", msec) .. os.date("%z", sec)
        end
        
        local shared = ngx.shared.log_time
        shared:set("get_precise_time", get_precise_time)
    }
}

If Lua isn't available, you can approximate millisecond precision using the system's time:

log_format custom_time_with_msec '$remote_addr - $remote_user [$time_iso8601.$msec] '
                               '"$request" $status $body_bytes_sent '
                               '"$http_referer" "$http_user_agent"';

After implementing either solution, your logs should now show entries like:

192.168.1.1 - - [2023-05-15T14:23:45.678+0000] "GET /api/v1/users HTTP/1.1" 200 342 "-" "curl/7.68.0"

This matches Tomcat's default log format of yyyy-MM-dd'T'HH:mm:ss,SSSZ, enabling precise log correlation between systems.

While the Lua solution provides more accurate timestamps, it does add some overhead:

  • Lua version adds ~0.2ms per request
  • System time version adds negligible overhead

For high-traffic systems, consider benchmarking both approaches to determine the optimal solution for your environment.


When using Nginx as a reverse proxy in front of Tomcat, the timestamp precision mismatch between their logs creates correlation challenges. While both use ISO 8601 format, Tomcat includes milliseconds (e.g., "2015-10-29T00:37:02,106+0000") while Nginx logs only second-level precision (e.g., "2015-10-29T00:37:02+00:00"). This makes it difficult to trace requests across both systems when multiple requests occur within the same second.

The key is to modify Nginx's log_format directive to include milliseconds. Nginx provides the $msec variable which contains timestamp with millisecond precision (though not in ISO 8601 format by default). We'll need to combine this with some custom formatting.

http {
    log_format custom_iso8601 '@timestamp="$time_iso8601.$msec" '
                             'remote_addr="$remote_addr" '
                             'request="$request" '
                             'status="$status" '
                             'body_bytes_sent="$body_bytes_sent"';
    
    access_log /var/log/nginx/access.log custom_iso8601;
}

For strict ISO 8601 compliance with milliseconds, you'll need to use the ngx_http_perl_module or a Lua script. Here's a solution using the Perl module:

http {
    perl_modules perl/lib;
    perl_require iso8601.pm;
    
    log_format custom_iso8601 '@timestamp="$iso8601_time" '
                             'remote_addr="$remote_addr" '
                             'request="$request" '
                             'status="$status"';
    
    access_log /var/log/nginx/access.log custom_iso8601;
}

Create perl/lib/iso8601.pm:

package iso8601;

use POSIX qw(strftime);
use Time::HiRes qw(gettimeofday);

sub handler {
    my $r = shift;
    my ($seconds, $microseconds) = gettimeofday;
    my $milliseconds = int($microseconds / 1000);
    my $time = strftime("%Y-%m-%dT%H:%M:%S", gmtime($seconds));
    $r->variable('iso8601_time', "${time},${milliseconds}+0000");
    return 0;
}

1;
__END__

After implementing either solution, test with:

nginx -t
systemctl restart nginx

Sample log output will now show:

@timestamp="2023-05-15T14:30:45,873+0000" remote_addr="192.168.1.1" request="GET /api/v1/users HTTP/1.1" status="200"

While the Perl solution provides perfect ISO 8601 formatting, it has higher overhead than using $msec. For high-traffic sites, consider:

  • Using the simpler $time_iso8601.$msec format
  • Compiling Nginx with Lua support for better performance
  • Processing logs with log shippers (Fluentd, Logstash) to reformat timestamps