<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
    xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>logging — LowEndSpirit</title>
        <link>https://lowendspirit.com/</link>
        <pubDate>Mon, 09 Nov 2026 16:57:04 +0000</pubDate>
        <language>en</language>
            <description>logging — LowEndSpirit</description>
    <atom:link href="https://lowendspirit.com/discussions/tagged/logging/feed.rss" rel="self" type="application/rss+xml"/>
    <item>
        <title>cron logging that actually tells you what failed</title>
        <link>https://lowendspirit.com/discussion/11600/cron-logging-that-actually-tells-you-what-failed</link>
        <pubDate>Mon, 09 Nov 2026 16:29:03 +0000</pubDate>
        <category>Blog</category>
        <dc:creator>mikho</dc:creator>
        <guid isPermaLink="false">11600@/discussions</guid>
        <description><![CDATA[<p>A backup script has been running at 3am every night for four months. You know this because you checked <code>/var/log/syslog</code> once, back when you set it up, and saw a line like this:</p>

<pre><code>Nov 05 03:00:01 vps01 CRON[8821]: (root) CMD (/usr/local/bin/backup.sh)
</code></pre>

<p>That line only ever says one thing: cron attempted to run the command. It says nothing about whether the command succeeded, failed, timed out, or silently wrote an error to a file descriptor nobody's reading. Four months later, the backup has actually been failing since early September, because the remote storage mount it writes to started requiring a renewed auth token, and nothing told you. The only line cron ever logs is "I tried," never "it worked."</p>

<h3>Why the default cron behavior hides this</h3>

<p>Cron's actual default is almost the opposite of silent, when a job has no output redirection, cron emails the job's stdout and stderr to the local user, assuming a working mail setup exists. On most low-end VPS boxes it doesn't, there's no mail server configured, no MTA relay set up, so that email goes nowhere, isn't even an error, it just evaporates into an unconfigured <code>sendmail</code> or gets refused outright. The failure is invisible because the one place cron tries to put it has no table to land on.</p>

<h3>Step one: stop letting output disappear</h3>

<pre><code>*/10 * * * *  /usr/local/bin/backup.sh &gt;&gt; /var/log/backup.log 2&gt;&amp;1
</code></pre>

<p><code>&gt;&gt; /var/log/backup.log</code> appends stdout to a real file. <code>2&gt;&amp;1</code> redirects stderr into the same place stdout is going, so error messages land in the same log instead of vanishing into cron's broken mail path. This alone turns "no idea what happened" into "a growing log file," which is progress, but an ever-growing log with no structure is still just as unreadable as <code>/var/log/syslog</code> once it's a few thousand lines long.</p>

<h3>Step two: make the script log its own exit status</h3>

<p>The fix isn't in the crontab line, it's in the script. A backup script that only prints what it's doing, and never states clearly whether it finished successfully, forces you to read the whole log to find out. Fix that inside <code>backup.sh</code> itself:</p>

<pre><code>#!/bin/bash
LOGFILE=/var/log/backup.log
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

echo "[$TIMESTAMP] starting backup" &gt;&gt; "$LOGFILE"

if mysqldump -u root -p"$DB_PASS" bigdb | gzip &gt; /backups/bigdb-$(date +%F).sql.gz; then
    echo "[$TIMESTAMP] backup succeeded, size $(du -h /backups/bigdb-$(date +%F).sql.gz | cut -f1)" &gt;&gt; "$LOGFILE"
else
    echo "[$TIMESTAMP] backup FAILED, exit code $?" &gt;&gt; "$LOGFILE"
    exit 1
fi
</code></pre>

<p>Now the log actually answers the question instead of just recording that an attempt happened:</p>

<pre><code>[2026-09-03 03:00:01] starting backup
[2026-09-03 03:00:14] backup succeeded, size 1.2G
[2026-09-04 03:00:01] starting backup
[2026-09-04 03:00:09] backup FAILED, exit code 1
[2026-09-05 03:00:01] starting backup
[2026-09-05 03:00:08] backup FAILED, exit code 1
</code></pre>

<p>September 4th is the actual day things broke, visible in the log the morning after instead of discovered in November when a restore is needed and there's nothing recent to restore from.<br />
<img src="https://lowendspirit.com/uploads/editor/cc/dz86uz71d9gv.webp" alt="LowEndSpirit - VPS Hosting and tech forum" title="" /></p>

<h3>Step three: stop reading the log by hand every morning</h3>

<p>Nobody actually opens a log file daily just to check for the word FAILED, that habit lasts about a week. Make the failure find you instead:</p>

<pre><code>*/10 * * * * /usr/local/bin/backup.sh &gt;&gt; /var/log/backup.log 2&gt;&amp;1 || echo "backup.sh failed on $(hostname)" | mail -s "Backup failure" you@example.com
</code></pre>

<p><code>||</code> only runs the second command if the first one exits non-zero, so on a success night nothing extra happens at all. This does depend on <code>mail</code> actually being configured to send somewhere, which circles back to the exact problem cron's default behavior has, so verify this path works by forcing a failure once (<code>exit 1</code> temporarily at the top of the script) and confirming the email actually lands, not just that the command didn't error locally.</p>

<h3>The actual rule</h3>

<p>Cron answers one question by default: did it attempt to run. That's never the question you actually care about. Redirect the output somewhere real, make the script state its own result explicitly instead of trusting silence to mean success, and wire a failure path that reaches you without requiring you to go looking. Three small changes, and a failure that would have sat unnoticed for two months instead shows up the next morning.</p>
]]>
        </description>
    </item>
   </channel>
</rss>
