1

I have a nginx access.log There is very infrequent traffic, so I want to get a mail on each access. I've tried

tail -f access.log | cat

That works. So piping with tail seems ok.

With following nothing happens:

tail -f access.log | mail -s "Dateizugriff" <Destinationaddress>

Neither with

tail -f access.log | grep --line-buffered '.*' | mail -s "Dateizugriff" <Destinationaddress>

nor

stdbuf -oL -eL  tail -f access.log | mail -s "Dateizugriff" <Destinationaddress>

works Of course

echo "test" | mail -s "Dateizugriff" <Destinationaddress>

works.

Then I've tried xargs

tail -f access.log | xargs -I % mail -s "Dateizugriff" <Destinationaddress>

Now I've got one mail for each line but without any content

tail -f access.log | xargs -I % | echo "%" |  mail -s "Dateizugriff" <Destinationaddress>

doesn't work. Nothing is happening at all. Following also doesn't work:

tail -f access.log | xargs |  mail -s "Dateizugriff" <Destinationaddress>

So whats the Trick to get it work?

2

The problem is that tail -f never terminates -- it just waits for input forever. So the pipe remains open, mail never gets EOF, and it just waits for the message to "complete", which it never can.

Adding other processes in the pipe does not help, unless at least one of them exits and breaks the pipe.

If you are certain you want every single line added to the log to be in a separate mail, you can use:

tail -f access.log | head -n 1 | mail -s "Dateizugriff" <user@domain>

Head will exit as soon as it reads one line, tail will get SIGPIPE and exit, mail will get EOF and carry on with sending the mail.

If a log entry can be multiline, but you can always recognise the last line, you could use sed with a q condition to break the pipe.

First downside is that you have a mail process hanging permanently for the next line to arrive.

Second downside is that you will have to recognise that the process has completed every time, and restart the whole thing.

Personally, I would write a shell loop that repeatedly tried to find any added lines, inside a loop that included a sleep (maybe 60 seconds). When the file size increases, it can run tail without -f so it does not hang.

Initialise nLine before the loop, to the current number of lines. The redirection is important, because it stops wc reporting the filename with the count.

nLine="$( wc -l < access.log )"

while : ; do
    NEW=$( tail -n +$(( 1 + nLine )) access.log )
    [[ "${#NEW}" -gt 0 ]] && {
        echo "${NEW}" | mail ....
        nLine=$(( nLine + $( wc -l <<<${NEW} ) ))
    }
    sleep 60
done
2
  • Thanks for the detailed answer. A one liner like tail -f access.log | head -n 1 | mail -s "Dateizugriff" <user@domain> fits perfectly. However this terminates after execution and sends only the first line tail -f returns.
    – Hannes
    Dec 2 '19 at 17:20
  • Precisely. Tail can't do this by itself, either with -f (which hangs) or with -n (which does not know how many lines you expect, and keeps exiting). That's why I noted two "downsides" in my answer above. There is a third downside: if the arrival rate of new lines is higher than you can deal with by restarting the process for every line, you never catch up. So I posted 10 lines of code that does not hang, and deals with a burst of messages in one email. Did you try it? Dec 2 '19 at 17:44
0

Since you probably want to have this "monitoring" process start automatically on each reboot of the system, I would think that a systemd path is a reasonable way to perform this.

This means, you would write a file monitor_nginx_access.path with a content like

[Unit]
Description=Alert admin about access

[Path]
PathChanged=/absolute/path/to/access.log

and a service of the same name, monitor_nginx_access.service, which contains basically is a wrapper to a shell-script that does the magic:

[Unit]
Description=Send mail when access log changes

[Service]
ExecStart=/absolute/path/to/mailscript.sh

Both of these files would be placed in /etc/systemd/system or wherever your installation expects admin-defined systemd units (cf. the man page).

The mailscript.sh would then contain something like

#!/bin/bash
MAILLINES=5  # how many of the last lines of access.log to mail
...

tail -n "$MAILLINES" /absolute/path/to/access.log | mail -s <your arguments here>
3
  • Setting up a service is (probably) a good idea, although the OP might not have inotify installed. However, tail -n does not fix the original problem: the script still needs to retain a record of the number of lines last seen, so it can sent the actual additions. Otherwise, it can repeat lines in the case of a shorter append, or omit them for a large append. It would also be useful to consider the case where the file is truncated or rolled over. Nov 29 '19 at 13:37
  • @Paul_Pedant It is true that tail -n is too static to be useful in the general case, however since the OP noted there was "infrequent" traffic, I considered it safe to assume that every file change corresponds to only one access, and assumed the number of log lines is usually the same for each such event. As for inotify: I would be surprised if there were any systemd installations that don't pull this in as a required dependency, and systemd is pretty much standard nowadays ...
    – AdminBee
    Nov 29 '19 at 14:03
  • I last used this method in a situation where I had to tail logs remotely on 160 servers via rsh (about 140 in Solaris 2.6 and 20 in AIX 4.5, IIRC). So no inotify available, it does not work on remotes anyway, and variable append size every time. I had to schedule the rotation too, to take account of the priority of servers over workstations. It is a resilient method and has few dependencies. It also gave me a remote system failure indication for free. Nov 29 '19 at 18:22
0

Have a look at this answer: https://stackoverflow.com/a/4657782/12332118 I have tested it, it works for me. It does exactly what you want, without systemd.

1
  • 1
    Welcome to the site, and thank you for your contribution. Please note that while this may answer the question, it is better to include a summary of the externally linked content here. Even external content within the wider SE framework can become invalid, or their content change.
    – AdminBee
    Nov 26 '20 at 16:07
0

As Max Müller linked to, you can do

tail -f access.log | grep --line-buffered GET | 
while read line 
do 
echo $line | mail -s New Access <Destinationaddress>
done

or if you prefer it in one line use my solution

tail -f access.log | awk -W interactive '/GET/{system("echo "$0" | mail -s \"New Access\" <Destinationaddress>")}'

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.