add verbose option, license for small program
[log-quiet] / log-once
1 #!/bin/bash
2 # Copyright (C) 2019 Ian Kelling
3 # Log errors once or so instead of many times.
4
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18 # SPDX-License-Identifier: GPL-3.0-or-later
19
20 append() {
21 cat >> "$1"
22 }
23 log-once() {
24 local cbase c log line i out file o tmp mvout mverr verbose
25 cbase=/var/local/cron-errors
26 [[ $EUID == 0 ]] || cbase=$HOME/cron-errors
27 local help="Usage: some-command-that-outputs-on-error |& log-once [OPTION]... LOG_NAME
28
29 Main use case: in cronjobs where STDIN represents an error,
30 but we only want to output that to STDOUT if we've seen this type of
31 error ERRORS(default 3) number of times in a row, then we don't
32 want to output anything again until we've seen a success: no standard input.
33
34 Logs STDIN to /var/local/cron-errors/LOG_NAME\$error_count or
35 $HOME/cron-errors if not root, and keeps state in the same directory.
36
37 -ERRORS: ERRORS is the number of errors to accumulate before outputing the error
38 -v: Output the errors along the way to ERRORS
39
40
41 You can emulate how cronjobs work by doing this for example:
42
43 cmdname |& log-once | ifne mail -s 'cmdname failed' root@localhost
44
45 I could imagine a similar command that considers its non-option args to
46 be an error, but I haven't written it yet.
47 "
48 errors=3
49 mverr=0
50 verbose=false
51
52 while true; do
53 if [[ $1 == --help ]]; then
54 echo "$help"
55 return
56 elif [[ $1 == -v ]]; then
57 verbose=true
58 shift
59 elif [[ $1 == -[0-9]* ]]; then
60 errors=${1#-}
61 shift
62 elif [[ $1 == -- ]]; then
63 shift
64 break
65 else
66 break
67 fi
68 done
69 log_name=$1
70 # todo, make option & make them overridable based on command line or env variable
71 [[ -d $cbase ]] || mkdir -p $cbase
72 c=$cbase/$log_name
73 log=false
74 while read -r line; do
75 output+=( "$line" )
76 # If we find something that is not just a newline:
77 if [[ $line ]]; then
78 log=true
79 break
80 fi
81 done
82 glob="$c[0-9]*"
83 # file is error file indicating previous error
84 tmp=($glob); file="${tmp[0]}"
85 if [[ $file == "$glob" ]]; then
86 file=
87 fi
88 if $log; then
89 out=append
90 if [[ $file ]]; then
91 i="${file#$c}"
92 if (( i < errors )); then
93 new_file=$c$((i+1))
94 mvout=$(mv $file $new_file 2>&1) || mverr=$?
95 if (( mverr )); then
96 if [[ $mvout == *": No such file or directory" ]]; then
97 # We've very likely hit a race condition, where another
98 # log-once did the mv before us.
99 return 0
100 else
101 echo "log-once: error on mv $file $new_file"
102 return $mverr
103 fi
104 fi
105 file=$new_file
106 if [[ $file == $c$errors ]] || $verbose; then
107 out="tee -a"
108 fi
109 fi
110 else
111 file=${c}1
112 if (( errors == 1 )); then
113 out="tee -a"
114 fi
115 fi
116 $out $file <<<"log-once: $(date -R)"
117 if [[ $2 ]]; then
118 $out $file <<<"$*"
119 else
120 for o in "${output[@]}"; do
121 $out $file <<<"$o"
122 done
123 $out $file
124 fi
125 return 0
126 fi
127 if [[ $file ]]; then
128 rm -f $file
129 if [[ $file == $c$errors ]]; then
130 echo "log-once: $(date -R): success after failure for $c"
131 fi
132 fi
133 return 0
134 }
135 log-once "$@"