Skip to content

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 journald
  • logger - 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:

DEBUG=true bash my_script.sh

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:

systemctl status systemd-journald

2. Logs don't appear immediately?

Add --flush to force writing:

logger -t test "Test message"
journalctl --flush
journalctl -t test

3. Debugging with verbose:

journalctl -t my_script -o verbose --no-pager

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 journald
  • logger - 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.