MEDIUM: log: add support for logging to existing file descriptors
In certain situations it would be desirable to log to an existing file
descriptor, the most common case being a pipe between containers or
processes. The main issue with pipes is that using write() on them will
randomly truncate messages. But there is a trick. By using writev(), we
can atomically deliver or drop a message, which perfectly fits the
purpose. The only caveat is that large messages (4096 bytes on modern
operating systems) may be interleaved with messages from other processes
if using nbproc for example. In practice such messages are rare and most
of the time when users need such type of logging, the load is low enough
for a single process to be running so this is not really a problem.
This logging method thus uses unbuffered writev() calls and is uses more
CPU than if it used its own buffer with large writes at once, though this
is not a problem for moderate loads.
Logging to a file descriptor attached to a file also works with the side
effect that the process is significantly slowed down during disk accesses
and that it's not possible to rotate the file without restarting the
process. For this reason this option is not offered as a configuration
option, since it would confuse most users, but one could decide to
redirect haproxy's output to a file during debugging sessions. Two aliases
"stdout" and "stderr" are provided, but keep in mind that these are closed
by default in daemon mode.
When logging to a pipe or socket at a high enough rate, some logs will be
dropped and the number of dropped messages is reported in "show info".
diff --git a/doc/configuration.txt b/doc/configuration.txt
index 7679998..9f4fb59 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -837,11 +837,30 @@
no port is specified, 514 is used by default (the standard syslog
port).
- - A filesystem path to a UNIX domain socket, keeping in mind
+ - A filesystem path to a datagram UNIX domain socket, keeping in mind
considerations for chroot (be sure the path is accessible inside
the chroot) and uid/gid (be sure the path is appropriately
writable).
+ - A file descriptor number in the form "fd@<number>", which may point
+ to a pipe, terminal, or socket. In this case unbuffered logs are used
+ and one writev() call per log is performed. This is a bit expensive
+ but acceptable for most workloads. Messages sent this way will not be
+ truncated but may be dropped, in which case the DroppedLogs counter
+ will be incremented. The writev() call is atomic even on pipes for
+ messages up to PIPE_BUF size, which POSIX recommends to be at least
+ 512 and which is 4096 bytes on most modern operating systems. Any
+ larger message may be interleaved with messages from other processes.
+ Exceptionally for debugging purposes the file descriptor may also be
+ directed to a file, but doing so will significantly slow haproxy down
+ as non-blocking calls will be ignored. Also there will be no way to
+ purge nor rotate this file without restarting the process. Note that
+ the configured syslog format is preserved, so the output is suitable
+ for use with a TCP syslog server.
+
+ - "stdout" / "stderr", which are respectively aliases for "fd@1" and
+ "fd@2", see above.
+
You may want to reference some environment variables in the address
parameter, see section 2.3 about environment variables.
@@ -5101,8 +5120,29 @@
inside the chroot) and uid/gid (be sure the path is
appropriately writable).
- You may want to reference some environment variables in the
- address parameter, see section 2.3 about environment variables.
+ - A file descriptor number in the form "fd@<number>", which may
+ point to a pipe, terminal, or socket. In this case unbuffered
+ logs are used and one writev() call per log is performed. This
+ is a bit expensive but acceptable for most workloads. Messages
+ sent this way will not be truncated but may be dropped, in
+ which case the DroppedLogs counter will be incremented. The
+ writev() call is atomic even on pipes for messages up to
+ PIPE_BUF size, which POSIX recommends to be at least 512 and
+ which is 4096 bytes on most modern operating systems. Any
+ larger message may be interleaved with messages from other
+ processes. Exceptionally for debugging purposes the file
+ descriptor may also be directed to a file, but doing so will
+ significantly slow haproxy down as non-blocking calls will be
+ ignored. Also there will be no way to purge nor rotate this
+ file without restarting the process. Note that the configured
+ syslog format is preserved, so the output is suitable for use
+ with a TCP syslog server.
+
+ - "stdout" / "stderr", which are respectively aliases for "fd@1"
+ and "fd@2", see above.
+
+ You may want to reference some environment variables in the
+ address parameter, see section 2.3 about environment variables.
<length> is an optional maximum line length. Log lines larger than this
value will be truncated before being sent. The reason is that
diff --git a/src/log.c b/src/log.c
index 3434550..c8ee6a9 100644
--- a/src/log.c
+++ b/src/log.c
@@ -754,6 +754,12 @@
goto error;
}
+ /* take care of "stdout" and "stderr" as regular aliases for fd@1 / fd@2 */
+ if (strcmp(args[1], "stdout") == 0)
+ args[1] = "fd@1";
+ else if (strcmp(args[1], "stderr") == 0)
+ args[1] = "fd@2";
+
logsrv = calloc(1, sizeof(*logsrv));
if (!logsrv) {
memprintf(err, "out of memory");
@@ -1342,9 +1348,13 @@
if (unlikely(*plogfd < 0)) {
/* socket not successfully initialized yet */
- int proto = logsrv->addr.ss_family == AF_UNIX ? 0 : IPPROTO_UDP;
-
- if ((*plogfd = socket(logsrv->addr.ss_family, SOCK_DGRAM, proto)) < 0) {
+ if (logsrv->addr.ss_family == AF_UNSPEC) {
+ /* the socket's address is a file descriptor */
+ *plogfd = ((struct sockaddr_in *)&logsrv->addr)->sin_addr.s_addr;
+ fcntl(*plogfd, F_SETFL, O_NONBLOCK);
+ }
+ else if ((*plogfd = socket(logsrv->addr.ss_family, SOCK_DGRAM,
+ (logsrv->addr.ss_family == AF_UNIX) ? 0 : IPPROTO_UDP)) < 0) {
static char once;
if (!once) {
@@ -1473,10 +1483,16 @@
iovec[7].iov_base = "\n"; /* insert a \n at the end of the message */
iovec[7].iov_len = 1;
- msghdr.msg_name = (struct sockaddr *)&logsrv->addr;
- msghdr.msg_namelen = get_addr_len(&logsrv->addr);
+ if (logsrv->addr.ss_family == AF_UNSPEC) {
+ /* the target is a direct file descriptor */
+ sent = writev(*plogfd, iovec, 8);
+ }
+ else {
+ msghdr.msg_name = (struct sockaddr *)&logsrv->addr;
+ msghdr.msg_namelen = get_addr_len(&logsrv->addr);
- sent = sendmsg(*plogfd, &msghdr, MSG_DONTWAIT | MSG_NOSIGNAL);
+ sent = sendmsg(*plogfd, &msghdr, MSG_DONTWAIT | MSG_NOSIGNAL);
+ }
if (sent < 0) {
static char once;
@@ -1485,7 +1501,7 @@
HA_ATOMIC_ADD(&dropped_logs, 1);
else if (!once) {
once = 1; /* note: no need for atomic ops here */
- ha_alert("sendmsg() failed in logger #%d: %s (errno=%d)\n",
+ ha_alert("sendmsg()/writev() failed in logger #%d: %s (errno=%d)\n",
nblogger, strerror(errno), errno);
}
}