Logging to journald from Bash
When creating system tools, daemons, or services running on systemd based systems, it's worth logging events directly to journald. This way, logs go to the central system journal.
In this article, I'll show how to log to journald in Bash using systemd-cat, as well as directly via logger and journalctl.
Requirements
All tools are available in standard distributions with systemd:
systemd-cat- redirect stdout/stderr to journaldlogger- standard syslog/journald tool
Method 1: systemd-cat
Parameters:
-t IDENTIFIER- log source identifier (e.g., script name)-p PRIORITY- priority level (name or number)-f FACILITY- syslog facility (default:user)
Example with different levels:
#!/bin/bash
echo "App started" | systemd-cat -t my_app -p info
echo "Error occured!" | systemd-cat -t my_app -p err
echo "Low memory" | systemd-cat -t my_app -p warning
Method 2: logger
Standard Unix/Linux tool for logging via syslog/journald:
#!/bin/bash
logger -t my_script "Script started"
# With specified level
logger -t my_script -p user.err "Error occurred"
logger -t my_script -p user.warning "Warning: low memory"
logger -t my_script -p user.info "just info message"
logger parameters:
-t TAG- log source identifier-p FACILITY.PRIORITY- facility and level (e.g.,user.err,daemon.info)-s- also print to stderr
PRIORITY levels:
| Name | Number | Description |
|---|---|---|
emerg |
0 | Critical - system unusable |
alert |
1 | Requires immediate action |
crit |
2 | Critical error |
err |
3 | Error |
warning |
4 | Warning |
notice |
5 | Important information |
info |
6 | Informational |
debug |
7 | Debugging |
Method 3: Helper functions 🔧
For larger scripts, it's worth creating logging functions:
#!/bin/bash
SCRIPT_NAME="my_service"
log_info() {
logger -t "$SCRIPT_NAME" -p user.info "$1"
}
log_warning() {
logger -t "$SCRIPT_NAME" -p user.warning "$1"
}
log_error() {
logger -t "$SCRIPT_NAME" -p user.err "$1"
}
log_debug() {
logger -t "$SCRIPT_NAME" -p user.debug "$1"
}
log_info "Script started"
log_warning "Low memory: $(free -h | awk 'NR==2{print $3}')"
log_error "Cannot connect to database"
Advanced functions with extra features:
#!/bin/bash
SCRIPT_NAME="$(basename "$0")"
DEBUG_MODE=${DEBUG:-false}
log_with_level() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
logger -t "$SCRIPT_NAME" -p "user.$level" "$message"
if [[ "$DEBUG_MODE" == "true" ]]; then
echo "[$timestamp] [$level] $message"
fi
}
log_info() { log_with_level "info" "$1"; }
log_warn() { log_with_level "warning" "$1"; }
log_error() { log_with_level "err" "$1"; }
log_debug() {
if [[ "$DEBUG_MODE" == "true" ]]; then
log_with_level "debug" "$1"
fi
}
log_info "App started"
log_warn "Low memory"
log_error "Connection error"
log_debug "Configured PATH: $PATH"
Run with debugging:
where DEBUG is an environment variable that enables extra console logging, and my_script.sh is the script name.
The result will be logs displayed both in the console and in journald, e.g.:
[2025-06-01 12:00:00] [info] App started
[2025-06-01 12:00:01] [warning] Low memory
[2025-06-01 12:00:02] [err] Connection error
[2025-06-01 12:00:03] [debug] Configured PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Reading logs in journalctl 🔍
Check logs:
# All logs for a given identifier
journalctl -t my_script
# Only errors and critical
journalctl -t my_script -p err
# Last 50 entries
journalctl -t my_script -n 50
# Live follow
journalctl -t my_script -f
# Full data (with PRIORITY, PID, etc.)
journalctl -t my_script -o verbose
# Logs from the last hour
journalctl -t my_script --since "1 hour ago"
Method 4: Logging the entire script
Redirect the entire script output to journald:
#!/bin/bash
exec > >(systemd-cat -t my_script -p info)
exec 2> >(systemd-cat -t my_script -p err)
echo "This is an info message"
echo "This is an error" >&2
Example with signal handling:
#!/bin/bash
SCRIPT_NAME="my_daemon"
exec > >(systemd-cat -t "$SCRIPT_NAME" -p info)
exec 2> >(systemd-cat -t "$SCRIPT_NAME" -p err)
cleanup() {
logger -t "$SCRIPT_NAME" -p user.info "Stopped due to signal"
exit 0
}
trap cleanup SIGTERM SIGINT
echo "Daemon started (PID: $$)"
while true; do
echo "Doing task..."
sleep 3
echo "Task done"
done
Troubleshooting
1. No logs in journalctl?
Check if journald is running:
2. Logs don't appear immediately?
Add --flush to force writing:
3. Debugging with verbose:
4. Check available facilities:
# Standard facilities
logger -t test -p mail.info "Test mail facility"
logger -t test -p daemon.warning "Test daemon facility"
logger -t test -p local0.err "Test local0 facility"
Summary ✅
systemd-cat- simple way to redirect stdout/stderr to journaldlogger- flexible tool for logging with different levels- helper functions - make log management easier in scripts
journalctl- filter logs by identifier (-t) and level (-p)- signal handling - important for daemons and long-running scripts
For simple scripts, use systemd-cat; for more complex solutions, use logger with helper functions.