How to Write Logs From a Shell Script to syslog and journald
Log from a shell script to syslog and journald with logger, set tags and priorities, read the entries back with journalctl, and keep stdout clean for data.

A script that runs from cron or a timer has no terminal, so echo goes nowhere useful. Sending messages to syslog puts them next to the system's own logs, subject to the same rotation and retention. The commands below come from util-linux logger and systemd 255-era journalctl, and fit the log habits in a logging function for Bash.
Write a message
logger -t nightly-backup -p user.info "backup started"
logger -t nightly-backup -p user.err "backup failed: disk full"-t sets the tag, which is the label you filter on later. -p facility.level sets the priority; the levels from most to least severe are emerg, alert, crit, err, warning, notice, info and debug. Add -i to record the process ID, which helps when two runs overlap.
Read it back
journalctl -t nightly-backup --since "1 hour ago"
journalctl -t nightly-backup -p errNov 03 02:00:01 host nightly-backup[4121]: backup started
Nov 03 02:00:09 host nightly-backup[4121]: backup failed: disk full
-p err shows entries at that level and more severe. If the machine does not run journald, the same messages are in /var/log/syslog or /var/log/messages, depending on the distribution; see journalctl and the files under /var/log.
Capture a whole command
/usr/local/bin/sync-data 2>&1 | logger -t sync-data -p user.noticeEach line the command prints becomes one log entry. Merging stderr first with 2>&1 keeps error output in the log. The pipeline's exit status is that of logger unless pipefail is set, so run set -o pipefail and check the status if the command's failure matters.
Add structured fields
echo "job finished" | systemd-cat -t nightly-backup -p infosystemd-cat connects stdin to the journal directly. Recent util-linux versions add logger --journald, which accepts FIELD=value lines so entries can be filtered by custom fields, a feature documented in the logger manual page. The journal's own fields are explained in the systemd journal fields reference.
Facilities, levels and what a filter sees
Every syslog message carries a facility and a severity. The facility is a category inherited from the old days: auth, cron, daemon, mail, user and eight local0 to local7 slots for site use. Scripts normally use user or one of the local slots, so their messages do not mix with system daemons. A separate local3 facility for the backup tooling, for example, lets a log forwarder route those lines to a different file or collector with one rule, without parsing message text.
The severity decides what gets kept. Many systems store everything, but a forwarder may ship only warning and above, and a monitoring rule may page on err. Use the levels with meaning: info for progress a human might want, warning for something odd that did not stop the work, err for a failed step, and crit only for a failure that needs action now. A script that logs every line at err trains people to ignore the channel.
Making log lines useful
The text of a message is read months later by someone who was not there. Put the verb and the object in it, the identifier of the thing being processed and, on failure, the reason. "backup failed" helps no one, and "backup of /srv/data to s3://bucket failed: AccessDenied after 3 tries" can be acted on. Use a stable format such as key=value pairs, job=nightly step=upload status=failed code=403, which is easy to search with grep and to parse in a log platform without a custom pattern. Never log secrets, and be careful with command lines, which often hold passwords and tokens in arguments.
Include a run identifier when jobs can overlap or repeat: RUN=$(date +%s)-$$ at the start and the value in every message lets journalctl --grep pull out a single execution from interleaved ones.
Rate, size and truncation
A message sent through the syslog protocol is limited in length, commonly around 1 KiB in traditional UDP and 8 KiB with the journal, and longer lines are split or cut. A script that dumps a large output into one line loses the tail. Send multi-line output line by line, by piping into logger, and keep each line short. Also keep an eye on volume: a loop that logs on every iteration across a million records can overflow journal rate limits, and systemd-journald drops messages once a service exceeds RateLimitBurst in the interval, replacing them with a notice that some were suppressed. Log at the level of steps and summaries, and write bulk detail to a file that the script rotates.
Where the messages end up
On a systemd host, logger writes to the journal through the syslog socket, and the journal may also forward to rsyslog, which writes the traditional files. Retention is set in journald.conf with SystemMaxUse and by logrotate for the files. If the logs must outlive the host, forward them to a central system with rsyslog or a shipper, and test that a message with the facility you chose actually arrives. A quick test line from the script, logger -t mytag "test", followed by a search at the destination, settles it in a minute. Include in the runbook the exact journalctl command to find the script's lines, so whoever is on call at night can use it without reading the code.
A logging helper with levels
A script with many log calls benefits from a function that adds the tag, the level and optionally the terminal output, so the call sites stay short. The helper below writes to the journal and also to stderr when the script runs in a terminal.
TAG=${TAG:-${0##*/}}
log() {
local level=$1; shift
logger -t "$TAG" -i -p "user.$level" -- "$*"
[[ -t 2 ]] && printf '%s: %s\n' "$level" "$*" >&2
return 0
}
log info "starting"
log err "upload failed for $file"The -- keeps a message that starts with a dash from being read as an option, and [[ -t 2 ]] prints to the terminal only when someone is watching, which keeps cron mail empty on success. The explicit return 0 stops a failed terminal test from becoming the function's status under set -e.
Where this does not apply
If the script runs as a systemd service, anything it writes to stdout and stderr already goes to the journal under the unit name, and adding logger only duplicates lines. Journals are not persistent by default on every distribution, so entries can vanish at reboot unless /var/log/journal exists, as covered in persistent journald storage. Do not log secrets: syslog is readable by administrators and often forwarded to other machines, which is the point of forwarding logs with rsyslog.
For scripts that run on a schedule, pick the tag once at the top and reuse it, for example readonly TAG=nightly-backup, so that one journalctl -t query shows every run. Log the start, each major step and the final status, including the exit code, and keep each message on a single line so that filtering and forwarding stay predictable.


