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 10000keeps 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/$rcreports the exit code: 0 is success, anything else alerts immediately.|| truekeeps 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]}
teewrites the output to the temporary file and passes it on tologger.logger -t import-orderstags each line, so you can find it later withjournalctl -t import-ordersorgrep import-orders /var/log/syslog.${PIPESTATUS[0]}is the exit status of the first command in the pipeline, the job itself, regardless of whatteeandloggerreturned.
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 -xin 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 islogger's status. Use${PIPESTATUS[0]}. PIPESTATUS: unbound variableor syntax errors. The script runs undersh, 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
- Cron Job Exit Code MonitoringSend a cron job's exit code to Cuckoo via /ping/YOUR-UUID/$?, attach the log tail with curl --data-binary and use set -o pipefail so pipes can't hide errors.
- Crontab and curl: Add a Cuckoo Ping to Any JobAdd a heartbeat ping to crontab entries with curl: safe flags, && chaining, when to use || true, PATH and environment pitfalls, MAILTO and tested examples.
- Monitor Certbot Certificate RenewalMonitor certbot renew from a systemd timer or cron: why --deploy-hook and --post-hook fall short, and how to report the exit code (plus acme.sh).
- Monitor systemd Timers with CuckooMonitor systemd timers with heartbeat pings: ExecStartPre and ExecStartPost signals, OnFailure= or ExecStopPost with SERVICE_RESULT, Persistent= and journalctl.