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.