From 9b8add58f3c8ec0e125ecdb1bc4e3011e6e47e93 Mon Sep 17 00:00:00 2001 From: Vadim Zhestikov Date: Wed, 25 Mar 2026 10:18:06 -0700 Subject: [PATCH 1/8] Core: add RFC 5424 syslog protocol version parameter. Add "rfc=rfc3164|rfc5424" to the syslog directive. The default remains rfc3164 for backward compatibility. Tag validation is moved from ngx_syslog_parse_args() to ngx_syslog_process_conf() so that the character set and length constraints can be conditioned on the chosen protocol version. RFC 5424 (APP-NAME) allows up to 48 printable US-ASCII characters (0x21-0x7E), while RFC 3164 (TAG) restricts to alphanumeric characters and underscore with a 32-character limit. --- src/core/ngx_syslog.c | 97 +++++++++++++++++++++++++++++++++++-------- src/core/ngx_syslog.h | 2 + 2 files changed, 82 insertions(+), 17 deletions(-) diff --git a/src/core/ngx_syslog.c b/src/core/ngx_syslog.c index bad45bd16..cb197bb5d 100644 --- a/src/core/ngx_syslog.c +++ b/src/core/ngx_syslog.c @@ -49,6 +49,56 @@ ngx_syslog_process_conf(ngx_conf_t *cf, ngx_syslog_peer_t *peer) return NGX_CONF_ERROR; } + if (peer->tag.data != NULL) { + ngx_uint_t j; + u_char ch; + + if (peer->rfc5424) { + + /* + * RFC 5424: APP-NAME is at most 48 printable US-ASCII + * characters (0x21-0x7E; space and controls are excluded). + */ + for (j = 0; j < peer->tag.len; j++) { + ch = peer->tag.data[j]; + + if (ch < '!' || ch > '~') { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "syslog \"tag\" must contain " + "printable US-ASCII characters"); + return NGX_CONF_ERROR; + } + } + + } else { + + /* + * RFC 3164: the TAG is a string of ABNF alphanumeric + * characters that MUST NOT exceed 32 characters. + */ + if (peer->tag.len > 32) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "syslog tag length exceeds 32"); + return NGX_CONF_ERROR; + } + + for (j = 0; j < peer->tag.len; j++) { + ch = ngx_tolower(peer->tag.data[j]); + + if (ch < '0' + || (ch > '9' && ch < 'a' && ch != '_') + || ch > 'z') + { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "syslog \"tag\" only allows " + "alphanumeric characters " + "and underscore"); + return NGX_CONF_ERROR; + } + } + } + } + if (peer->server.sockaddr == NULL) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "no syslog server specified"); @@ -92,7 +142,7 @@ ngx_syslog_process_conf(ngx_conf_t *cf, ngx_syslog_peer_t *peer) static char * ngx_syslog_parse_args(ngx_conf_t *cf, ngx_syslog_peer_t *peer) { - u_char *p, *comma, c; + u_char *p, *comma; size_t len; ngx_str_t *value; ngx_url_t u; @@ -188,30 +238,43 @@ ngx_syslog_parse_args(ngx_conf_t *cf, ngx_syslog_peer_t *peer) } /* - * RFC 3164: the TAG is a string of ABNF alphanumeric characters - * that MUST NOT exceed 32 characters. + * Character set and maximum length depend on the syslog + * protocol version (rfc= parameter) and are validated in + * ngx_syslog_process_conf() once all parameters are known. + * RFC 5424 APP-NAME allows up to 48 printable US-ASCII + * characters; RFC 3164 TAG allows up to 32 alphanumeric + * characters. Reject anything that exceeds the larger + * of the two limits here so that the later check can rely + * on the data fitting into the statically-sized buffers. */ - if (len - 4 > 32) { + if (len - 4 > 48) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, - "syslog tag length exceeds 32"); + "syslog tag length exceeds 48"); return NGX_CONF_ERROR; } - for (i = 4; i < len; i++) { - c = ngx_tolower(p[i]); - - if (c < '0' || (c > '9' && c < 'a' && c != '_') || c > 'z') { - ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, - "syslog \"tag\" only allows " - "alphanumeric characters " - "and underscore"); - return NGX_CONF_ERROR; - } - } - peer->tag.data = p + 4; peer->tag.len = len - 4; + } else if (ngx_strncmp(p, "rfc=", 4) == 0) { + + if (peer->rfc_set) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "duplicate syslog \"rfc\""); + return NGX_CONF_ERROR; + } + peer->rfc_set = 1; + + if (ngx_strcmp(p + 4, "rfc5424") == 0) { + peer->rfc5424 = 1; + + } else if (ngx_strcmp(p + 4, "rfc3164") != 0) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "unknown syslog \"rfc\" value \"%s\"", + p + 4); + return NGX_CONF_ERROR; + } + } else if (len == 10 && ngx_strncmp(p, "nohostname", 10) == 0) { peer->nohostname = 1; diff --git a/src/core/ngx_syslog.h b/src/core/ngx_syslog.h index e2d54acdb..3cc0fbe56 100644 --- a/src/core/ngx_syslog.h +++ b/src/core/ngx_syslog.h @@ -23,6 +23,8 @@ typedef struct { unsigned busy:1; unsigned nohostname:1; + unsigned rfc5424:1; + unsigned rfc_set:1; } ngx_syslog_peer_t; From b3819741a3d73b6662f4fcf2bedd49ef944f9195 Mon Sep 17 00:00:00 2001 From: Vadim Zhestikov Date: Wed, 25 Mar 2026 10:24:04 -0700 Subject: [PATCH 2/8] Tests: add RFC 5424 syslog protocol version config parsing. Test that the "rfc=" parameter of the syslog directive accepts "rfc3164" and "rfc5424", and rejects unknown values. Test the protocol-version-dependent tag validation: RFC 5424 (APP-NAME) allows hyphenated/dotted tags up to 48 printable US-ASCII characters; RFC 3164 (TAG) restricts to alphanumeric characters and underscore with a 32-character limit. --- t/syslog_rfc5424_config.t | 110 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 t/syslog_rfc5424_config.t diff --git a/t/syslog_rfc5424_config.t b/t/syslog_rfc5424_config.t new file mode 100644 index 000000000..8c13b6044 --- /dev/null +++ b/t/syslog_rfc5424_config.t @@ -0,0 +1,110 @@ +#!/usr/bin/perl + +# (C) Nginx, Inc. + +# Tests for "rfc=" parameter of the syslog directive. +# Uses "nginx -t" config-check mode; no server is started. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +plan(skip_all => 'win32') if $^O eq 'MSWin32'; + +my $t = Test::Nginx->new(); + +############################################################################### + +# Write a minimal nginx.conf containing $syslog_param as a global error_log +# directive and run "nginx -t" against it. Returns combined stdout+stderr. + +sub config_check { + my ($syslog_param) = @_; + + $t->write_file_expand('nginx.conf', <<"EOF"); + +%%TEST_GLOBALS%% + +error_log $syslog_param info; + +daemon off; + +events { +} + +EOF + + my $testdir = $t->testdir(); + return qx{$Test::Nginx::NGINX -t -p $testdir/ -c nginx.conf 2>&1}; +} + +############################################################################### + +my $out; + +# rfc=rfc3164 is the default and must be accepted explicitly. + +$out = config_check('syslog:server=127.0.0.1:5140,rfc=rfc3164'); +like($out, qr/test is successful/i, 'rfc=rfc3164 accepted'); + +# rfc=rfc5424 must be accepted. + +$out = config_check('syslog:server=127.0.0.1:5140,rfc=rfc5424'); +like($out, qr/test is successful/i, 'rfc=rfc5424 accepted'); + +# An unknown rfc= value must be rejected with a descriptive error. + +$out = config_check('syslog:server=127.0.0.1:5140,rfc=rfc9999'); +like($out, qr/unknown syslog "rfc" value/, 'rfc=rfc9999 rejected'); + +# A hyphenated tag is printable ASCII and must be accepted with rfc5424. + +$out = config_check('syslog:server=127.0.0.1:5140,rfc=rfc5424,tag=my-app'); +like($out, qr/test is successful/i, 'rfc5424: hyphenated tag accepted'); + +# The same hyphenated tag must be rejected with rfc3164. + +$out = config_check('syslog:server=127.0.0.1:5140,rfc=rfc3164,tag=my-app'); +like($out, qr/only allows alphanumeric/, 'rfc3164: hyphenated tag rejected'); + +# A tag with a dot is printable ASCII and must be accepted with rfc5424. + +$out = config_check('syslog:server=127.0.0.1:5140,rfc=rfc5424,tag=nginx.1'); +like($out, qr/test is successful/i, 'rfc5424: dot in tag accepted'); + +# A 33-character tag is within the rfc5424 limit (48) and must be accepted. + +my $tag33 = 'a' x 33; +$out = config_check("syslog:server=127.0.0.1:5140,rfc=rfc5424,tag=$tag33"); +like($out, qr/test is successful/i, 'rfc5424: 33-char tag accepted'); + +# The same 33-character tag exceeds the rfc3164 limit (32) and must fail. + +$out = config_check("syslog:server=127.0.0.1:5140,rfc=rfc3164,tag=$tag33"); +like($out, qr/tag length exceeds 32/, 'rfc3164: 33-char tag rejected'); + +# A 49-character tag exceeds the maximum of both protocol versions (48). + +my $tag49 = 'a' x 49; +$out = config_check("syslog:server=127.0.0.1:5140,rfc=rfc5424,tag=$tag49"); +like($out, qr/tag length exceeds 48/, 'rfc5424: 49-char tag rejected'); + +$out = config_check("syslog:server=127.0.0.1:5140,rfc=rfc3164,tag=$tag49"); +like($out, qr/tag length exceeds 48/, 'rfc3164: 49-char tag rejected'); + +done_testing; + +############################################################################### From 13cc07acb867b652c50b6e71484acdffedd79dff Mon Sep 17 00:00:00 2001 From: Vadim Zhestikov Date: Wed, 25 Mar 2026 10:25:43 -0700 Subject: [PATCH 3/8] Core: add RFC 5424 syslog message format. When the "rfc=rfc5424" syslog parameter is used, format syslog messages according to RFC 5424 instead of RFC 3164. The RFC 5424 HEADER contains VERSION (always "1"), an ISO 8601 TIMESTAMP with millisecond precision and UTC offset (e.g. "2003-10-11T22:14:15.003+05:30"), HOSTNAME, APP-NAME, PROCID (nginx process PID), and nil values for MSGID and STRUCTURED-DATA. Add cached_syslog_rfc5424_time to ngx_times.c populated alongside the existing cached_syslog_time in both ngx_time_update() and ngx_time_sigsafe_update(). The signal-safe path has no millisecond counter available and emits ".000" for the sub-second field. Expand NGX_SYSLOG_MAX_STR to accommodate the larger RFC 5424 header. --- src/core/ngx_syslog.c | 31 +++++++++++++++++++++++++++---- src/core/ngx_times.c | 30 ++++++++++++++++++++++++++++-- src/core/ngx_times.h | 1 + 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/core/ngx_syslog.c b/src/core/ngx_syslog.c index cb197bb5d..122589c01 100644 --- a/src/core/ngx_syslog.c +++ b/src/core/ngx_syslog.c @@ -10,9 +10,11 @@ #define NGX_SYSLOG_MAX_STR \ - NGX_MAX_ERROR_STR + sizeof("<255>Jan 01 00:00:00 ") - 1 \ + NGX_MAX_ERROR_STR + sizeof("<255>1 ") - 1 \ + + sizeof("1970-09-28T12:00:00.000+06:00") - 1 + 1 /* space */ \ + (NGX_MAXHOSTNAMELEN - 1) + 1 /* space */ \ - + 32 /* tag */ + 2 /* colon, space */ + + 48 /* APP-NAME/TAG */ + 1 /* space */ \ + + NGX_INT64_LEN /* PROCID */ + sizeof(" - - ") - 1 static char *ngx_syslog_parse_args(ngx_conf_t *cf, ngx_syslog_peer_t *peer); @@ -40,6 +42,8 @@ static ngx_event_t ngx_syslog_dummy_event; char * ngx_syslog_process_conf(ngx_conf_t *cf, ngx_syslog_peer_t *peer) { + u_char ch; + ngx_uint_t j; ngx_pool_cleanup_t *cln; peer->facility = NGX_CONF_UNSET_UINT; @@ -50,8 +54,6 @@ ngx_syslog_process_conf(ngx_conf_t *cf, ngx_syslog_peer_t *peer) } if (peer->tag.data != NULL) { - ngx_uint_t j; - u_char ch; if (peer->rfc5424) { @@ -304,6 +306,27 @@ ngx_syslog_add_header(ngx_syslog_peer_t *peer, u_char *buf) pri = peer->facility * 8 + peer->severity; + if (peer->rfc5424) { + + /* + * RFC 5424 HEADER: VERSION SP TIMESTAMP SP HOSTNAME SP + * APP-NAME SP PROCID SP MSGID SP STRUCTURED-DATA SP + * + * PROCID is set to the nginx process PID. MSGID and + * STRUCTURED-DATA are set to the nil value "-". + */ + + if (peer->nohostname) { + return ngx_sprintf(buf, "<%ui>1 %V - %V %P - - ", + pri, &ngx_cached_syslog_rfc5424_time, + &peer->tag, ngx_pid); + } + + return ngx_sprintf(buf, "<%ui>1 %V %V %V %P - - ", + pri, &ngx_cached_syslog_rfc5424_time, + peer->hostname, &peer->tag, ngx_pid); + } + if (peer->nohostname) { return ngx_sprintf(buf, "<%ui>%V %V: ", pri, &ngx_cached_syslog_time, &peer->tag); diff --git a/src/core/ngx_times.c b/src/core/ngx_times.c index b0057d2ab..585674cbf 100644 --- a/src/core/ngx_times.c +++ b/src/core/ngx_times.c @@ -33,6 +33,7 @@ volatile ngx_str_t ngx_cached_http_time; volatile ngx_str_t ngx_cached_http_log_time; volatile ngx_str_t ngx_cached_http_log_iso8601; volatile ngx_str_t ngx_cached_syslog_time; +volatile ngx_str_t ngx_cached_syslog_rfc5424_time; #if !(NGX_WIN32) @@ -56,6 +57,8 @@ static u_char cached_http_log_iso8601[NGX_TIME_SLOTS] [sizeof("1970-09-28T12:00:00+06:00")]; static u_char cached_syslog_time[NGX_TIME_SLOTS] [sizeof("Sep 28 12:00:00")]; +static u_char cached_syslog_rfc5424_time[NGX_TIME_SLOTS] + [sizeof("1970-09-28T12:00:00.000+06:00")]; static char *week[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; @@ -70,6 +73,8 @@ ngx_time_init(void) ngx_cached_http_log_time.len = sizeof("28/Sep/1970:12:00:00 +0600") - 1; ngx_cached_http_log_iso8601.len = sizeof("1970-09-28T12:00:00+06:00") - 1; ngx_cached_syslog_time.len = sizeof("Sep 28 12:00:00") - 1; + ngx_cached_syslog_rfc5424_time.len = + sizeof("1970-09-28T12:00:00.000+06:00") - 1; ngx_cached_time = &cached_time[0]; @@ -80,7 +85,7 @@ ngx_time_init(void) void ngx_time_update(void) { - u_char *p0, *p1, *p2, *p3, *p4; + u_char *p0, *p1, *p2, *p3, *p4, *p5; ngx_tm_t tm, gmt; time_t sec; ngx_uint_t msec; @@ -179,6 +184,15 @@ ngx_time_update(void) months[tm.ngx_tm_mon - 1], tm.ngx_tm_mday, tm.ngx_tm_hour, tm.ngx_tm_min, tm.ngx_tm_sec); + p5 = &cached_syslog_rfc5424_time[slot][0]; + + (void) ngx_sprintf(p5, "%4d-%02d-%02dT%02d:%02d:%02d.%03ui%c%02i:%02i", + tm.ngx_tm_year, tm.ngx_tm_mon, + tm.ngx_tm_mday, tm.ngx_tm_hour, + tm.ngx_tm_min, tm.ngx_tm_sec, msec, + tp->gmtoff < 0 ? '-' : '+', + ngx_abs(tp->gmtoff / 60), ngx_abs(tp->gmtoff % 60)); + ngx_memory_barrier(); ngx_cached_time = tp; @@ -187,6 +201,7 @@ ngx_time_update(void) ngx_cached_http_log_time.data = p2; ngx_cached_http_log_iso8601.data = p3; ngx_cached_syslog_time.data = p4; + ngx_cached_syslog_rfc5424_time.data = p5; ngx_unlock(&ngx_time_lock); } @@ -214,7 +229,7 @@ ngx_monotonic_time(time_t sec, ngx_uint_t msec) void ngx_time_sigsafe_update(void) { - u_char *p, *p2; + u_char *p, *p2, *p3; ngx_tm_t tm; time_t sec; ngx_time_t *tp; @@ -260,10 +275,21 @@ ngx_time_sigsafe_update(void) months[tm.ngx_tm_mon - 1], tm.ngx_tm_mday, tm.ngx_tm_hour, tm.ngx_tm_min, tm.ngx_tm_sec); + p3 = &cached_syslog_rfc5424_time[slot][0]; + + (void) ngx_sprintf(p3, "%4d-%02d-%02dT%02d:%02d:%02d.000%c%02i:%02i", + tm.ngx_tm_year, tm.ngx_tm_mon, + tm.ngx_tm_mday, tm.ngx_tm_hour, + tm.ngx_tm_min, tm.ngx_tm_sec, + cached_gmtoff < 0 ? '-' : '+', + ngx_abs(cached_gmtoff / 60), + ngx_abs(cached_gmtoff % 60)); + ngx_memory_barrier(); ngx_cached_err_log_time.data = p; ngx_cached_syslog_time.data = p2; + ngx_cached_syslog_rfc5424_time.data = p3; ngx_unlock(&ngx_time_lock); } diff --git a/src/core/ngx_times.h b/src/core/ngx_times.h index 49e0a8c48..4d6142fe9 100644 --- a/src/core/ngx_times.h +++ b/src/core/ngx_times.h @@ -41,6 +41,7 @@ extern volatile ngx_str_t ngx_cached_http_time; extern volatile ngx_str_t ngx_cached_http_log_time; extern volatile ngx_str_t ngx_cached_http_log_iso8601; extern volatile ngx_str_t ngx_cached_syslog_time; +extern volatile ngx_str_t ngx_cached_syslog_rfc5424_time; /* * milliseconds elapsed since some unspecified point in the past From 366d275785f8f8c17cb97dbbbe0833cc0ae8cb5f Mon Sep 17 00:00:00 2001 From: Vadim Zhestikov Date: Wed, 25 Mar 2026 10:28:32 -0700 Subject: [PATCH 4/8] Tests: add RFC 5424 syslog message format. Test all RFC 5424 HEADER fields in an access_log syslog message: PRI, VERSION ("1"), ISO 8601 timestamp with milliseconds and UTC offset, HOSTNAME, APP-NAME (printable US-ASCII), PROCID (decimal integer PID), nil MSGID, nil STRUCTURED-DATA, and non-empty MSG. Additionally test: - nohostname: HOSTNAME is the nil value "-" - Custom hyphenated tag (valid in APP-NAME per RFC 5424) - facility=user encoded as facility 1 in the PRI field - Global error_log using rfc5424 produces messages with VERSION "1" - Default (rfc3164) format is unchanged: BSD timestamp, no VERSION field --- t/syslog_rfc5424.t | 244 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 t/syslog_rfc5424.t diff --git a/t/syslog_rfc5424.t b/t/syslog_rfc5424.t new file mode 100644 index 000000000..74c982ecb --- /dev/null +++ b/t/syslog_rfc5424.t @@ -0,0 +1,244 @@ +#!/usr/bin/perl + +# (C) Nginx, Inc. + +# Tests for RFC 5424 syslog message format. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use IO::Select; +use IO::Socket::INET; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +plan(skip_all => 'win32') if $^O eq 'MSWin32'; + +my $t = Test::Nginx->new()->has(qw/http/)->plan(20); + +############################################################################### + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +error_log syslog:server=127.0.0.1:%%PORT_8981_UDP%%,rfc=rfc5424 info; + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + log_format logf "$uri:$status"; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + # RFC 5424 access log — basic format check + location /a5424 { + access_log syslog:server=127.0.0.1:%%PORT_8982_UDP%%,rfc=rfc5424 + logf; + } + + # RFC 5424 nohostname — HOSTNAME must be the nil value "-" + location /nohostname5424 { + access_log + syslog:server=127.0.0.1:%%PORT_8982_UDP%%,rfc=rfc5424,nohostname + logf; + } + + # RFC 5424 error log with custom tag (hyphen) and facility + location /e5424 { + error_log + syslog:server=127.0.0.1:%%PORT_8982_UDP%%,rfc=rfc5424,tag=my-app,facility=user; + } + + # RFC 3164 access log — verify backward compatibility + location /a3164 { + access_log syslog:server=127.0.0.1:%%PORT_8983_UDP%% logf; + } + } +} + +EOF + +# Port 8981: background daemon that writes global error_log messages to a file. +# Port 8982: the test binds this socket directly after run(). +# Port 8983: the test binds this socket directly after run(). + +$t->run_daemon(\&syslog_daemon, port(8981), $t, 's_glob.log'); +$t->waitforfile($t->testdir() . '/s_glob.log'); + +$t->run(); + +############################################################################### + +my $s5424 = IO::Socket::INET->new( + Proto => 'udp', + LocalAddr => '127.0.0.1:' . port(8982) +) or die "Can't open syslog socket (8982): $!"; + +my $s3164 = IO::Socket::INET->new( + Proto => 'udp', + LocalAddr => '127.0.0.1:' . port(8983) +) or die "Can't open syslog socket (8983): $!"; + +############################################################################### + +# RFC 5424 access log — full field-by-field check + +parse_rfc5424_message('access_log', get_syslog($s5424, '/a5424')); + +# RFC 5424 with nohostname — HOSTNAME field must be nil "-" + +my $msg = get_syslog($s5424, '/nohostname5424'); +like($msg, + qr/^<\d+>1\s # PRI + VERSION + \S+\s # TIMESTAMP + -\s # HOSTNAME = nil "-" + \S+\s # APP-NAME + \d+\s # PROCID + -\s-\s/x, + 'rfc5424 nohostname: HOSTNAME is nil "-"'); + +# RFC 5424 error log — custom hyphenated tag (only valid in rfc5424) + +$msg = get_syslog($s5424, '/e5424'); +like($msg, qr/my-app/, 'rfc5424: hyphenated tag present in APP-NAME'); + +# RFC 5424 facility=user must be encoded as facility 1 in PRI + +my ($pri) = $msg =~ /^<(\d+)>/; +my $fac = ($pri & 0x03f8) >> 3; +is($fac, 1, 'rfc5424: facility=user (1) encoded in PRI'); + +# Global error_log uses rfc5424 — check via background-daemon log file + +http_get('/a5424'); + +my $glob = ''; +for (1 .. 50) { + select undef, undef, undef, 0.1; + $glob = $t->read_file('s_glob.log'); + last if $glob; +} +like($glob, qr/^<\d+>1\s/m, 'rfc5424 global error_log: VERSION "1" present'); + +# RFC 3164 format is unchanged (backward compatibility) + +$msg = get_syslog($s3164, '/a3164'); +like($msg, + qr/^<\d+> # PRI (no VERSION field) + [A-Z][a-z]{2}\s # mon (BSD syslog timestamp) + [ \d]\d\s\d{2}:\d{2}:\d{2}\s # day HH:MM:SS + /x, + 'rfc3164: BSD syslog timestamp unchanged'); +unlike($msg, qr/^<\d+>1\s/, 'rfc3164: no VERSION field'); + +############################################################################### + +sub get_syslog { + my ($sock, $uri) = @_; + my $data = ''; + + http_get($uri); + + IO::Select->new($sock)->can_read(1); + while (IO::Select->new($sock)->can_read(0.1)) { + my $buf; + sysread($sock, $buf, 4096); + $data .= $buf; + } + return $data; +} + +# Validate that $line looks like a valid RFC 5424 message and run +# 14 individual Test::More assertions, one per protocol field. + +sub parse_rfc5424_message { + my ($desc, $line) = @_; + + unless ($line) { + fail("$desc: no syslog message received"); + return; + } + + # RFC 5424 SYSLOG-MSG: + # VERSION SP TIMESTAMP SP HOSTNAME SP APP-NAME SP + # PROCID SP MSGID SP STRUCTURED-DATA SP MSG + + my ($pri, $ts, $host, $app, $pid, $msgid, $sd, $msg) = + $line =~ /^<(\d{1,3})> # PRI + 1\s # VERSION (literal "1") + (\S+)\s # TIMESTAMP + (\S+)\s # HOSTNAME + (\S+)\s # APP-NAME + (\S+)\s # PROCID + (\S+)\s # MSGID + (\S+)\s # STRUCTURED-DATA + (.*)/x; # MSG + + ok(defined($pri), "$desc: has PRI"); + + my $sev = $pri & 0x07; + my $fac = ($pri & 0x03f8) >> 3; + ok($sev >= 0 && $sev <= 7, "$desc: severity in PRI is 0-7"); + ok($fac >= 0 && $fac < 24, "$desc: facility in PRI is 0-23"); + + ok(defined($ts), "$desc: has TIMESTAMP"); + like($ts, + qr/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}$/, + "$desc: TIMESTAMP is ISO 8601 with ms and tz offset"); + + ok(defined($host), "$desc: has HOSTNAME"); + ok(length($host) > 0 && $host ne '-', "$desc: HOSTNAME is non-nil"); + + ok(defined($app), "$desc: has APP-NAME"); + like($app, qr/^[!-~]+$/, "$desc: APP-NAME is printable US-ASCII"); + + ok(defined($pid), "$desc: has PROCID"); + like($pid, qr/^\d+$/, "$desc: PROCID is a decimal integer"); + + is($msgid, '-', "$desc: MSGID is nil"); + is($sd, '-', "$desc: STRUCTURED-DATA is nil"); + + ok(defined($msg) && length($msg) > 0, "$desc: MSG is non-empty"); +} + +############################################################################### + +sub syslog_daemon { + my ($port, $t, $file) = @_; + + my $s = IO::Socket::INET->new( + Proto => 'udp', + LocalAddr => "127.0.0.1:$port" + ); + + open my $fh, '>', $t->testdir() . '/' . $file; + select $fh; $| = 1; + + while (1) { + my $buffer; + $s->recv($buffer, 4096); + print $fh $buffer . "\n"; + } +} + +############################################################################### From d92ee133cf559aaef3c3fedfd06a1e2ea7b1a511 Mon Sep 17 00:00:00 2001 From: Vadim Zhestikov Date: Wed, 25 Mar 2026 10:37:35 -0700 Subject: [PATCH 5/8] Core: add RFC 5424 syslog millisecond-accurate timestamp. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the per-second cached_syslog_rfc5424_time slot array that was added in the previous commit. The cached approach would populate the millisecond field with the value at second rollover (usually near 0), defeating the sub-second precision that RFC 5424 was requested for. Instead, in ngx_syslog_add_header(), build the RFC 5424 TIMESTAMP field inline: - The date, time, and UTC offset are read from ngx_cached_http_log_iso8601 ("YYYY-MM-DDTHH:MM:SS±HH:MM"), splitting the 25-byte string at position 19. - The millisecond sub-field is taken from ngx_timeofday()->msec, which is updated on every event-loop tick by ngx_time_update(). This produces timestamps accurate to the resolution of the event loop (typically 1 ms) at negligible per-message cost. --- src/core/ngx_syslog.c | 39 +++++++++++++++++++++++++++++++-------- src/core/ngx_times.c | 30 ++---------------------------- src/core/ngx_times.h | 1 - 3 files changed, 33 insertions(+), 37 deletions(-) diff --git a/src/core/ngx_syslog.c b/src/core/ngx_syslog.c index 122589c01..4f74afa24 100644 --- a/src/core/ngx_syslog.c +++ b/src/core/ngx_syslog.c @@ -11,7 +11,7 @@ #define NGX_SYSLOG_MAX_STR \ NGX_MAX_ERROR_STR + sizeof("<255>1 ") - 1 \ - + sizeof("1970-09-28T12:00:00.000+06:00") - 1 + 1 /* space */ \ + + sizeof("1970-09-28T12:00:00.000+06:00") - 1 + 1 /* space */ \ + (NGX_MAXHOSTNAMELEN - 1) + 1 /* space */ \ + 48 /* APP-NAME/TAG */ + 1 /* space */ \ + NGX_INT64_LEN /* PROCID */ + sizeof(" - - ") - 1 @@ -302,7 +302,9 @@ ngx_syslog_parse_args(ngx_conf_t *cf, ngx_syslog_peer_t *peer) u_char * ngx_syslog_add_header(ngx_syslog_peer_t *peer, u_char *buf) { - ngx_uint_t pri; + ngx_uint_t pri; + ngx_str_t datetime, tz; + ngx_time_t *tp; pri = peer->facility * 8 + peer->severity; @@ -312,18 +314,39 @@ ngx_syslog_add_header(ngx_syslog_peer_t *peer, u_char *buf) * RFC 5424 HEADER: VERSION SP TIMESTAMP SP HOSTNAME SP * APP-NAME SP PROCID SP MSGID SP STRUCTURED-DATA SP * - * PROCID is set to the nginx process PID. MSGID and - * STRUCTURED-DATA are set to the nil value "-". + * TIMESTAMP is formatted as an ISO 8601 date-time with + * millisecond precision and UTC offset, e.g.: + * 2003-10-11T22:14:15.003+05:30 + * + * The date, time, and UTC offset are taken from the + * ngx_cached_http_log_iso8601 cache ("YYYY-MM-DDTHH:MM:SS±HH:MM", + * 25 bytes). The first 19 bytes are "YYYY-MM-DDTHH:MM:SS" + * and the trailing 6 bytes are "±HH:MM". The millisecond + * field is read live from ngx_timeofday() so that it reflects + * the current event-loop tick rather than the start of the + * current second. + * + * PROCID is the nginx process PID. MSGID and STRUCTURED-DATA + * are set to the nil value "-". */ + tp = ngx_timeofday(); + + datetime.data = ngx_cached_http_log_iso8601.data; + datetime.len = sizeof("1970-09-28T12:00:00") - 1; + + tz.data = ngx_cached_http_log_iso8601.data + + sizeof("1970-09-28T12:00:00") - 1; + tz.len = sizeof("+06:00") - 1; + if (peer->nohostname) { - return ngx_sprintf(buf, "<%ui>1 %V - %V %P - - ", - pri, &ngx_cached_syslog_rfc5424_time, + return ngx_sprintf(buf, "<%ui>1 %V.%03ui%V - %V %P - - ", + pri, &datetime, tp->msec, &tz, &peer->tag, ngx_pid); } - return ngx_sprintf(buf, "<%ui>1 %V %V %V %P - - ", - pri, &ngx_cached_syslog_rfc5424_time, + return ngx_sprintf(buf, "<%ui>1 %V.%03ui%V %V %V %P - - ", + pri, &datetime, tp->msec, &tz, peer->hostname, &peer->tag, ngx_pid); } diff --git a/src/core/ngx_times.c b/src/core/ngx_times.c index 585674cbf..b0057d2ab 100644 --- a/src/core/ngx_times.c +++ b/src/core/ngx_times.c @@ -33,7 +33,6 @@ volatile ngx_str_t ngx_cached_http_time; volatile ngx_str_t ngx_cached_http_log_time; volatile ngx_str_t ngx_cached_http_log_iso8601; volatile ngx_str_t ngx_cached_syslog_time; -volatile ngx_str_t ngx_cached_syslog_rfc5424_time; #if !(NGX_WIN32) @@ -57,8 +56,6 @@ static u_char cached_http_log_iso8601[NGX_TIME_SLOTS] [sizeof("1970-09-28T12:00:00+06:00")]; static u_char cached_syslog_time[NGX_TIME_SLOTS] [sizeof("Sep 28 12:00:00")]; -static u_char cached_syslog_rfc5424_time[NGX_TIME_SLOTS] - [sizeof("1970-09-28T12:00:00.000+06:00")]; static char *week[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; @@ -73,8 +70,6 @@ ngx_time_init(void) ngx_cached_http_log_time.len = sizeof("28/Sep/1970:12:00:00 +0600") - 1; ngx_cached_http_log_iso8601.len = sizeof("1970-09-28T12:00:00+06:00") - 1; ngx_cached_syslog_time.len = sizeof("Sep 28 12:00:00") - 1; - ngx_cached_syslog_rfc5424_time.len = - sizeof("1970-09-28T12:00:00.000+06:00") - 1; ngx_cached_time = &cached_time[0]; @@ -85,7 +80,7 @@ ngx_time_init(void) void ngx_time_update(void) { - u_char *p0, *p1, *p2, *p3, *p4, *p5; + u_char *p0, *p1, *p2, *p3, *p4; ngx_tm_t tm, gmt; time_t sec; ngx_uint_t msec; @@ -184,15 +179,6 @@ ngx_time_update(void) months[tm.ngx_tm_mon - 1], tm.ngx_tm_mday, tm.ngx_tm_hour, tm.ngx_tm_min, tm.ngx_tm_sec); - p5 = &cached_syslog_rfc5424_time[slot][0]; - - (void) ngx_sprintf(p5, "%4d-%02d-%02dT%02d:%02d:%02d.%03ui%c%02i:%02i", - tm.ngx_tm_year, tm.ngx_tm_mon, - tm.ngx_tm_mday, tm.ngx_tm_hour, - tm.ngx_tm_min, tm.ngx_tm_sec, msec, - tp->gmtoff < 0 ? '-' : '+', - ngx_abs(tp->gmtoff / 60), ngx_abs(tp->gmtoff % 60)); - ngx_memory_barrier(); ngx_cached_time = tp; @@ -201,7 +187,6 @@ ngx_time_update(void) ngx_cached_http_log_time.data = p2; ngx_cached_http_log_iso8601.data = p3; ngx_cached_syslog_time.data = p4; - ngx_cached_syslog_rfc5424_time.data = p5; ngx_unlock(&ngx_time_lock); } @@ -229,7 +214,7 @@ ngx_monotonic_time(time_t sec, ngx_uint_t msec) void ngx_time_sigsafe_update(void) { - u_char *p, *p2, *p3; + u_char *p, *p2; ngx_tm_t tm; time_t sec; ngx_time_t *tp; @@ -275,21 +260,10 @@ ngx_time_sigsafe_update(void) months[tm.ngx_tm_mon - 1], tm.ngx_tm_mday, tm.ngx_tm_hour, tm.ngx_tm_min, tm.ngx_tm_sec); - p3 = &cached_syslog_rfc5424_time[slot][0]; - - (void) ngx_sprintf(p3, "%4d-%02d-%02dT%02d:%02d:%02d.000%c%02i:%02i", - tm.ngx_tm_year, tm.ngx_tm_mon, - tm.ngx_tm_mday, tm.ngx_tm_hour, - tm.ngx_tm_min, tm.ngx_tm_sec, - cached_gmtoff < 0 ? '-' : '+', - ngx_abs(cached_gmtoff / 60), - ngx_abs(cached_gmtoff % 60)); - ngx_memory_barrier(); ngx_cached_err_log_time.data = p; ngx_cached_syslog_time.data = p2; - ngx_cached_syslog_rfc5424_time.data = p3; ngx_unlock(&ngx_time_lock); } diff --git a/src/core/ngx_times.h b/src/core/ngx_times.h index 4d6142fe9..49e0a8c48 100644 --- a/src/core/ngx_times.h +++ b/src/core/ngx_times.h @@ -41,7 +41,6 @@ extern volatile ngx_str_t ngx_cached_http_time; extern volatile ngx_str_t ngx_cached_http_log_time; extern volatile ngx_str_t ngx_cached_http_log_iso8601; extern volatile ngx_str_t ngx_cached_syslog_time; -extern volatile ngx_str_t ngx_cached_syslog_rfc5424_time; /* * milliseconds elapsed since some unspecified point in the past From 34df9a586ef6eeb80f59545018c1243bd59f1ce0 Mon Sep 17 00:00:00 2001 From: Vadim Zhestikov Date: Wed, 25 Mar 2026 10:41:38 -0700 Subject: [PATCH 6/8] Tests: add RFC 5424 syslog millisecond precision and stream module. Update syslog_rfc5424.t: add a test that sends several messages with 20 ms gaps and verifies that the millisecond field in the RFC 5424 timestamp is not permanently stuck at "000", confirming that the implementation reads the live ngx_timeofday()->msec value rather than a once-per-second cached value. Add syslog_rfc5424_stream.t: exercise the RFC 5424 syslog format via the stream access_log directive using the same per-field checks as the HTTP test (PRI, VERSION, ISO 8601 timestamp, HOSTNAME, APP-NAME, PROCID, nil MSGID, nil STRUCTURED-DATA, MSG). Also test that the "nohostname" parameter produces the RFC 5424 nil HOSTNAME "-". --- t/syslog_rfc5424.t | 16 +++- t/syslog_rfc5424_stream.t | 168 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 t/syslog_rfc5424_stream.t diff --git a/t/syslog_rfc5424.t b/t/syslog_rfc5424.t index 74c982ecb..724973785 100644 --- a/t/syslog_rfc5424.t +++ b/t/syslog_rfc5424.t @@ -26,7 +26,7 @@ select STDOUT; $| = 1; plan(skip_all => 'win32') if $^O eq 'MSWin32'; -my $t = Test::Nginx->new()->has(qw/http/)->plan(20); +my $t = Test::Nginx->new()->has(qw/http/)->plan(21); ############################################################################### @@ -140,6 +140,20 @@ for (1 .. 50) { } like($glob, qr/^<\d+>1\s/m, 'rfc5424 global error_log: VERSION "1" present'); +# Millisecond field is live (ngx_timeofday()->msec, not once-per-second cache). +# Send several messages with small gaps and verify that the ms field is not +# permanently stuck at "000" as it would be with a second-boundary-only cache. + +my @ms_vals; +for (1..10) { + select undef, undef, undef, 0.02; # 20 ms gap + my $m = get_syslog($s5424, '/a5424'); + my ($ms_field) = $m =~ /T\d{2}:\d{2}:\d{2}\.(\d{3})/; + push @ms_vals, $ms_field if defined $ms_field; +} +ok((grep { $_ ne '000' } @ms_vals) > 0, + 'rfc5424: millisecond field is not always zero (live ngx_timeofday)'); + # RFC 3164 format is unchanged (backward compatibility) $msg = get_syslog($s3164, '/a3164'); diff --git a/t/syslog_rfc5424_stream.t b/t/syslog_rfc5424_stream.t new file mode 100644 index 000000000..8b8a34bd5 --- /dev/null +++ b/t/syslog_rfc5424_stream.t @@ -0,0 +1,168 @@ +#!/usr/bin/perl + +# (C) Nginx, Inc. + +# Tests for RFC 5424 syslog format in the stream access_log directive. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use IO::Select; +use IO::Socket::INET; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +plan(skip_all => 'win32') if $^O eq 'MSWin32'; + +my $t = Test::Nginx->new()->has(qw/stream http/)->plan(15); + +############################################################################### + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location / { + return 200 ok; + } + } +} + +stream { + %%TEST_GLOBALS_STREAM%% + + log_format streamf "$remote_addr $protocol"; + + server { + listen 127.0.0.1:8081; + proxy_pass 127.0.0.1:8080; + access_log syslog:server=127.0.0.1:%%PORT_8982_UDP%%,rfc=rfc5424 + streamf; + } + + server { + listen 127.0.0.1:8082; + proxy_pass 127.0.0.1:8080; + access_log syslog:server=127.0.0.1:%%PORT_8982_UDP%%,rfc=rfc5424,nohostname + streamf; + } +} + +EOF + +$t->run(); + +############################################################################### + +my $s5424 = IO::Socket::INET->new( + Proto => 'udp', + LocalAddr => '127.0.0.1:' . port(8982) +) or die "Can't open syslog socket: $!"; + +# RFC 5424 stream access log — full field-by-field check + +stream('127.0.0.1:' . port(8081))->read(); +parse_rfc5424_message('stream access_log', get_syslog_raw($s5424)); + +# RFC 5424 stream with nohostname + +stream('127.0.0.1:' . port(8082))->read(); +my $msg = get_syslog_raw($s5424); +like($msg, + qr/^<\d+>1\s # PRI + VERSION + \S+\s # TIMESTAMP + -\s # HOSTNAME = nil "-" + \S+\s # APP-NAME + \d+\s # PROCID + -\s-\s/x, + 'stream rfc5424 nohostname: HOSTNAME is nil "-"'); + +############################################################################### + +sub get_syslog_raw { + my ($sock) = @_; + my $data = ''; + + IO::Select->new($sock)->can_read(2); + while (IO::Select->new($sock)->can_read(0.1)) { + my $buf; + sysread($sock, $buf, 4096); + $data .= $buf; + } + return $data; +} + +# Validate all RFC 5424 header fields; runs 14 assertions. + +sub parse_rfc5424_message { + my ($desc, $line) = @_; + + unless ($line) { + fail("$desc: no syslog message received"); + return; + } + + my ($pri, $ts, $host, $app, $pid, $msgid, $sd, $msg) = + $line =~ /^<(\d{1,3})> # PRI + 1\s # VERSION + (\S+)\s # TIMESTAMP + (\S+)\s # HOSTNAME + (\S+)\s # APP-NAME + (\S+)\s # PROCID + (\S+)\s # MSGID + (\S+)\s # STRUCTURED-DATA + (.*)/x; # MSG + + ok(defined($pri), "$desc: has PRI"); + + my $sev = $pri & 0x07; + my $fac = ($pri & 0x03f8) >> 3; + ok($sev >= 0 && $sev <= 7, "$desc: severity in PRI is 0-7"); + ok($fac >= 0 && $fac < 24, "$desc: facility in PRI is 0-23"); + + ok(defined($ts), "$desc: has TIMESTAMP"); + like($ts, + qr/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}$/, + "$desc: TIMESTAMP is ISO 8601 with ms and tz offset"); + + ok(defined($host), "$desc: has HOSTNAME"); + ok(length($host) > 0 && $host ne '-', "$desc: HOSTNAME is non-nil"); + + ok(defined($app), "$desc: has APP-NAME"); + like($app, qr/^[!-~]+$/, "$desc: APP-NAME is printable US-ASCII"); + + ok(defined($pid), "$desc: has PROCID"); + like($pid, qr/^\d+$/, "$desc: PROCID is a decimal integer"); + + is($msgid, '-', "$desc: MSGID is nil"); + is($sd, '-', "$desc: STRUCTURED-DATA is nil"); + + ok(defined($msg) && length($msg) > 0, "$desc: MSG is non-empty"); +} + +############################################################################### From 4e553a14e462cfb4f19bcd5918241105c2791847 Mon Sep 17 00:00:00 2001 From: Vadim Zhestikov Date: Wed, 25 Mar 2026 10:55:34 -0700 Subject: [PATCH 7/8] Core: add RFC 5424 syslog MSGID parameter. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add "msgid=" to the syslog directive. When rfc=rfc5424 is in use, the configured value is placed in the MSGID field of the RFC 5424 HEADER (§6.2.7) instead of the nil value "-". MSGID is at most 32 printable US-ASCII characters (0x21-0x7E; space and controls are excluded). Specifying "msgid=" without "rfc=rfc5424" is a configuration error. The default MSGID is "-". Expand NGX_SYSLOG_MAX_STR by 31 bytes to accommodate a maximum-length MSGID field. --- src/core/ngx_syslog.c | 72 +++++++++++++++++++++++++++++++++++++------ src/core/ngx_syslog.h | 1 + 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/src/core/ngx_syslog.c b/src/core/ngx_syslog.c index 4f74afa24..0e90046f0 100644 --- a/src/core/ngx_syslog.c +++ b/src/core/ngx_syslog.c @@ -14,7 +14,8 @@ + sizeof("1970-09-28T12:00:00.000+06:00") - 1 + 1 /* space */ \ + (NGX_MAXHOSTNAMELEN - 1) + 1 /* space */ \ + 48 /* APP-NAME/TAG */ + 1 /* space */ \ - + NGX_INT64_LEN /* PROCID */ + sizeof(" - - ") - 1 + + NGX_INT64_LEN /* PROCID */ + 1 /* space */ \ + + 32 /* MSGID */ + sizeof(" - ") - 1 static char *ngx_syslog_parse_args(ngx_conf_t *cf, ngx_syslog_peer_t *peer); @@ -101,6 +102,30 @@ ngx_syslog_process_conf(ngx_conf_t *cf, ngx_syslog_peer_t *peer) } } + if (peer->msgid.data != NULL) { + + if (!peer->rfc5424) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "syslog \"msgid\" requires rfc=rfc5424"); + return NGX_CONF_ERROR; + } + + /* + * RFC 5424 s6.2.7: MSGID consists of printable US-ASCII + * characters (0x21-0x7E; space and controls are excluded). + */ + for (j = 0; j < peer->msgid.len; j++) { + ch = peer->msgid.data[j]; + + if (ch < '!' || ch > '~') { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "syslog \"msgid\" must contain " + "printable US-ASCII characters"); + return NGX_CONF_ERROR; + } + } + } + if (peer->server.sockaddr == NULL) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "no syslog server specified"); @@ -119,6 +144,10 @@ ngx_syslog_process_conf(ngx_conf_t *cf, ngx_syslog_peer_t *peer) ngx_str_set(&peer->tag, "nginx"); } + if (peer->msgid.data == NULL) { + ngx_str_set(&peer->msgid, "-"); + } + peer->hostname = &cf->cycle->hostname; peer->logp = &cf->cycle->new_log; @@ -277,6 +306,29 @@ ngx_syslog_parse_args(ngx_conf_t *cf, ngx_syslog_peer_t *peer) return NGX_CONF_ERROR; } + } else if (ngx_strncmp(p, "msgid=", 6) == 0) { + + if (peer->msgid.data != NULL) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "duplicate syslog \"msgid\""); + return NGX_CONF_ERROR; + } + + /* + * RFC 5424 s6.2.7: MSGID is at most 32 printable US-ASCII + * characters. Character set and protocol-version constraints + * are validated in ngx_syslog_process_conf() once all + * parameters are known. + */ + if (len - 6 > 32) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "syslog msgid length exceeds 32"); + return NGX_CONF_ERROR; + } + + peer->msgid.data = p + 6; + peer->msgid.len = len - 6; + } else if (len == 10 && ngx_strncmp(p, "nohostname", 10) == 0) { peer->nohostname = 1; @@ -319,15 +371,16 @@ ngx_syslog_add_header(ngx_syslog_peer_t *peer, u_char *buf) * 2003-10-11T22:14:15.003+05:30 * * The date, time, and UTC offset are taken from the - * ngx_cached_http_log_iso8601 cache ("YYYY-MM-DDTHH:MM:SS±HH:MM", + * ngx_cached_http_log_iso8601 cache ("YYYY-MM-DDTHH:MM:SS+/-HH:MM", * 25 bytes). The first 19 bytes are "YYYY-MM-DDTHH:MM:SS" - * and the trailing 6 bytes are "±HH:MM". The millisecond + * and the trailing 6 bytes are "+/-HH:MM". The millisecond * field is read live from ngx_timeofday() so that it reflects * the current event-loop tick rather than the start of the * current second. * - * PROCID is the nginx process PID. MSGID and STRUCTURED-DATA - * are set to the nil value "-". + * PROCID is the nginx process PID. MSGID defaults to the nil + * value "-" and can be overridden with the "msgid=" parameter. + * STRUCTURED-DATA is always the nil value "-". */ tp = ngx_timeofday(); @@ -340,14 +393,15 @@ ngx_syslog_add_header(ngx_syslog_peer_t *peer, u_char *buf) tz.len = sizeof("+06:00") - 1; if (peer->nohostname) { - return ngx_sprintf(buf, "<%ui>1 %V.%03ui%V - %V %P - - ", + return ngx_sprintf(buf, "<%ui>1 %V.%03ui%V - %V %P %V - ", pri, &datetime, tp->msec, &tz, - &peer->tag, ngx_pid); + &peer->tag, ngx_pid, &peer->msgid); } - return ngx_sprintf(buf, "<%ui>1 %V.%03ui%V %V %V %P - - ", + return ngx_sprintf(buf, "<%ui>1 %V.%03ui%V %V %V %P %V - ", pri, &datetime, tp->msec, &tz, - peer->hostname, &peer->tag, ngx_pid); + peer->hostname, &peer->tag, ngx_pid, + &peer->msgid); } if (peer->nohostname) { diff --git a/src/core/ngx_syslog.h b/src/core/ngx_syslog.h index 3cc0fbe56..80a46fa76 100644 --- a/src/core/ngx_syslog.h +++ b/src/core/ngx_syslog.h @@ -12,6 +12,7 @@ typedef struct { ngx_uint_t facility; ngx_uint_t severity; ngx_str_t tag; + ngx_str_t msgid; ngx_str_t *hostname; From bcb409bfee465cd52568bb7ca2c55099f6786ecf Mon Sep 17 00:00:00 2001 From: Vadim Zhestikov Date: Wed, 25 Mar 2026 10:57:43 -0700 Subject: [PATCH 8/8] Tests: add RFC 5424 syslog MSGID parameter. Config tests (syslog_rfc5424_config.t): - msgid= accepted with rfc=rfc5424 - msgid= rejected when rfc=rfc5424 is not set - msgid= with a non-ASCII byte (>0x7E) rejected - msgid= exceeding 32 characters rejected - 32-character msgid= accepted Format tests (syslog_rfc5424.t): - Explicit msgid=MYAPP appears in the MSGID field (position 6 of the RFC 5424 HEADER) of the emitted syslog message. - Without msgid=, the MSGID field defaults to the nil value "-". --- t/syslog_rfc5424.t | 20 +++++++++++++++++++- t/syslog_rfc5424_config.t | 29 +++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/t/syslog_rfc5424.t b/t/syslog_rfc5424.t index 724973785..209f83961 100644 --- a/t/syslog_rfc5424.t +++ b/t/syslog_rfc5424.t @@ -26,7 +26,7 @@ select STDOUT; $| = 1; plan(skip_all => 'win32') if $^O eq 'MSWin32'; -my $t = Test::Nginx->new()->has(qw/http/)->plan(21); +my $t = Test::Nginx->new()->has(qw/http/)->plan(23); ############################################################################### @@ -69,6 +69,12 @@ http { syslog:server=127.0.0.1:%%PORT_8982_UDP%%,rfc=rfc5424,tag=my-app,facility=user; } + # RFC 5424 access log with explicit msgid= + location /a5424_msgid { + access_log syslog:server=127.0.0.1:%%PORT_8982_UDP%%,rfc=rfc5424,msgid=MYAPP + logf; + } + # RFC 3164 access log — verify backward compatibility location /a3164 { access_log syslog:server=127.0.0.1:%%PORT_8983_UDP%% logf; @@ -128,6 +134,18 @@ my ($pri) = $msg =~ /^<(\d+)>/; my $fac = ($pri & 0x03f8) >> 3; is($fac, 1, 'rfc5424: facility=user (1) encoded in PRI'); +# RFC 5424 MSGID field: explicit msgid= value appears in position 6 of the header + +$msg = get_syslog($s5424, '/a5424_msgid'); +my ($msgid_field) = $msg =~ /^<\d+>1\s\S+\s\S+\s\S+\s\d+\s(\S+)\s/; +is($msgid_field, 'MYAPP', 'rfc5424: explicit msgid= appears in MSGID field'); + +# Without msgid=, the MSGID field defaults to nil "-" + +$msg = get_syslog($s5424, '/a5424'); +($msgid_field) = $msg =~ /^<\d+>1\s\S+\s\S+\s\S+\s\d+\s(\S+)\s/; +is($msgid_field, '-', 'rfc5424: default MSGID is nil "-"'); + # Global error_log uses rfc5424 — check via background-daemon log file http_get('/a5424'); diff --git a/t/syslog_rfc5424_config.t b/t/syslog_rfc5424_config.t index 8c13b6044..89dade6e7 100644 --- a/t/syslog_rfc5424_config.t +++ b/t/syslog_rfc5424_config.t @@ -105,6 +105,35 @@ like($out, qr/tag length exceeds 48/, 'rfc5424: 49-char tag rejected'); $out = config_check("syslog:server=127.0.0.1:5140,rfc=rfc3164,tag=$tag49"); like($out, qr/tag length exceeds 48/, 'rfc3164: 49-char tag rejected'); +# msgid= is accepted with rfc=rfc5424. + +$out = config_check('syslog:server=127.0.0.1:5140,rfc=rfc5424,msgid=MYAPP'); +like($out, qr/test is successful/i, 'rfc5424: msgid= accepted'); + +# msgid= without rfc=rfc5424 must be rejected. + +$out = config_check('syslog:server=127.0.0.1:5140,msgid=MYAPP'); +like($out, qr/requires rfc=rfc5424/, 'msgid= without rfc5424 rejected'); + +# msgid= with a non-ASCII byte (>0x7E) must be rejected. +# Space cannot be tested this way because nginx's config parser splits on +# whitespace before syslog parsing sees the value. + +$out = config_check("syslog:server=127.0.0.1:5140,rfc=rfc5424,msgid=MY\xc3APP"); +like($out, qr/printable US-ASCII/, 'msgid= with non-ASCII byte rejected'); + +# msgid= must not exceed 32 characters. + +my $msgid33 = 'x' x 33; +$out = config_check("syslog:server=127.0.0.1:5140,rfc=rfc5424,msgid=$msgid33"); +like($out, qr/msgid length exceeds 32/, 'msgid= 33-char rejected'); + +# A 32-character msgid is within the limit and must be accepted. + +my $msgid32 = 'x' x 32; +$out = config_check("syslog:server=127.0.0.1:5140,rfc=rfc5424,msgid=$msgid32"); +like($out, qr/test is successful/i, 'msgid= 32-char accepted'); + done_testing; ###############################################################################