handle race condition
[log-quiet] / log-once
1 #!/bin/bash
2 # Copyright (C) 2019 Ian Kelling
3 # SPDX-License-Identifier: AGPL-3.0-or-later
4
5 append() {
6 cat >> "$1"
7 }
8 log-once() {
9 local cbase c log line i out file o tmp mvout mverr
10 cbase=/var/local/cron-errors
11 [[ $EUID == 0 ]] || cbase=$HOME/cron-errors
12 local help="Usage: some-command-that-outputs-on-error |& log-once [OPTION]... LOG_NAME
13
14 Main use case: in cronjobs where STDIN represents an error,
15 but we only want to output that to STDOUT if we've seen this type of
16 error ERRORS(default 3) number of times in a row, then we don't
17 want to output anything again until we've seen a success: no standard input.
18
19 Logs STDIN to /var/local/cron-errors/LOG_NAME\$error_count or
20 $HOME/cron-errors if not root, and keeps state in the same directory.
21
22 -ERRORS: ERRORS is the number of errors to accumulate before outputing the error
23
24
25 You can emulate how cronjobs work by doing this for example:
26
27 cmdname |& log-once | ifne mail -s 'cmdname failed' root@localhost
28
29 I could imagine a similar command that considers its non-option args to
30 be an error, but I haven't written it yet.
31 "
32 errors=3
33 mverr=0
34 while true; do
35 if [[ $1 == --help ]]; then
36 echo "$help"
37 return
38 elif [[ $1 == -[0-9]* ]]; then
39 errors=${1#-}
40 shift
41 elif [[ $1 == -- ]]; then
42 shift
43 break
44 else
45 break
46 fi
47 done
48 log_name=$1
49 # todo, make option & make them overridable based on command line or env variable
50 [[ -d $cbase ]] || mkdir -p $cbase
51 c=$cbase/$log_name
52 log=false
53 while read -r line; do
54 output+=( "$line" )
55 # If we find something that is not just a newline:
56 if [[ $line ]]; then
57 log=true
58 break
59 fi
60 done
61 glob="$c[0-9]*"
62 # file is error file indicating previous error
63 tmp=($glob); file="${tmp[0]}"
64 if [[ $file == "$glob" ]]; then
65 file=
66 fi
67 if $log; then
68 out=append
69 if [[ $file ]]; then
70 i="${file#$c}"
71 if (( i < errors )); then
72 new_file=$c$((i+1))
73 mvout=$(mv $file $new_file 2>&1) || mverr=$?
74 if (( mverr )); then
75 if [[ $mvout == *": No such file or directory" ]]; then
76 # We've very likely hit a race condition, where another
77 # log-once did the mv before us.
78 return 0
79 else
80 echo "log-once: error on mv $file $new_file"
81 return $mverr
82 fi
83 fi
84 file=$new_file
85 if [[ $file == $c$errors ]]; then
86 out="tee -a"
87 fi
88 fi
89 else
90 file=${c}1
91 if (( errors == 1 )); then
92 out="tee -a"
93 fi
94 fi
95 $out $file <<<"log-once: $(date -R)"
96 if [[ $2 ]]; then
97 $out $file <<<"$*"
98 else
99 for o in "${output[@]}"; do
100 $out $file <<<"$o"
101 done
102 $out $file
103 fi
104 return 0
105 fi
106 if [[ $file ]]; then
107 rm -f $file
108 if [[ $file == $c$errors ]]; then
109 echo "log-once: $(date -R): success after failure for $c"
110 fi
111 fi
112 return 0
113 }
114 log-once "$@"