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