Differences between revisions 7 and 8
Revision 7 as of 2012-05-19 12:23:00
Size: 2093
Editor: GreyCat
Comment:
Revision 8 as of 2018-02-05 15:50:00
Size: 4199
Editor: GreyCat
Comment: Forgot this page was here. The previous question had a MUCH better answer, so split that off and put it here. Merge a few pieces of the old page.
Deletions are marked like this. Additions are marked like this.
Line 3: Line 3:
There are numerous ways to do this, but all of them are either limited by the available tools, or slow. We'll show a few examples. Adding timestamps to a stream is a challenge, because there aren't any standard tools to do it. You either have to install something specifically for it (e.g. `ts` from [[http://joeyh.name/code/moreutils/|moreutils]], or [[http://cr.yp.to/daemontools/multilog.html|multilog]] from daemontools), or write a filter in some programming language. Ideally, you '''do not want''' to fork a `date(1)` command for every line of input that you're logging, because it's too slow. You want to use builtins. Older versions of bash ''cannot do this''. You need at least [[BashFAQ/061|Bash 4.2]] for the `printf %(...)T` option. Otherwise, you could write something in Perl, Python, Tcl, etc. to read lines and write them out with timestamps.
Line 5: Line 5:
Let's start with the slow, portable way first and get it over with: Here's a simple timestamper using Bash 4.2, with 1 second resolution:
Line 8: Line 8:
# POSIX
while IFS= read -r line; do
  echo "$(date +%Y%m%d-%H:%M:%S) $line"
# Bash 4.2 or later
stamper() {
  while read -r; do
    printf '%(%Y-%m-%d %H:%M:%S)T %s\n' -1 "$REPLY"
  done
}
}}}

You could also use `%(%s)T` on systems that support `%s` in `strftime(3)`, for [[BashFAQ/070|Unix epoch]] timestamps. Sadly, this is not portable.

One external tool designed specifically for this purpose is `ts` from the [[http://joeyh.name/code/moreutils/|moreutils]] package. Its default format is poorly chosen (human readable month names, in Mmm DD YYYY format), but you can specify a format. It supports microsecond resolution.

{{{
stamper() {
    ts "%Y-%m-%d %H:%M:%S.%.S"
}
}}}

Another choice is multilog, with nanosecond resolution:

{{{
stamper() {
  multilog t e 2>&1
}
}}}

The `e` option of `multilog` tells it to write each line to stderr, instead of to a file in an automatically rotated directory. `multilog` writes timestamps in [[http://cr.yp.to/libtai/tai64.html|TAI64N]] format, which is similar in nature to Unix epoch format. Use a conversion tool (e.g. `tai64nlocal`) to make them human-readable.

Here's an example using Tcl, with microsecond resolution:

{{{#!format plain
#!/usr/bin/env tclsh
while {[gets stdin line] >= 0} {
    set now [clock microseconds]
    set frac [expr {$now % 1000000}]
    set now [expr {$now / 1000000}]
    puts "[clock format $now -format {%Y-%m-%d %H:%M:%S}].$frac $line"
}
}}}

That works best as a separate, standalone script. Embedding it in a bash function is also ''possible'', albeit ugly:

{{{
stamper() {
  tclsh <(cat <<\EOF
    while {[gets stdin line] >= 0} {
      set now [clock microseconds]
      set frac [expr {$now % 1000000}]
      set now [expr {$now / 1000000}]
      puts "[clock format $now -format {%Y-%m-%d %H:%M:%S}].$frac $line"
    }
EOF
  )
}
}}}

For perl fans, here's one with 1 second resolution:

{{{
stamper() {
    perl -p -e '$|=1; @l=localtime; printf "%04d-%02d-%02d %02d:%02d:%02d ",
        1900+$l[5], $l[4], $l[3], $l[2], $l[1], $l[0]'
}
}}}

Pick one of these, or write another one (or a better one) in your preferred language. Let's call whichever one you chose '''stamper'''. For the purposes of this section, let's say we're not completely insane, so we don't care about separation of stdout and stderr. We just want it all in one file, in order, with stdout and stderr mixed together.

Now, it's ''somewhat'' easy to run a script so that its output and stderr go to the screen normally, while timestamped lines go to a logfile:

{{{
./myscript 2>&1 | tee >(stamper >logfile)
}}}

Putting this same thing ''inside'' a script is slightly more involved:

{{{#!format plain
#!/bin/bash
exec > >(tee >(stamper >logfile)) 2>&1

while sleep 0.2s; do
    echo "TPS report $RANDOM"
Line 14: Line 92:
And another one that's even slower: And of course, [[BashFAQ/009|buffering issues]] may exist, if the parts of the script that write to '''stamper''' use stdout buffering. Take that into consideration.

==== I don't care how slow it is! I want to call date(1) a thousand times a second! My PC doubles as my space-heater! ====

This is inefficient and horrible.
Line 16: Line 99:
awk '{system("printf \"`date +%T ` \">&2")}$0' stamper() {
  while read -r; do
    printf '%s %s\n' "$(date +'%Y-%m-%d %H:%M:%S')" "$REPLY"
  done
}
Line 19: Line 106:
And a third one, which is slightly faster, but which may mangle some of the input lines:
{{{
xargs -I@ -n1 date "+%T @"
}}}


The obvious disadvantage to all of the above examples is that we are executing the external `date` command for every line of input. If we only get a line every couple seconds, that may be acceptable. But if we're trying to timestamp a stream that gets dozens of lines per second, we may not even be able to keep up with the writer.

There are various ways to do it without forking for every line, but they all require nonstandard tools or specific shells. Bash 4.2 can do it with `printf`:

{{{
# Bash 4.2
while read -r; do
  printf "%(%Y%m%d-%H:%M:%S)T %s\n" -1 "$REPLY"
done
}}}

The `%(...)T` format specifier is new in bash 4.2. The argument of -1 tells it to use the current time, rather than a time passed as an argument. See the man page for details.

Another way is to write a perl one-liner:

{{{
perl -p -e '@l=localtime; printf "%04d%02d%02d-%02d:%02d:%02d ", 1900+$l[5], $l[4], $l[3], $l[2], $l[1], $l[0]'
}}}

I'm sure someone will come up with a 7-byte alternative that does the same thing using some magic perl syntax I've never seen before and can't understand....

There are other tools available specifically for timestamping logfiles and the like. One of them is [[http://cr.yp.to/daemontools/multilog.html|multilog]] from daemontools; but its timestamping format is [[http://cr.yp.to/libtai/tai64.html#tai64n|TAI64N]] which is not human-readable. Another is ts from the [[http://kitenet.net/~joey/code/moreutils/|moreutils]] package.
Don't use this. It's stupid. You were warned.

How do I add a timestamp to every line of a stream?

Adding timestamps to a stream is a challenge, because there aren't any standard tools to do it. You either have to install something specifically for it (e.g. ts from moreutils, or multilog from daemontools), or write a filter in some programming language. Ideally, you do not want to fork a date(1) command for every line of input that you're logging, because it's too slow. You want to use builtins. Older versions of bash cannot do this. You need at least Bash 4.2 for the printf %(...)T option. Otherwise, you could write something in Perl, Python, Tcl, etc. to read lines and write them out with timestamps.

Here's a simple timestamper using Bash 4.2, with 1 second resolution:

# Bash 4.2 or later
stamper() {
  while read -r; do
    printf '%(%Y-%m-%d %H:%M:%S)T %s\n' -1 "$REPLY"
  done
}

You could also use %(%s)T on systems that support %s in strftime(3), for Unix epoch timestamps. Sadly, this is not portable.

One external tool designed specifically for this purpose is ts from the moreutils package. Its default format is poorly chosen (human readable month names, in Mmm DD YYYY format), but you can specify a format. It supports microsecond resolution.

stamper() {
    ts "%Y-%m-%d %H:%M:%S.%.S"
}

Another choice is multilog, with nanosecond resolution:

stamper() {
  multilog t e 2>&1
}

The e option of multilog tells it to write each line to stderr, instead of to a file in an automatically rotated directory. multilog writes timestamps in TAI64N format, which is similar in nature to Unix epoch format. Use a conversion tool (e.g. tai64nlocal) to make them human-readable.

Here's an example using Tcl, with microsecond resolution:

#!/usr/bin/env tclsh
while {[gets stdin line] >= 0} {
    set now [clock microseconds]
    set frac [expr {$now % 1000000}]
    set now [expr {$now / 1000000}]
    puts "[clock format $now -format {%Y-%m-%d %H:%M:%S}].$frac $line"
}

That works best as a separate, standalone script. Embedding it in a bash function is also possible, albeit ugly:

stamper() {
  tclsh <(cat <<\EOF
    while {[gets stdin line] >= 0} {
      set now [clock microseconds]
      set frac [expr {$now % 1000000}]
      set now [expr {$now / 1000000}]
      puts "[clock format $now -format {%Y-%m-%d %H:%M:%S}].$frac $line"
    }
EOF
  )
}

For perl fans, here's one with 1 second resolution:

stamper() {
    perl -p -e '$|=1; @l=localtime; printf "%04d-%02d-%02d %02d:%02d:%02d ",
        1900+$l[5], $l[4], $l[3], $l[2], $l[1], $l[0]'
}

Pick one of these, or write another one (or a better one) in your preferred language. Let's call whichever one you chose stamper. For the purposes of this section, let's say we're not completely insane, so we don't care about separation of stdout and stderr. We just want it all in one file, in order, with stdout and stderr mixed together.

Now, it's somewhat easy to run a script so that its output and stderr go to the screen normally, while timestamped lines go to a logfile:

./myscript 2>&1 | tee >(stamper >logfile)

Putting this same thing inside a script is slightly more involved:

#!/bin/bash
exec > >(tee >(stamper >logfile)) 2>&1

while sleep 0.2s; do
    echo "TPS report $RANDOM"
done

And of course, buffering issues may exist, if the parts of the script that write to stamper use stdout buffering. Take that into consideration.

I don't care how slow it is! I want to call date(1) a thousand times a second! My PC doubles as my space-heater!

This is inefficient and horrible.

stamper() {
  while read -r; do
    printf '%s %s\n' "$(date +'%Y-%m-%d %H:%M:%S')" "$REPLY"
  done
}

Don't use this. It's stupid. You were warned.

BashFAQ/107 (last edited 2020-12-21 19:30:23 by GreyCat)