26

I have the following issue:

I have an application, which continuously produces output to stderr and stdout. The output of this application is captured in a logfile (the app is redirected as: &> log.txt ). I don't have any options to produce a proper logging to file for this.

Now, I have a cron job, which runs every hour and beside of doing other things, it also tries to rotate this logfile above, by copying it to log.txt.1 and then creates an empty file and copies it to log.txt

It looks like:

cp log.txt log.txt.1
touch /tmp/empty
cp /tmp/empty log.txt

The problem is, that the application is still writing to it, and because of this I get some very strange stuff in the log.txt.1, it starts with a lot of garbage characters, and the actual log file is somewhere at the end.

Do you have any idea, how to make a correct log rotating for this specific situation (I also tried cat log.txt > log.txt.1, does not work)? Using logrotate for this specific application not an option, there is a whole mechanism behind the scenes that I may not change.

Thanks, f.

Ferenc Deak
  • 34,348
  • 17
  • 99
  • 167
  • 2
    @fritzone: have you considered using the logrotate utility itself? It also allows you to force log rotation. – 0xC0000022L Apr 26 '11 at 11:26
  • Does `head --lines=-10 log.txt > log.txt.1` work (where `10` is some "reasonable number" of lines to leave at the end)? Then to empty out the log, maybe `truncate` can work, although that works on byte sizes not lines... – drysdam Apr 26 '11 at 11:28
  • @drysdam: just tried it, does not work @STATUS_ACCES_DENIED: unfortunately I cannot use logrotate, there were lots of political debates, we are using shell scripts :( – Ferenc Deak Apr 26 '11 at 11:34
  • 2
    `logrotate` is a command line utility available from a shell script. If you can't use that, what else can't you use? You used `cat`, `cp` and `touch` above. Is `mv` in or out? – drysdam Apr 26 '11 at 11:38
  • I have tried `logrotate` the effect is the same, I get a lot of garbage characters in the files ... maybe the reason is that when I'm redirecting, the underlying OS knows at which file position the current file descriptor is and just continues writing from that point? – Ferenc Deak Apr 26 '11 at 12:13
  • @fritzone yes, that's how Linux's IO works. If the program opening a file does not close it first, the pointer never gets reset. To properly 'restart' a logfile, either (1) the program must be able to handle a signal so it will close the filestream and re-open it, or (2) the program must be restarted. – pepoluan Apr 26 '11 at 16:05
  • 1
    Developing a bash script for log rotation instead of using the widely available `logrotate` command is not only reinventing the wheel, but doing it very poorly. – MestreLion Mar 26 '13 at 11:23

6 Answers6

13

Okay, here's an idea, inspired by http://en.wikibooks.org/wiki/Bourne_Shell_Scripting/Files_and_streams

  1. make a named pipe:

    mkfifo /dev/mypipe
    
  2. redirect stdout and stderr to the named pipe:

    &> /dev/mypipe
    
  3. read from mypipe into a file:

    cat < /dev/mypipe > /var/log/log.txt &
    
  4. when you need to log-rotate, kill the cat, rotate the log, and restart the cat.

Now, I haven't tested this. Tell us how it goes.

Note: you can give the named pipe any name, like /var/tmp/pipe1 , /var/log/pipe , /tmp/abracadabra , and so on. Just make sure to re-create the pipe after booting before your logging-script runs.


Alternatively, don't use cat, but use a simple script file:

#!/bin/bash

while : ; do
  read line
  printf "%s\n" "$line"
done

This script guarantees an output for every newline read. (cat might not start outputting until its buffer is full or it encounters an EOF)


Final -- and TESTED -- attempt

IMPORTANT NOTE: Please read the comments from @andrew below. There are several situations which you need to be aware of.

Alright! Finally got access to my Linux box. Here's how:

Step 1: Make this recorder script:

#!/bin/bash

LOGFILE="/path/to/log/file"
SEMAPHORE="/path/to/log/file.semaphore"

while : ; do
  read line
  while [[ -f $SEMAPHORE ]]; do
    sleep 1s
  done
  printf "%s\n" "$line" >> $LOGFILE
done

Step 2: put the recorder into work:

  1. Make a named pipe:

    mkfifo $PIPENAME
    
  2. Redirect your application's STDOUT & STDERR to the named pipe:

    ...things... &> $PIPENAME
    
  3. Start the recorder:

    /path/to/recorder.sh < $PIPENAME &
    

    You might want to nohup the above to make it survive logouts.

  4. Done!

Step 3: If you need to logrotate, pause the recorder:

touch /path/to/log/file.semaphore
mv /path/to/log/file /path/to/archive/of/log/file
rm /path/to/log/file.semaphore

I suggest putting the above steps into its own script. Feel free to change the 2nd line to whatever log-rotating method you want to use.


Note : If you're handy with C programming, you might want to make a short C program to perform the function of recorder.sh. Compiled C programs will certainly be lighter than a nohup-ed detached bash script.


Note 2: David Newcomb provided a helpful warning in the comments: While the recorder is not running then writes to the pipe will block and may cause the program to fail unpredictably. Make sure the recorder is down (or rotating) for as short time as possible.

So, if you can ensure that rotating happens really quickly, you can replace sleep (a built-in command which accepts only integer values) with /bin/sleep (a program that accepts float values) and set the sleep period to 0.5 or shorter.

pepoluan
  • 6,132
  • 4
  • 46
  • 76
  • 1
    One thing to be careful of that I just ran into: make sure you only have one recorder.sh running, or your log file will be missing half the characters or other weirdness. This maybe sounds obvious, but I didn't think to kill it when the main process ends. – andrew Aug 05 '11 at 22:46
  • 1
    One more thing I just fixed: when the main process is killed, the logger script reads a line from the named pipe regardless of whether there is data. This means if your process writing to the pipe fails, you'll get a ton of empty lines written to the log. I fixed this by just doing `read line; if [ "$line" != "" ]; then [do logging]; fi` – andrew Aug 15 '11 at 22:40
  • 1
    @andrew again, nice catch! I have to admit I did not try lots of cases with the script, so your additional tips are really helpful; I'll edit my answer to ensure people will read your tips :) – pepoluan Aug 16 '11 at 04:13
  • 1
    I know it's been a while but it is probably worth mentioning that while the recorder is not running then writes to the pipe will block and may cause the program to fail unpredictably. Make sure the recorder is down (or rotating) for as shorter time as possible. – David Newcomb Feb 06 '17 at 21:41
  • Complicated and nonstandard. A square wheel as it is. (The post is also a conversation history rather than end result so there's a lot of redundant stuff that is useless for a new reader) – ivan_pozdeev Apr 09 '17 at 15:27
  • Besides, and more importantly, the downtime can be avoided fully if using the standard approach. – ivan_pozdeev Apr 09 '17 at 15:52
  • @ivan_pozdeev did you actually read the OP's question? Let me quote for you "Using logrotate for this specific application not an option, there is a whole mechanism behind the scenes that I may not change." YES, using logrotate IS much simpler, but for some reasons the OP cannot use that. – pepoluan Apr 09 '17 at 21:15
  • @pepoluan "the standard approach"!=`logrotate`. The solution here is suboptimal on its own, as is the post's composition. While SO is supposed to teach best practices. These are the two reasons for the downvote. – ivan_pozdeev Apr 10 '17 at 00:57
  • @ivan_pozdeev SO _first and foremost_ is to **provide a solution**. If the solution is suboptimal, then of course you're free to write an optimal solution. Number two, my post's composition is meant to preserve the train of thought and how we finally came up with a **workable solution**. But I'm tired of flogging a dead horse and I will not reply to you again, but let you sit high and mighty on your ivory tower of pedantic issues. Good day. – pepoluan Apr 10 '17 at 01:51
  • @pepoluan that's it - "preserve the train of thought and how we finally came up..." is only good during the conversation. After it's over, the provisional info is only getting in the way of further readers. I learned this the hard way by reading a fair number of such posts (including mine) and getting my share of downvotes for them. – ivan_pozdeev Apr 10 '17 at 08:55
6

First of all, you really should not reinvent the square wheel here. Your peers are probably against rotating the logs on daily schedule which automatically applies to all scripts in /etc/logrotate.d/ - this can be avoided by placing the script elsewhere.


Now, the standard approach to log rotation (that is implemented in logrotate) can be implemented by any other facility just as well. E.g. here's a sample implementation in bash:

MAXLOG=<maximum index of a log copy>
for i in `seq $((MAXLOG-1)) -1 1`; do
    mv "log."{$i,$((i+1))}    #will need to ignore file not found errors here
done 
mv log log.1    # since a file descriptor is linked to an inode rather than path,
                #if you move (or even remove) an open file, the program will continue
                #to write into it as if nothing happened
                #see https://stackoverflow.com/questions/5219896/how-do-the-unix-commands-mv-and-rm-work-with-open-files
<make the daemon reopen the log file with the old path>

The last item is done by sending SIGHUP or (less often) SIGUSR1 and having a signal handler in the daemon that replaces the corresponding file descriptor or variable. This way, the switch is atomic, so there's no interruption in logging availability. In bash, this would look like:

trap { exec &>"$LOGFILE"; } HUP

The other approach is to make the writing program itself keep track of the log size each time it writes to it and do the rotation. This limits your options in where you can write to and what rotation logic is to what the program itself supports. But it has the benefit of being a self-contained solution and checking the log size at each write rather than on schedule. Many languages' standard libraries have such a facility. As a drop-in solution, this is implemented in Apache's rotatelogs:

<your_program> 2>&1 | rotatelogs <opts> <logfile> <rotation_criteria>
ivan_pozdeev
  • 33,874
  • 19
  • 107
  • 152
1

I wrote a logrotee this weekend. I probably wouldn't if I've read @JdeBP's great answer about multilog before.

I focused on it being lightweight and being able to bzip2 its output chunks like:

verbosecommand | logrotee  \
  --compress "bzip2 {}" --compress-suffix .bz2 \
  /var/log/verbosecommand.log

There's a lot of to be done and tested yet, though.

Victor Sergienko
  • 13,115
  • 3
  • 57
  • 91
0

You can also pipe your output thru Apache rotatelogs utility. Or following script:

#!/bin/ksh
#rotatelogs.sh -n numberOfFiles pathToLog fileSize[B|K|M|G]

numberOfFiles=10

while getopts "n:fltvecp:L:" opt; do
    case $opt in
  n) numberOfFiles="$OPTARG"
    if ! printf '%s\n' "$numberOfFiles" | grep '^[0-9][0-9]*$' >/dev/null; then
      printf 'Numeric numberOfFiles required %s. rotatelogs.sh -n numberOfFiles pathToLog fileSize[B|K|M|G]\n' "$numberOfFiles" 1>&2
      exit 1
    elif [ $numberOfFiles -lt 3 ]; then
      printf 'numberOfFiles < 3 %s. rotatelogs.sh -n numberOfFiles pathToLog fileSize[B|K|M|G]\n' "$numberOfFiles" 1>&2
    fi
  ;;
  *) printf '-%s ignored. rotatelogs.sh -n numberOfFiles pathToLog fileSize[B|K|M|G]\n' "$opt" 1>&2
  ;;
  esac
done
shift $(( $OPTIND - 1 ))

pathToLog="$1"
fileSize="$2"

if ! printf '%s\n' "$fileSize" | grep '^[0-9][0-9]*[BKMG]$' >/dev/null; then
  printf 'Numeric fileSize followed by B|K|M|G required %s. rotatelogs.sh -n numberOfFiles pathToLog fileSize[B|K|M|G]\n' "$fileSize" 1>&2
  exit 1
fi

sizeQualifier=`printf "%s\n" "$fileSize" | sed "s%^[0-9][0-9]*\([BKMG]\)$%\1%"`

multip=1
case $sizeQualifier in
B) multip=1 ;;
K) multip=1024 ;;
M) multip=1048576 ;;
G) multip=1073741824 ;;
esac

fileSize=`printf "%s\n" "$fileSize" | sed "s%^\([0-9][0-9]*\)[BKMG]$%\1%"`
fileSize=$(( $fileSize * $multip ))
fileSize=$(( $fileSize / 1024 ))

if [ $fileSize -le 10 ]; then
  printf 'fileSize %sKB < 10KB. rotatelogs.sh -n numberOfFiles pathToLog fileSize[B|K|M|G]\n' "$fileSize" 1>&2
  exit 1
fi

if ! touch "$pathToLog"; then
  printf 'Could not write to log file %s. rotatelogs.sh -n numberOfFiles pathToLog fileSize[B|K|M|G]\n' "$pathToLog" 1>&2
  exit 1
fi

lineCnt=0
while read line
do
  printf "%s\n" "$line" >>"$pathToLog"
  lineCnt=$(( $lineCnt + 1 ))
  if [ $lineCnt -gt 200 ]; then
    lineCnt=0
    curFileSize=`du -k "$pathToLog" | sed -e 's/^[  ][  ]*//' -e 's%[   ][  ]*$%%' -e 's/[  ][  ]*/[    ]/g' | cut -f1 -d" "`
    if [ $curFileSize -gt $fileSize ]; then
      DATE=`date +%Y%m%d_%H%M%S`
      cat "$pathToLog" | gzip -c >"${pathToLog}.${DATE}".gz && cat /dev/null >"$pathToLog"
      curNumberOfFiles=`ls "$pathToLog".[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]_[0-9][0-9][0-9][0-9][0-9][0-9].gz | wc -l | sed -e 's/^[   ][  ]*//' -e 's%[   ][  ]*$%%' -e 's/[  ][  ]*/[    ]/g'`
      while [ $curNumberOfFiles -ge $numberOfFiles ]; do
        fileToRemove=`ls "$pathToLog".[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]_[0-9][0-9][0-9][0-9][0-9][0-9].gz | head -1`
        if [ -f "$fileToRemove" ]; then
          rm -f "$fileToRemove"
          curNumberOfFiles=`ls "$pathToLog".[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]_[0-9][0-9][0-9][0-9][0-9][0-9].gz | wc -l | sed -e 's/^[   ][  ]*//' -e 's%[   ][  ]*$%%' -e 's/[  ][  ]*/[    ]/g'`
        else
          break
        fi
      done
    fi
  fi
done
bravomail
  • 21
  • 2
0

You can leverage rotatelogs (docs here). This utility will decouple your script's stdout from the log file, managing the rotation in a transparent way. For example:

your_script.sh | rotatelogs /var/log/your.log 100M

will automatically rotate the output file when it reaches 100M (can be configured to rotate based on a time interval).

Cavaz
  • 2,996
  • 24
  • 38
0

A simplest script could be something like this rotatelog.sh:

#! /bin/bash

#DATE_FMT="%Y%m%d-%H%M" # for testing
#DATE_FMT="%Y%m%d-%H"   # rotate each hour
DATE_FMT="%Y%m%d"       # rotate each day

if [ "$1" != "" ]
then
        f=$1
else
        f="rotatelog"
fi

p=$(date +${DATE_FMT})

r=$f-$p.log
exec 2>&1 > $r

while read l
do
        d=$(date +${DATE_FMT})
        if [ $p != $d ]
        then
                x=$r
                p=$d
                r=$f-$p.log
                exec 2>&1 > $r
                gzip $x
        fi
        echo $l
done

You can use like:

your_process | rotatelog.sh yout_log_path_pattern
  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community May 16 '23 at 12:47