Cuckoo

Send Cron Job Logs with a Cuckoo Ping

Updated

An alert that says "the nightly import failed" starts an investigation. An alert that also shows the last lines of the import's output often ends it: the error message is right there. Cron jobs rarely make that easy. Their output goes to cron mail that nobody configured, or to a log file on a server you first have to log in to.

This guide shows how to capture a job's output, attach the relevant part to the Cuckoo ping, keep secrets out of it, and still keep the full log locally.

How Cuckoo stores output

Any ping to https://cuckoo.metastic.ch/ping/YOUR-UUID (and its /start, /fail and exit code variants) can be a POST request. Cuckoo stores the first 10 KB of the body as UTF-8 text with the ping, visible in the check's history. Because the useful part of a log is usually at the end, send the tail of the output.

Step 1: Capture stdout and stderr

Most errors are written to stderr, so redirect both streams:

/usr/local/bin/import-orders > "$LOG" 2>&1

The order matters. > file 2>&1 sends both to the file; 2>&1 > file would send stderr to the old stdout (cron mail) and only stdout to the file.

Step 2: POST the tail with the exit code

#!/usr/bin/env bash
# /usr/local/bin/import-orders-monitored
set -uo pipefail

URL="https://cuckoo.metastic.ch/ping/YOUR-UUID"
LOG=$(mktemp)
trap 'rm -f "$LOG"' EXIT

curl -fsS -m 10 --retry 5 -o /dev/null "$URL/start" || true

/usr/local/bin/import-orders > "$LOG" 2>&1
rc=$?

tail -c 10000 "$LOG" \
  | curl -fsS -m 10 --retry 5 -o /dev/null \
      -H "Content-Type: text/plain; charset=utf-8" \
      --data-binary @- "$URL/$rc" || true

exit "$rc"
  • tail -c 10000 keeps the last 10,000 bytes, which fits the 10 KB that Cuckoo stores.
  • --data-binary @- reads the body from stdin and keeps newlines intact. -d @- would strip them.
  • $URL/$rc reports the exit code: 0 is success, anything else alerts immediately.
  • || true keeps a failed ping from changing the job's result.

If the cut lands in the middle of a multi-byte character, the first line may be garbled. Append | sed 1d after tail to drop the partial first line.

Step 3: Keep the full log with logger

The ping body is a summary, not an archive. Send the complete output to syslog or the systemd journal at the same time with logger, and still capture the job's own exit code:

/usr/local/bin/import-orders 2>&1 | tee "$LOG" | logger -t import-orders
rc=${PIPESTATUS[0]}
  • tee writes the output to the temporary file and passes it on to logger.
  • logger -t import-orders tags each line, so you can find it later with journalctl -t import-orders or grep import-orders /var/log/syslog.
  • ${PIPESTATUS[0]} is the exit status of the first command in the pipeline, the job itself, regardless of what tee and logger returned.

On systemd hosts, systemd-cat -t import-orders is an alternative to logger with the same effect.

Step 4: Keep secrets out of the body

Logs leak more than you think: connection strings, bearer tokens, password= arguments printed by a verbose tool. The safest fix is not to print them in the first place. As a second line of defence, redact common patterns before sending. The I (case-insensitive) flag needs GNU sed or a recent BSD sed:

redact() {
  sed -E \
    -e 's#([a-z][a-z0-9+.-]*://)[^/@[:space:]]+@#\1[REDACTED]@#Ig' \
    -e 's/((password|passwd|secret|token|api[_-]?key)["'\'']?[[:space:]]*[=:][[:space:]]*["'\'']?)[^[:space:],;"'\'']+/\1[REDACTED]/Ig' \
    -e 's/(Authorization:[[:space:]]*)[^[:space:]]+([[:space:]]+[^[:space:]]+)?/\1[REDACTED]/Ig'
}

redact < "$LOG" | tail -c 10000 | curl -fsS -m 10 --retry 5 -o /dev/null \
  --data-binary @- "https://cuckoo.metastic.ch/ping/YOUR-UUID/$rc" || true

Other good habits:

  • Avoid set -x in jobs whose output is sent anywhere; it prints every expanded command, including secrets.
  • Pass credentials through files or environment variables, not command-line arguments that tools may echo.
  • Strip colour codes so the stored text is readable: sed 's/\x1b\[[0-9;]*m//g'.

Using it from cron

SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=""
*/30 * * * * /usr/local/bin/import-orders-monitored

MAILTO="" turns off cron mail because output now goes to Cuckoo and the journal. Leave it set if you want cron mail as an extra channel.

Schedule and grace advice

Configure the check with the same cron expression (*/30 * * * *) and the server's IANA time zone. Give it a grace time that covers the longest normal run, for example 10 minutes for a job that takes up to five, and set a max runtime if the job can hang, since it already sends /start.

Troubleshooting

  • Body is empty in the dashboard. The job wrote nothing, or output went elsewhere. Check the redirection order.
  • Only the beginning of the log is stored. You sent the whole file; Cuckoo keeps the first 10 KB. Use tail -c.
  • Exit code is always 0 with tee. You used $?, which is logger's status. Use ${PIPESTATUS[0]}.
  • PIPESTATUS: unbound variable or syntax errors. The script runs under sh, not bash. Use a bash shebang.

FAQ

Can I send the log with the success ping too, or only on failure?

Both work. Sending it every time gives you a history of normal runs to compare against; sending it only on failure keeps the stored data minimal. Either way, use POST to the ping URL with the exit code.

Is the ping body encrypted in transit?

Yes, the ping URL uses HTTPS. Still treat the body as data you are sharing with a third party, which is why redaction and avoiding secrets in logs matter.

Can I send JSON instead of plain text?

The body is stored as text, so JSON lines are fine to send. Keep it human-readable, because the main purpose is to read it quickly when an alert fires.

Related guides

All guides · Compare Cuckoo with other tools

Send feedback

Ideas, bugs, questions: we read every message. Or write to hello@metastic.ch.