Repository navigation
php-fpm workers' log get silenced with config catch_workers_output = yes and error_log = syslog #25077
Description
Description
Follow up of Syslog broken on FPM since PHP 7.3.0 https://bugs.php.net/bug.php?id=79342.
When the php-fpm config catch_workers_output is enabled, and error_log is syslog, all workers' log won't go syslog.
I saw /proc/<php-fpm master pid>/fd/2 points to /dev/null, this is expected as
php-src/sapi/fpm/fpm/fpm_stdio.c
Lines 130 to 132 in 136e049
| if (fpm_globals.error_log_fd == ZLOG_SYSLOG) { | |
| /* dup to /dev/null when using syslog */ | |
| dup2(STDOUT_FILENO, STDERR_FILENO); |
php-src/sapi/fpm/fpm/fpm_stdio.c
Lines 63 to 66 in 136e049
| int fpm_stdio_init_final(void) | |
| { | |
| if (0 > fpm_stdio_redirect_stderr_to_error_log() || | |
| 0 > fpm_stdio_redirect_stderr_to_dev_null_for_syslog()) { |
Line 82 in 136e049
| fpm_stdio_init_final(); |
point
stderr to stdout when php-fpm config error_log = syslog, and stdout was previously pointed to /dev/null:php-src/sapi/fpm/fpm/fpm_stdio.c
Lines 27 to 36 in 136e049
| int fpm_stdio_init_main(void) | |
| { | |
| int fd = open("/dev/null", O_RDWR); | |
| if (0 > fd) { | |
| zlog(ZLOG_SYSERROR, "failed to init stdio: open(\"/dev/null\")"); | |
| return -1; | |
| } | |
| if (0 > dup2(fd, STDIN_FILENO) || 0 > dup2(fd, STDOUT_FILENO)) { |
Line 57 in 136e049
| 0 > fpm_stdio_init_main() || |
This was introduced in #1432 to fix https://bugs.php.net/bug.php?id=69865.
Also I guess this can prevent seeing duplicate logs from php-fpm in journalctl when using systemd service unit with the default value journal of option StandardError= to run php-fpm with daemonize = off as php-fpm itself already reporting logs to syslog and journald should be syslogd on many distros using systemd.
With php-fpm config catch_workers_output = yes, all workers' stderr are piped to master process, assume one of the worker's pid is 404:
$ ls -l /proc/404/fd/2
l-wx------ 1 root root 64 Oct 31 05:58 /proc/404/fd/2 -> 'pipe:[1853673300]'
$ find /proc -lname 'pipe:\[1853673300]' 2>/dev/null # https://superuser.com/q/401066/1777086#comment2862240_401619
/proc/317/task/317/fd/11
/proc/317/fd/11
/proc/404/task/404/fd/2
/proc/404/fd/2
$ lsof -d ^mem -a -p 317 # fd used by master processSo workers' stderr such as PHP error should be pipe to master process and then journald via syslog, but there's only log from master process in journalctl -u <php-fpm unit name>, such as NOTICE: ready to handle connections.
When I strace into the master process:
$ echo '<?php 1' > test.php
$ strace -p 317 -e read,write -vs 999 &
strace: Process 317 attached
$ SCRIPT_FILENAME=$PWD/test.php REQUEST_METHOD=GET cgi-fcgi -bind -connect localhost:9000 # or path to unix domain socket of php-fpm
Status: 500 Internal Server Error
Content-type: text/html; charset=UTF-8
read(11
, "NOTICE: PHP message: PHP Parse error: syntax error, unexpected end of file in /foo/bar/test.php on line 2\n\0fscf\0", 1023) = 113
write(2, "[31-Oct-2026 06:41:53] WARNING: [pool roundcube] child 407 said into stderr: \"NOTICE: PHP message: PHP Parse error: syntax error, unexpected end of file in /foo/bar/test.php on line 2\"\n", 186) = 186
read(11, 0x7fff1dc02980, 1023) = -1 EAGAIN (Resource temporarily unavailable)it just pipe the fd/11 that piped from worker process's stderr to its stderr, which is /dev/null.
I guess it's caused by this hardcoded stderr destination:
php-src/sapi/fpm/fpm/fpm_stdio.c
Line 195 in 136e049
| zlog_stream_init_ex(log_stream, ZLOG_WARNING, STDERR_FILENO); |
Lines 528 to 556 in 136e049
| static inline void zlog_stream_init_internal( | |
| struct zlog_stream *stream, int flags, size_t capacity, int fd) /* {{{ */ | |
| { | |
| if (fd == 0) { | |
| fd = zlog_fd; | |
| } | |
| memset(stream, 0, sizeof(struct zlog_stream)); | |
| stream->flags = flags; | |
| stream->use_syslog = fd == ZLOG_SYSLOG; | |
| stream->use_fd = fd > 0; | |
| stream->fd = fd > -1 ? fd : STDERR_FILENO; | |
| stream->buf_init_size = capacity; | |
| if (flags & ZLOG_ACCESS_LOG) { | |
| stream->use_external_logger = 0; | |
| stream->use_buffer = 1; | |
| stream->use_stderr = fd < 0; | |
| } else { | |
| stream->use_external_logger = external_logger != NULL; | |
| stream->use_buffer = zlog_buffering || external_logger != NULL || stream->use_syslog; | |
| stream->use_stderr = fd < 0 || | |
| ( | |
| fd != STDERR_FILENO && fd != STDOUT_FILENO && !launched && | |
| (flags & ZLOG_LEVEL_MASK) >= ZLOG_NOTICE | |
| ); | |
| stream->prefix_buffer = (flags & ZLOG_LEVEL_MASK) >= zlog_level && | |
| (stream->use_fd || stream->use_stderr || stream->use_syslog); | |
| } | |
| } |
that introduced in commit 3e5afbf#diff-5af9050ab2bfafe95720d7badd345919c64c8fcd41a16089facf80a439ce86b0R144 of #2458 which appeared in https://github.com/php/php-src/releases/tag/php-7.3.0RC1, before that it's using
zlog():php-src/sapi/fpm/fpm/fpm_stdio.c
Lines 189 to 190 in 2a78006
| zlog(ZLOG_WARNING, "[pool %s] child %d said into %s: \"%s\"%s", child->wp->config->name, | |
| (int) child->pid, is_stdout ? "stdout" : "stderr", buf, is_last_message ? ", pipe is closed" : ""); |
Line 9 in 2a78006
| #define zlog(flags,...) zlog_ex(__func__, __LINE__, flags, __VA_ARGS__) |
Lines 198 to 201 in 2a78006
| void zlog_ex(const char *function, int line, int flags, const char *fmt, ...) /* {{{ */ { | |
| va_list args; | |
| va_start(args, fmt); | |
| vzlog(function, line, flags, fmt, args); |
which supports writing to
syslog:Lines 129 to 138 in 2a78006
| #ifdef HAVE_SYSLOG_H | |
| if (zlog_fd == ZLOG_SYSLOG /* && !fpm_globals.is_child */) { | |
| len = 0; | |
| if (zlog_level == ZLOG_DEBUG) { | |
| len += snprintf(buf, buf_size, "[%s] %s(), line %d: ", level_names[flags & ZLOG_LEVEL_MASK], function, line); | |
| } else { | |
| len += snprintf(buf, buf_size, "[%s] ", level_names[flags & ZLOG_LEVEL_MASK]); | |
| } | |
| } else | |
| #endif |
when
error_log = syslog:php-src/sapi/fpm/fpm/fpm_stdio.c
Lines 286 to 299 in 2a78006
| int fpm_stdio_open_error_log(int reopen) /* {{{ */ | |
| { | |
| int fd; | |
| #ifdef HAVE_SYSLOG_H | |
| if (!strcasecmp(fpm_global_config.error_log, "syslog")) { | |
| php_openlog(fpm_global_config.syslog_ident, LOG_PID | LOG_CONS, fpm_global_config.syslog_facility); | |
| fpm_globals.error_log_fd = ZLOG_SYSLOG; | |
| if (fpm_use_error_log()) { | |
| zlog_set_fd(fpm_globals.error_log_fd); | |
| } | |
| return 0; | |
| } | |
| #endif |
PHP Version
PHP 8.5.12 (fpm-fcgi) (built: Oct 19 2026 17:02:46) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.5.12, Copyright (c) Zend Technologies
with Zend OPcache v8.5.12, Copyright (c), by Zend Technologies
Operating System
No response
Activity
n0099 commentedon Oct 31, 2026
A quite similar question How to get PHP FPM errors into the journald? on serverfault.