1

Want to log all stdout and stderr to seperate files and add timestamp to each line.

Tried the following, which works but is missing timestamps.

#!/bin/bash

debug_file=./stdout.log
error_file=./stderr.log

exec > >(tee -a "$debug_file") 2> >(tee -a "$error_file")

echo "hello"
echo "hello world"

this-will-fail
and-so-will-this

Adding timestamps. (Only want timestamps prefixed to log output.)

#!/bin/bash

debug_file=./stdout.log
error_file=./stderr.log

log () {
  file=$1; shift 
  while read -r line; do
    printf '%(%s)T %s\n' -1 "$line"
  done >> "$file"
}

exec > >(tee >(log "$debug_file")) 2> >(tee >(log "$error_file"))

echo "hello"
echo "hello world"

this-will-fail
and-so-will-this

The latter adds timestamps to the logs but it also has the chance of messing up my terminal window. Reproducing this behavior was not straight forward, it only happend every now and then. I suspect it has to with the subroutine/buffer still having output flowing through it.

Examples of the script messing up my terminal.

# expected/desired behavior
user@system:~ ./log_test
hello
hello world
./log_test: line x: this-will-fail: command not found
./log_test: line x: and-so-will-this: command not found
user@system:~ # <-- cursor blinks here

# erroneous behavior
user@system:~ ./log_test
hello
hello world
user@system:~ ./log_test: line x: this-will-fail: command not found
./log_test: line x: and-so-will-this: command not found
# <-- cursor blinks here

# erroneous behavior
user@system:~ ./log_test
hello
hello world
./log_test: line x: this-will-fail: command not found
user@system:~
./log_test: line x: and-so-will-this: command not found
# <-- cursor blinks here

# erroneous behavior
user@system:~ ./log_test
hello
hello world
user@system:~
./log_test: line x: this-will-fail: command not found
./log_test: line x: and-so-will-this: command not found
# <-- cursor blinks here

For funs I put a sleep 2 at the end of the script to see what would happen and the problem never occurred again.

Hopefully someone knows the answer or can point me in the right derection.

Thanks

Edit

Judging from another question answered by Charles Duffy, what I'm trying to achieve is not really possible in bash. Separately redirecting and recombining stderr/stdout without losing ordering

sxkx
  • 512
  • 5
  • 13
  • Yes, you have a race condition, because all these processes are running asynchronously from each other. *If* a `write()` gets split along a boundary such that it doesn't comprise a full line, you can have them mixed. – Charles Duffy Aug 27 '19 at 15:01
  • There isn't really a solution to that, other than to make sure your software is flushing its output in single-line chunks (and that those chunks aren't so large that they get split anyhow). – Charles Duffy Aug 27 '19 at 15:02
  • 2
    BTW, `$(date +%s)` is **really** slow to execute if you're putting it in an inner loop, slow enough that it'll actually make a serious dent in your output speed. If you're using bash 4.3+, much better to `printf '%(%s)T %s\n' -1 "$line"` – Charles Duffy Aug 27 '19 at 15:03
  • ...and don't put `>> file` on your individual `printf` or `echo`, put it on the whole loop so you open the output file just once, instead of re-opening it every time you want to write a single line and closing it after. – Charles Duffy Aug 27 '19 at 15:04
  • And with Bash 5.0 or newer, you can use `$EPOCHSECONDS` instead of `printf '%(%s)T\n' -1`. – Benjamin W. Aug 27 '19 at 15:14
  • 1
    WOW! I did not know about `%(%$strftime_string)T` BTW, another option is `ts` from `moreutils` package on ubuntu. Copying just this binary across systems usually works. – anishsane Aug 27 '19 at 15:26
  • 1
    BTW, on an unrelated note, you should be redirecting the stderr to stderr correctly: `exec > >(tee -a "$debug_file") 2> >(tee -a "$error_file" >&2)` – anishsane Aug 27 '19 at 15:30
  • 1
    @anishsane, ...there are equivalents to `ts` in any daemontools-style tool suite (runit, &c), so it shouldn't be hard to find a native package or binary on any platform you may happen to be on; see f/e https://cr.yp.to/daemontools/tai64n.html – Charles Duffy Aug 27 '19 at 15:30
  • @sxkx, ...if you consider this question addressed by the other, I'll link them as duplicates. – Charles Duffy Aug 27 '19 at 15:31
  • @CharlesDuffy No I do not, my question is not answered. – sxkx Aug 27 '19 at 15:33
  • So the outstanding question is how to make the `tee`s finish before the script exits (and your terminal prompt gets printed)? That's already in the knowledgebase; I can find some links. – Charles Duffy Aug 27 '19 at 15:35
  • Yes and no, tried writing this without tee, had the same issue, just want to be able to stamp my logs. – sxkx Aug 27 '19 at 15:38
  • Right -- the problem's not specific to tee, it's about running your stdout and stderr through a process substitution in general -- if you want to let the process substitution finish before your script exits, you need to collect its PID and `wait` for it (something which requires bash 4.4 or newer, I want to say; for older versions, there are other tricks involving `flock` or such that can be used). – Charles Duffy Aug 27 '19 at 15:43
  • BTW, it's probably not a great idea to call your stderr log an "error log". stderr is supposed to be used for *all* diagnostic logging, not just errors -- "diagnostic logging" contains things like informational log messages, prompts (since information about when more user input is needed is informational and not part of output), etc. – Charles Duffy Aug 27 '19 at 15:44
  • @CharlesDuffy you removed your answer, was about to give it a go! :( – sxkx Aug 27 '19 at 15:50
  • https://serverfault.com/questions/310098/how-to-add-a-timestamp-to-bash-script-log – ewindes Aug 27 '19 at 15:54
  • @sxkx, ...removed it so I could iterate until I had something that worked right out-of-the-box; take another look. :) – Charles Duffy Aug 27 '19 at 16:05
  • @CharlesDuffy Thanks a lot, will look at it a bit later today. – sxkx Aug 27 '19 at 17:09
  • @ewindes, Read that question/answer before submitting mine, it's not quite what I'm after but might end up using something similar. Thanks for the suggestion – sxkx Aug 28 '19 at 14:20

1 Answers1

1

The trick is to make sure that tee, and the process substitution running your log function, exits before the script as a whole does -- so that when the shell that started the script prints its prompt, there isn't any backgrounded process that might write more output after it's done.

As a working example (albeit one focused more on being explicit than terseness):

#!/usr/bin/env bash
stdout_log=stdout.log; stderr_log=stderr.log

log () {
  file=$1; shift
  while read -r line; do
    printf '%(%s)T %s\n' -1 "$line"
  done >> "$file"
}

# first, make backups of your original stdout and stderr
exec {stdout_orig_fd}>&1 {stderr_orig_fd}>&2
# for stdout: start your process substitution, record its PID, start tee, record *its* PID
exec {stdout_log_fd}> >(log "$stdout_log"); stdout_log_pid=$!
exec {stdout_tee_fd}> >(tee "/dev/fd/$stdout_log_fd"); stdout_tee_pid=$!
exec {stdout_log_fd}>&- # close stdout_log_fd so the log process can exit when tee does
# for stderr: likewise
exec {stderr_log_fd}> >(log "$stderr_log"); stderr_log_pid=$!
exec {stderr_tee_fd}> >(tee "/dev/fd/$stderr_log_fd" >&2); stderr_tee_pid=$!
exec {stderr_log_fd}>&- # close stderr_log_fd so the log process can exit when tee does
# now actually swap out stdout and stderr for the processes we started
exec 1>&$stdout_tee_fd 2>&$stderr_tee_fd {stdout_tee_fd}>&- {stderr_tee_fd}>&-

# ...do the things you want to log here...
echo "this goes to stdout"; echo "this goes to stderr" >&2

# now, replace the FDs going to tee with the backups...
exec >&"$stdout_orig_fd" 2>&"$stderr_orig_fd"

# ...and wait for the associated processes to exit.
while :; do
  ready_to_exit=1
  for pid_var in stderr_tee_pid stderr_log_pid stdout_tee_pid stdout_log_pid; do
    # kill -0 just checks whether a PID exists; it doesn't actually send a signal
    kill -0 "${!pid_var}" &>/dev/null && ready_to_exit=0
  done
  (( ready_to_exit )) && break
  sleep 0.1 # avoid a busy-loop eating unnecessary CPU by sleeping before next poll
done

So What's With The File Descriptor Manipulation?

A few key concepts to make sure we have clear:

  • All subshells have their own copies of the file descriptor table as created when they were fork()ed off from their parent. From that point forward, each file descriptor table is effectively independent.
  • A process reading from (the read end of) a FIFO (or pipe) won't see an EOF until all programs writing to (the write end of) that FIFO have closed their copies of the descriptor.

...so, if you create a FIFO pair, fork() off a child process, and let the child process write to the write end of the FIFO, whatever's reading from the read end will never see an EOF until not just the child, but also the parent, closes their copies.

Thus, the gymnastics you see here:

  • When we run exec {stdout_log_fd}>&-, we're closing the parent shell's copy of the FIFO writing to the log function for stdout, so the only remaining copy is the one used by the tee child process -- so that when tee exits, the subshell running log exits too.
  • When we run exec 1>&$stdout_tee_fd {stdout_tee_fd}>&-, we're doing two things: First, we make FD 1 a copy of the file descriptor whose number is stored in the variable stdout_tee_fd; second, we delete the stdout_tee_fd entry from the file descriptor table, so only the copy on FD 1 remains. This ensures that later, when we run exec >&"$stdout_orig_fd", we're deleting the last remaining write handle to the stdout tee function, causing tee to get an EOF on stdin (so it exits, thus closing the handle it holds on the log function's subshell and letting that subshell exit as well).

Some Final Notes On Process Management

Unfortunately, how bash handles subshells created for process substitutions has changed substantially between still-actively-deployed releases; so while in theory it's possible to use wait "$pid" to let a process substitution exit and collect its exit status, this isn't always reliable -- hence the use of kill -0.

However, if wait "$pid" worked, it would be strongly preferable, because the wait() syscall is what removes a previously-exited process's entry from the process table: It is guaranteed that a PID will not be reused (and a zombie process-table entry is left as a placeholder) if no wait() or waitpid() invocation has taken place.

Modern operating systems try fairly hard to avoid short-term PID reuse, so wraparound is not an active concern in most scenarios. However, if you're worried about this, consider using the flock-based mechanism discussed in https://stackoverflow.com/a/31552333/14122 for waiting for your process substitutions to exit, instead of kill -0.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • This answers my question and at the same time raises many new ones. Took some time to study your answer. 1) After starting process substitution for `log` and `tee`, the `log` FD gets closed? How does this work? 2) `stdout` gets piped to `stdout_tee_fd` which pipes `stdout_log_fd` which writes to `stdout.log`, likewise for `stderr`. Do I understand this correctly? 3) On line _"now actually swap out.."_, `stdout` gets piped to `stdout_tee_fd` but it's closed right after. How does this work? – sxkx Aug 28 '19 at 14:09
  • 4) `kill` is used to determine if the subprocesses have exited? 5) Are these PIDs reserverd until the parent script exists? Could the system reassign the PID of an already exited subshell and prevent the script from exiting. 6) `sleep 0.1` is a ratelimiter? 7) Would you recommend this approach or do you think it is better to just pipe `stderr` to `stdout` and write this to a log? Not bothering with the separate `stderr.log`. – sxkx Aug 28 '19 at 14:10
  • Was reading about I/O redirection and found this: _"Child processes inherit open file descriptors. This is why pipes work. To prevent an fd from being inherited, close it."_ Would this answer my question 1 and 3? – sxkx Aug 28 '19 at 14:27
  • Heh. That's a large enough set of questions that I should probably write up a proper amendment to the answer describing them. Will try to get to that when opportunity presents; if I haven't pinged you with an edit inside the next few days, feel free to ask again. – Charles Duffy Aug 28 '19 at 15:16
  • Sure, no problem. Thanks a lot! – sxkx Aug 28 '19 at 15:20
  • @sxkx, see edit -- both extra text, and some additional comments. Re: (7), personally, I wouldn't log stderr separately without a compelling need in light of the specifics of the implementation in question, exactly what the code being run is, &c. Having done so in practice in the past in customer-visible logs, my sense is that more confusion than value was generated. – Charles Duffy Aug 29 '19 at 02:14