Using Logs to Troubleshoot Failing Cron Jobs

Let’s say you have a script that works when run in an interactive session, but does not produce expected results when run from cron. What could be the problem? Some potential culprits include:

  • The cron daemon does not see the new job. You added the job definition to a file that the cron daemon does not read.
  • The schedule is wrong. The job will run eventually, but not at your intended schedule.
  • cron’s PATH environment variable is different from the one in your interactive shell, so the script could not find a binary.
  • cron uses /bin/sh, which may be /usr/bin/dash instead of /usr/bin/bash, and your script relies on a bash-only feature.
  • The script uses relative filesystem paths and is intended to be run from a specific directory. When run by cron, the filesystem paths are all wrong.

Or it could be something else. How to troubleshoot this then, and where to start? Instead of trying fixes at random, I prefer to start by looking at logs:

  • Look at the system logs to see if cron ran the script at all.
  • Inspect the script’s stdout and stderr output for error messages and other clues.

System Logs

To check system logs on modern Linux systems using systemd, use the “journalctl” command:

journalctl -t CRON --since "today"

The “-t CRON” argument tells journalctl to show log entries with the “CRON” tag only. The “–since” parameter accepts timestamps and time durations in various formats. A couple of examples:

journalctl -t CRON --since "30 minutes ago"
journalctl -t CRON --since "2023-01-25"

If cron did run the job, you will find log entries that look like this:

jan 25 15:27:01 foo CRON[511824]: (user) CMD (/home/user/make-backup.sh)
jan 25 15:27:01 foo CRON[511823]: (CRON) info (No MTA installed, discarding output)

The first line shows the command line cron tried to run. The second line shows that the command generated some output, and cron discarded it. If the command completes without producing any output, or if your system has an MTA such as Postfix or sSMTP installed, the second line will be absent.

Checking Script’s Output

If a cron job produces output, cron will attempt to email the command’s output to the email address specified in the MAILTO= line in crontab. For this to work, the system needs to have a configured message transfer agent (MTA). See How to Send Email From Cron Jobs for instructions on how to configure sSMTP as a MTA.

Logging to System Logs

If you want to avoid the hassle of setting up a working MTA, a simpler option is to pipe the script’s output to the system log:

0 4 * * * /home/user/make-backup.sh 2>&1 | logger -t backups

Let’s analyze the above line:

  • The cron expression 0 4 * * * means “run this job ar 4:00 every day”.
  • /home/user/make-backup.sh is the script we want cron to run.
  • 2>&1 redirects sderr to stdout.
  • |, the pipe character, pipes output (both stdout and stderr, thanks to the redirection) from the previous command into the following command.
  • logger -t backups reads data from stdin and writes it to systemd logs, tagged as “backups”.

After the cron job has run, you can inspect its output with the journalctl utility:

journalctl -t backups --since "today"

To view live logs in the follow mode:

journalctl -t backups -f

Logging to Files

An even simpler option for an ad-hoc debugging session is to write the script’s output to a file. In this example, the log file gets overwritten each time the job runs, so it will only contain the output of the most recent run:

0 4 * * * /home/user/make-backup.sh > /tmp/backups.log 2>&1
  • > (right angle bracket) redirects output to a file.
  • 2>&1 redirects sderr to stdout.

Note: bash has a shorthand for redirecting stdout and stderr to a file: &> /path/to/file. dash does not support it, so it is safer to use the longer form: > /path/to/file 2>&1.

In this example, logs from each run get appended to a single file, and each line is prefixed with a timestamp:

0 4 * * * /home/user/make-backup.sh 2>&1 | ts >> /tmp/backups.log
  • 2>&1 redirects sderr to stdout.
  • The combined output is piped to ts, which prefixes each line with a timestamp.
  • Finally, >> redirects output to a file in append mode.

Note: The “ts” utility may not be installed by default. For Debian-based systems, it is available in the “moreutils” package.

Logging to Healthchecks.io

And yet another option is to forward the logs to Healthchecks.io and view them in the web browser. With this option, you also get external monitoring and notifications when your job does not complete on schedule or exits with non-zero exit status:

0 4 * * * m=$(/home/user/make-backup.sh 2>&1); curl --data-raw "$m" https://hc-ping.com/<uuid>/$?
  • The m=$(...) syntax runs the enclosed command and assigns its output to the variable $m
  • The semicolon separates two consecutive commands (without piping one’s output to the other)
  • curl’s --data-raw "$m" parameter sends the contents of $m in HTTP request body
  • $? is the exit status of the previous command. We tack it onto the URL so Healthchecks knows if the command finished successfully (exit status 0) or unsuccessfully (exit status > 0).

If the script produces a lot of output, you may get an error:

/usr/bin/curl: Argument list too long

In that case, one workaround is to save the output to a temporary file, then tell curl to read request body from the file:

0 4 * * * /home/user/make-backup.sh > /tmp/backups.log 2>&1; curl --data-binary @/tmp/backups.log https://hc-ping.com/<uuid>/$?

As the command starts to get a little unwieldly, you may also consider replacing curl with runitor, a command runner with Healthchecks.io integration:

0 4 * * * runitor -uuid <uuid> -- /home/user/make-backup.sh 

That is all for now,
Happy scripting,
Pēteris