Skip to content

Instantly share code, notes, and snippets.

@GingerGraham
Last active January 19, 2026 15:23
Show Gist options
  • Select an option

  • Save GingerGraham/99af97eed2cd89cd047a2088947a5405 to your computer and use it in GitHub Desktop.

Select an option

Save GingerGraham/99af97eed2cd89cd047a2088947a5405 to your computer and use it in GitHub Desktop.
Bash Logging

Bash Logging Module

A flexible, reusable logging module for Bash scripts that provides standardized logging functionality with various configuration options.

Important

This version of the bash logging module is now deprecated and the gist is no longer maintained. Please see the full release bash-logger

Note

This deprecated version has been left for reference purposes only. Supporting materials have been removed and replaced with improved versions in the new repository.

Features

  • Standard syslog log levels (DEBUG, INFO, WARN, ERROR, CRITICAL, etc.)
  • Console output with color-coding by severity
  • Configurable stdout/stderr output stream split
  • Optional file output
  • Optional systemd journal logging
  • Customizable log format
  • UTC or local time support
  • INI configuration file support
  • Runtime configuration changes
  • Special handling for sensitive data

Installation

Simply place the logging.sh file in a directory of your choice.

Basic Usage

# Source the logging module
source /path/to/logging.sh

# Initialize the logger with defaults
init_logger

# Log messages at different levels
log_debug "This is a debug message"
log_info "This is an info message"
log_warn "This is a warning message"
log_error "This is an error message"
log_fatal "This is a fatal error message"

License

This module is provided under the MIT License.

Copyright © 2026 Graham Watts
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#!/bin/bash
#
# all_demo.sh - Comprehensive demonstration of logging module features
#
# This script demonstrates all features of the logging module including:
# - Log levels
# - Formatting options
# - UTC time
# - Journal logging
# - Color settings
# - Stderr level configuration (stdout vs stderr output control)
# Get script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PARENT_DIR="$(dirname "$SCRIPT_DIR")"
# Path to logger module
LOGGER_PATH="${SCRIPT_DIR}/logging.sh" # uncomment if logger is in same directory
# Check if logger exists
if [[ ! -f "$LOGGER_PATH" ]]; then
echo "Error: Logger module not found at $LOGGER_PATH" >&2
exit 1
fi
# Create log directory
LOGS_DIR="${PARENT_DIR}/logs"
mkdir -p "$LOGS_DIR"
LOGGING_FILE="${LOGS_DIR}/all_demo.log"
echo "Log file is at $LOGGING_FILE"
# Source the logger module
echo "Sourcing logger from: $LOGGER_PATH"
source "$LOGGER_PATH"
# Function to test all log levels
test_all_log_levels() {
local reason="$1"
echo "Testing all log messages ($reason)"
# All syslog standard levels from least to most severe
log_debug "This is a DEBUG message (level 7)"
log_info "This is an INFO message (level 6)"
log_notice "This is a NOTICE message (level 5)"
log_warn "This is a WARN message (level 4)"
log_error "This is an ERROR message (level 3)"
log_critical "This is a CRITICAL message (level 2)"
log_alert "This is an ALERT message (level 1)"
log_emergency "This is an EMERGENCY message (level 0)"
# Special logging types
log_sensitive "This is a SENSITIVE message (console only)"
echo
}
# Test log messages with a specific format
test_format() {
local format="$1"
local description="$2"
echo -e "\n========== Using format: \"$format\" =========="
echo "$description"
# Update the format
set_log_format "$format"
# Log example messages
log_info "This is an example informational message"
log_error "This is an example error message"
}
# Function to check if logger command is available
check_logger_availability() {
if command -v logger &>/dev/null; then
echo "✓ 'logger' command is available for journal logging"
LOGGER_AVAILABLE=true
else
echo "✗ 'logger' command is not available. Journal logging features will be skipped."
LOGGER_AVAILABLE=false
fi
}
# ====================================================
# PART 1: Log Levels Demo
# ====================================================
echo "========== PART 1: Log Levels Demo =========="
# Initialize with default level (INFO)
echo "========== Initializing with default level (INFO) =========="
init_logger --log "${LOGGING_FILE}" || {
echo "Failed to initialize logger" >&2
exit 1
}
test_all_log_levels "with default INFO level"
# Initialize with DEBUG level
echo "========== Setting level to DEBUG =========="
set_log_level "DEBUG"
test_all_log_levels "with DEBUG level"
# Initialize with WARN level
echo "========== Setting level to WARN =========="
set_log_level "WARN"
test_all_log_levels "with WARN level"
# Initialize with ERROR level
echo "========== Setting level to ERROR =========="
set_log_level "ERROR"
test_all_log_levels "with ERROR level"
# Test initialization with level parameter
echo "========== Reinitializing with WARN level =========="
init_logger --log "${LOGGING_FILE}" --level WARN || {
echo "Failed to initialize logger" >&2
exit 1
}
test_all_log_levels "after init with --level WARN"
# Test verbose flag
echo "========== Reinitializing with --verbose =========="
init_logger --log "${LOGGING_FILE}" --verbose || {
echo "Failed to initialize logger" >&2
exit 1
}
test_all_log_levels "after init with --verbose (DEBUG level)"
echo "========== Log Level Demo Complete =========="
# ====================================================
# PART 2: Formatting Demo
# ====================================================
echo -e "\n========== PART 2: Formatting Demo =========="
init_logger --log "${LOGGING_FILE}" --level INFO || {
echo "Failed to initialize logger" >&2
exit 1
}
# Show the default format first
echo "Default format: \"$LOG_FORMAT\""
log_info "This is the default log format"
# Test various formats
test_format "%l: %m" "Basic format with just level and message"
test_format "[%l] [%s] %m" "Format without timestamp"
test_format "%d | %-5l | %m" "Format with aligned level"
test_format "{\"timestamp\":\"%d\", \"level\":\"%l\", \"script\":\"%s\", \"message\":\"%m\"}" "JSON-like format"
test_format "$(hostname) %d [%l] (%s) %m" "Format with hostname"
# Test initialization with format parameter
echo -e "\n========== Initializing with custom format =========="
init_logger --log "$LOGGING_FILE" --format "CUSTOM: %d [%l] %m" || {
echo "Failed to initialize logger" >&2
exit 1
}
log_info "This message uses the format specified during initialization"
echo -e "\n========== Format Demo Complete =========="
# ====================================================
# PART 3: Timezone Demo
# ====================================================
echo -e "\n========== PART 3: Timezone Demo =========="
echo "This demonstrates the use of UTC time in log messages."
# Initialize with UTC time
echo "========== Initializing with UTC Time =========="
init_logger --log "${LOGGING_FILE}" --format "%d %z [%l] [%s] %m" --utc || {
echo "Failed to initialize logger" >&2
exit 1
}
# Log some messages
log_info "This message shows the timestamp in UTC time"
log_warn "This is another message with UTC timestamp"
# Revert back to local time
echo "========== Setting back to local time =========="
set_timezone_utc "false"
# Log some messages
log_info "This message shows the timestamp in local time"
log_warn "This is another message with local timestamp"
echo "========== Timezone Demo Complete =========="
# ====================================================
# PART 4: Journal Logging Demo
# ====================================================
echo -e "\n========== PART 4: Journal Logging Demo =========="
# Check if logger command is available
check_logger_availability
if [[ "$LOGGER_AVAILABLE" == true ]]; then
# Initialize with journal logging enabled
echo "========== Initializing with journal logging =========="
init_logger --log "${LOGGING_FILE}" --journal || {
echo "Failed to initialize logger" >&2
exit 1
}
# Log with default tag (script name)
log_info "This message is logged to the journal with default tag"
log_warn "This warning message is also sent to the journal"
log_error "This error message should appear in the journal too"
# Test with custom tag
echo "========== Reinitializing with custom journal tag =========="
init_logger --log "${LOGGING_FILE}" --journal --tag "demo-logger" || {
echo "Failed to initialize logger" >&2
exit 1
}
log_info "This message is logged with the tag 'demo-logger'"
log_warn "This warning uses the custom tag in the journal"
# Test sensitive logging (shouldn't go to journal)
echo "========== Testing sensitive logging with journal enabled =========="
log_sensitive "This sensitive message should NOT appear in the journal"
# Test disabling journal logging
echo "========== Disabling journal logging =========="
set_journal_logging "false"
log_info "This message should NOT appear in the journal (it's disabled)"
# Re-enable and change tag
echo "========== Re-enabling journal and changing tag =========="
set_journal_logging "true"
set_journal_tag "new-tag"
log_info "This message should use the 'new-tag' tag in the journal"
echo "========== Journal Demo Complete =========="
echo "Journal logs can be viewed with: journalctl -t demo-logger -t new-tag"
else
echo "Skipping journal logging demo as 'logger' command is not available."
fi
# ====================================================
# PART 5: Color Settings Demo
# ====================================================
echo -e "\n========== PART 5: Color Settings Demo =========="
# Default auto-detection mode
echo "========== Default color auto-detection mode =========="
init_logger --log "${LOGGING_FILE}" || {
echo "Failed to initialize logger" >&2
exit 1
}
# Show current color mode
log_info "Current color mode: $USE_COLORS (auto-detection)"
test_all_log_levels "with auto-detected colors"
# Force colors on with --color
echo "========== Forcing colors ON with --color =========="
init_logger --log "${LOGGING_FILE}" --color || {
echo "Failed to initialize logger" >&2
exit 1
}
# Show current color mode
log_info "Current color mode: $USE_COLORS (forced on)"
test_all_log_levels "with colors forced ON"
# Force colors off with --no-color
echo "========== Forcing colors OFF with --no-color =========="
init_logger --log "${LOGGING_FILE}" --no-color || {
echo "Failed to initialize logger" >&2
exit 1
}
# Show current color mode
log_info "Current color mode: $USE_COLORS (forced off)"
test_all_log_levels "with colors forced OFF"
# Change color mode at runtime
echo "========== Changing color mode at runtime =========="
set_color_mode "always"
log_info "Color mode changed to: $USE_COLORS (always)"
log_warn "This warning should be colored"
log_error "This error should be colored"
set_color_mode "never"
log_info "Color mode changed to: $USE_COLORS (never)"
log_warn "This warning should NOT be colored"
log_error "This error should NOT be colored"
set_color_mode "auto"
log_info "Color mode changed to: $USE_COLORS (auto-detection)"
log_warn "This warning may be colored depending on terminal capabilities"
log_error "This error may be colored depending on terminal capabilities"
echo "========== Color Settings Demo Complete =========="
# ====================================================
# PART 6: Stderr Level Demo
# ====================================================
echo -e "\n========== PART 6: Stderr Level Demo =========="
echo "This demonstrates configuring which log levels go to stderr vs stdout."
echo "By default, ERROR and above go to stderr, while lower levels go to stdout."
# Default stderr level (ERROR)
echo -e "\n========== Default stderr level (ERROR and above to stderr) =========="
init_logger --log "${LOGGING_FILE}" --level DEBUG || {
echo "Failed to initialize logger" >&2
exit 1
}
echo "Running: some_script.sh 2>/dev/null (suppressing stderr)"
echo "You should see DEBUG, INFO, NOTICE, WARN but NOT ERROR, CRITICAL, ALERT, EMERGENCY:"
(
log_debug "DEBUG goes to stdout"
log_info "INFO goes to stdout"
log_notice "NOTICE goes to stdout"
log_warn "WARN goes to stdout"
log_error "ERROR goes to stderr (hidden)"
log_critical "CRITICAL goes to stderr (hidden)"
log_alert "ALERT goes to stderr (hidden)"
log_emergency "EMERGENCY goes to stderr (hidden)"
) 2>/dev/null
echo -e "\nRunning: some_script.sh 1>/dev/null (suppressing stdout)"
echo "You should see ERROR, CRITICAL, ALERT, EMERGENCY but NOT DEBUG, INFO, NOTICE, WARN:"
(
log_debug "DEBUG goes to stdout (hidden)"
log_info "INFO goes to stdout (hidden)"
log_notice "NOTICE goes to stdout (hidden)"
log_warn "WARN goes to stdout (hidden)"
log_error "ERROR goes to stderr"
log_critical "CRITICAL goes to stderr"
log_alert "ALERT goes to stderr"
log_emergency "EMERGENCY goes to stderr"
) 1>/dev/null
# Set stderr level to WARN
echo -e "\n========== Setting stderr level to WARN =========="
init_logger --log "${LOGGING_FILE}" --level DEBUG --stderr-level WARN || {
echo "Failed to initialize logger" >&2
exit 1
}
echo "Running: some_script.sh 2>/dev/null (suppressing stderr)"
echo "You should see DEBUG, INFO, NOTICE but NOT WARN and above:"
(
log_debug "DEBUG goes to stdout"
log_info "INFO goes to stdout"
log_notice "NOTICE goes to stdout"
log_warn "WARN goes to stderr (hidden)"
log_error "ERROR goes to stderr (hidden)"
) 2>/dev/null
# Set stderr level to DEBUG (everything to stderr)
echo -e "\n========== Setting stderr level to DEBUG (all output to stderr) =========="
init_logger --log "${LOGGING_FILE}" --level DEBUG --stderr-level DEBUG || {
echo "Failed to initialize logger" >&2
exit 1
}
echo "Running: some_script.sh 2>/dev/null (suppressing stderr)"
echo "You should see NOTHING (all output goes to stderr which is suppressed):"
(
log_debug "DEBUG goes to stderr (hidden)"
log_info "INFO goes to stderr (hidden)"
log_warn "WARN goes to stderr (hidden)"
log_error "ERROR goes to stderr (hidden)"
) 2>/dev/null
echo "(If you see nothing above, the test passed!)"
# Set stderr level to EMERGENCY (almost everything to stdout)
echo -e "\n========== Setting stderr level to EMERGENCY (only EMERGENCY to stderr) =========="
init_logger --log "${LOGGING_FILE}" --level DEBUG --stderr-level EMERGENCY || {
echo "Failed to initialize logger" >&2
exit 1
}
echo "Running: some_script.sh 2>/dev/null (suppressing stderr)"
echo "You should see everything except EMERGENCY:"
(
log_debug "DEBUG goes to stdout"
log_info "INFO goes to stdout"
log_warn "WARN goes to stdout"
log_error "ERROR goes to stdout"
log_critical "CRITICAL goes to stdout"
log_alert "ALERT goes to stdout"
log_emergency "EMERGENCY goes to stderr (hidden)"
) 2>/dev/null
echo "========== Stderr Level Demo Complete =========="
# ====================================================
# PART 7: Combined Features Demo
# ====================================================
echo -e "\n========== PART 7: Combined Features Demo =========="
# Initialize with multiple features enabled
JOURNAL_PARAM=""
if [[ "$LOGGER_AVAILABLE" == true ]]; then
JOURNAL_PARAM="--journal --tag all-features"
fi
echo "========== Initializing with multiple features =========="
init_logger --log "${LOGGING_FILE}" --level INFO --format "[%z %d] [%l] %m" --utc $JOURNAL_PARAM --color || {
echo "Failed to initialize logger" >&2
exit 1
}
# Log various messages
log_debug "This is a DEBUG message (shouldn't show with INFO level)"
log_info "This message combines UTC time, custom format, colors, and journal logging"
log_warn "This warning also demonstrates multiple features"
log_error "This error message shows the combined setup"
log_sensitive "This sensitive message shows only on console"
echo "========== Combined Features Demo Complete =========="
# ====================================================
# PART 8: Quiet Mode Demo
# ====================================================
echo -e "\n========== PART 8: Quiet Mode Demo =========="
# Initialize with quiet mode
echo "========== Initializing with quiet mode =========="
init_logger --log "${LOGGING_FILE}" --quiet $JOURNAL_PARAM || {
echo "Failed to initialize logger" >&2
exit 1
}
# Log messages (won't appear on console but will go to file and journal)
log_info "This info should NOT appear on console but will be in the log file"
log_warn "This warning should also be suppressed from console"
log_error "This error should be suppressed from console but in log file"
# Summarize what happened
echo "Messages were logged to file but not displayed on console due to --quiet"
echo "========== Quiet Mode Demo Complete =========="
# ====================================================
# PART 9: Configuration File Demo
# ====================================================
echo -e "\n========== PART 9: Configuration File Demo =========="
echo "This demonstrates loading logger configuration from an INI file."
# Create a temporary config file for testing
CONFIG_FILE="${LOGS_DIR}/test_logging.conf"
echo "========== Creating test configuration file =========="
cat > "$CONFIG_FILE" << 'EOF'
# Test configuration file for logging module
[logging]
level = DEBUG
format = [CONFIG] %d [%l] %m
utc = false
color = auto
stderr_level = ERROR
quiet = false
EOF
echo "Config file created at: $CONFIG_FILE"
echo "Contents:"
cat "$CONFIG_FILE"
echo
# Initialize logger using config file
echo "========== Initializing with config file =========="
init_logger --config "$CONFIG_FILE" --log "${LOGGING_FILE}" || {
echo "Failed to initialize logger with config file" >&2
exit 1
}
log_debug "This DEBUG message should appear (config sets level=DEBUG)"
log_info "This INFO message uses format from config file"
log_warn "This WARN message also uses the config format"
log_error "This ERROR message goes to stderr per config"
# Test CLI override of config values
echo -e "\n========== Testing CLI override of config values =========="
echo "Config file sets level=DEBUG, but CLI will override to WARN"
init_logger --config "$CONFIG_FILE" --log "${LOGGING_FILE}" --level WARN || {
echo "Failed to initialize logger" >&2
exit 1
}
log_debug "This DEBUG message should NOT appear (CLI override to WARN)"
log_info "This INFO message should NOT appear (CLI override to WARN)"
log_warn "This WARN message should appear (matches CLI level)"
log_error "This ERROR message should appear"
# Test config file with different settings
echo -e "\n========== Testing config with UTC and custom format =========="
cat > "$CONFIG_FILE" << 'EOF'
# Configuration with UTC time and JSON-like format
[logging]
level = INFO
format = {"time":"%d","tz":"%z","level":"%l","msg":"%m"}
utc = true
color = never
EOF
echo "Updated config file:"
cat "$CONFIG_FILE"
echo
init_logger --config "$CONFIG_FILE" --log "${LOGGING_FILE}" || {
echo "Failed to initialize logger" >&2
exit 1
}
log_info "This message uses JSON-like format with UTC time"
log_warn "Warning message in JSON format"
log_error "Error message in JSON format"
# Clean up temporary config file
rm -f "$CONFIG_FILE"
echo "========== Configuration File Demo Complete =========="
# ====================================================
# Final Summary
# ====================================================
echo -e "\n========== Demo Summary =========="
echo "All logging features have been demonstrated."
echo "Log file is at: ${LOGGING_FILE}"
if [[ "$LOGGER_AVAILABLE" == true ]]; then
echo "Journal logs were created with tags: demo-logger, new-tag, all-features"
echo "You can view them with:"
echo " journalctl -t demo-logger"
echo " journalctl -t new-tag"
echo " journalctl -t all-features"
fi
echo "Demo completed successfully!"
#!/usr/bin/env bash
#
# logging.sh - Reusable Bash Logging Module
#
# shellcheck disable=SC2034
# Note: SC2034 (unused variable) is disabled because this script is designed to be
# sourced by other scripts. Variables like LOG_LEVEL_FATAL, LOG_CONFIG_FILE, VERBOSE,
# and current_section are intentionally exported for external use or future features.
#
# This script provides logging functionality that can be sourced by other scripts
#
# Usage in other scripts:
# source /path/to/logging.sh # Ensure that the path is an absolute path
# init_logger [-c|--config FILE] [-l|--log FILE] [-q|--quiet] [-v|--verbose] [-d|--level LEVEL] [-f|--format FORMAT] [-j|--journal] [-t|--tag TAG] [-e|--stderr-level LEVEL] [--color] [--no-color]
#
# Options:
# -c, --config FILE Load configuration from INI file (CLI args override config values)
# -l, --log FILE Write logs to FILE
# -q, --quiet Disable console output
# -v, --verbose Enable debug level logging
# -d, --level LEVEL Set minimum log level (DEBUG, INFO, NOTICE, WARN, ERROR, CRITICAL, ALERT, EMERGENCY)
# -f, --format FORMAT Set log message format (see format variables below)
# -j, --journal Enable systemd journal logging
# -t, --tag TAG Set journal/syslog tag
# -u, --utc Use UTC timestamps instead of local time
# -e, --stderr-level LEVEL Set minimum level for stderr output (default: ERROR)
# Messages at this level and above go to stderr, below go to stdout
# --color, --colour Force colored output
# --no-color, --no-colour Disable colored output
#
# Configuration File Format (INI):
# [logging]
# level = INFO # Log level: DEBUG, INFO, NOTICE, WARN, ERROR, CRITICAL, ALERT, EMERGENCY
# format = %d [%l] [%s] %m # Log format string
# log_file = /path/to/file.log # Log file path (empty to disable)
# journal = false # Enable systemd journal: true/false
# tag = myapp # Journal/syslog tag
# utc = false # Use UTC timestamps: true/false
# color = auto # Color mode: auto/always/never
# stderr_level = ERROR # Minimum level for stderr output
# quiet = false # Disable console output: true/false
# verbose = false # Enable debug logging: true/false
#
# Functions provided:
# log_debug "message" - Log debug level message
# log_info "message" - Log info level message
# log_notice "message" - Log notice level message
# log_warn "message" - Log warning level message
# log_error "message" - Log error level message
# log_critical "message" - Log critical level message
# log_alert "message" - Log alert level message
# log_emergency "message" - Log emergency level message (system unusable)
# log_sensitive "message" - Log sensitive message (console only, never to file or journal)
#
# Log Levels (following complete syslog standard):
# 7 = DEBUG (most verbose/least severe)
# 6 = INFO (informational messages)
# 5 = NOTICE (normal but significant conditions)
# 4 = WARN/WARNING (warning conditions)
# 3 = ERROR (error conditions)
# 2 = CRITICAL (critical conditions)
# 1 = ALERT (action must be taken immediately)
# 0 = EMERGENCY (system is unusable)
# Log levels (following complete syslog standard - higher number = less severe)
LOG_LEVEL_EMERGENCY=0 # System is unusable (most severe)
LOG_LEVEL_ALERT=1 # Action must be taken immediately
LOG_LEVEL_CRITICAL=2 # Critical conditions
LOG_LEVEL_ERROR=3 # Error conditions
LOG_LEVEL_WARN=4 # Warning conditions
LOG_LEVEL_NOTICE=5 # Normal but significant conditions
LOG_LEVEL_INFO=6 # Informational messages
LOG_LEVEL_DEBUG=7 # Debug information (least severe)
# Aliases for backward compatibility
LOG_LEVEL_FATAL=$LOG_LEVEL_EMERGENCY # Alias for EMERGENCY
# Default settings (these can be overridden by init_logger)
CONSOLE_LOG="true"
LOG_FILE=""
VERBOSE="false"
CURRENT_LOG_LEVEL=$LOG_LEVEL_INFO
USE_UTC="false" # Set to true to use UTC time in logs
# Journal logging settings
USE_JOURNAL="true"
JOURNAL_TAG="" # Tag for syslog/journal entries
# Color settings
USE_COLORS="auto" # Can be "auto", "always", or "never"
# Stream output settings
# Messages at this level and above (more severe) go to stderr, below go to stdout
# Default: ERROR (level 3) and above to stderr
LOG_STDERR_LEVEL=$LOG_LEVEL_ERROR
# Default log format
# Format variables:
# %d = date and time (YYYY-MM-DD HH:MM:SS)
# %z = timezone (UTC or LOCAL)
# %l = log level name (DEBUG, INFO, WARN, ERROR)
# %s = script name
# %m = message
# Example:
# "[%l] %d [%s] %m" => "[INFO] 2025-03-03 12:34:56 [myscript.sh] Hello world"
# "%d %z [%l] [%s] %m" => "2025-03-03 12:34:56 UTC [INFO] [myscript.sh] Hello world"
LOG_FORMAT="%d [%l] [%s] %m"
# Function to detect terminal color support
detect_color_support() {
# Default to no colors if explicitly disabled
if [[ -n "${NO_COLOR:-}" || "${CLICOLOR:-}" == "0" ]]; then
return 1
fi
# Force colors if explicitly enabled
if [[ "${CLICOLOR_FORCE:-}" == "1" ]]; then
return 0
fi
# Check if stdout is a terminal
if [[ ! -t 1 ]]; then
return 1
fi
# Check color capabilities with tput if available
if command -v tput >/dev/null 2>&1; then
if [[ $(tput colors 2>/dev/null || echo 0) -ge 8 ]]; then
return 0
fi
fi
# Check TERM as fallback
if [[ -n "${TERM:-}" && "${TERM:-}" != "dumb" ]]; then
case "${TERM:-}" in
xterm*|rxvt*|ansi|linux|screen*|tmux*|vt100|vt220|alacritty)
return 0
;;
esac
fi
return 1 # Default to no colors
}
# Function to determine if colors should be used
should_use_colors() {
case "$USE_COLORS" in
"always")
return 0
;;
"never")
return 1
;;
"auto"|*)
detect_color_support
return $?
;;
esac
}
# Function to determine if a log level should output to stderr
# Returns 0 (true) if the given level should go to stderr
should_use_stderr() {
local level_value="$1"
# Lower number = more severe, so use stderr if level <= threshold
[[ "$level_value" -le "$LOG_STDERR_LEVEL" ]]
}
# Check if logger command is available
check_logger_available() {
command -v logger &>/dev/null
}
# Configuration file path (set by init_logger when using -c option)
LOG_CONFIG_FILE=""
# Parse an INI-style configuration file
# Usage: parse_config_file "/path/to/config.ini"
# Returns 0 on success, 1 on error
# Config values are applied to global variables; CLI args can override them later
parse_config_file() {
local config_file="$1"
# Validate file exists and is readable
if [[ ! -f "$config_file" ]]; then
echo "Error: Configuration file not found: $config_file" >&2
return 1
fi
if [[ ! -r "$config_file" ]]; then
echo "Error: Configuration file not readable: $config_file" >&2
return 1
fi
local line_num=0
local current_section=""
while IFS= read -r line || [[ -n "$line" ]]; do
((line_num++))
# Remove leading/trailing whitespace
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
# Skip empty lines and comments
[[ -z "$line" || "$line" =~ ^[#\;] ]] && continue
# Handle section headers [section]
if [[ "$line" =~ ^\[([^]]+)\]$ ]]; then
current_section="${BASH_REMATCH[1]}"
continue
fi
# Parse key = value pairs
if [[ "$line" =~ ^([^=]+)=(.*)$ ]]; then
local key="${BASH_REMATCH[1]}"
local value="${BASH_REMATCH[2]}"
# Trim whitespace from key and value
key="${key#"${key%%[![:space:]]*}"}"
key="${key%"${key##*[![:space:]]}"}"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
# Remove surrounding quotes if present
if [[ "$value" =~ ^\"(.*)\"$ ]] || [[ "$value" =~ ^\'(.*)\'$ ]]; then
value="${BASH_REMATCH[1]}"
fi
# Apply configuration based on key (case-insensitive)
case "${key,,}" in
level|log_level)
CURRENT_LOG_LEVEL=$(get_log_level_value "$value")
;;
format|log_format)
LOG_FORMAT="$value"
;;
log_file|logfile|file)
LOG_FILE="$value"
;;
journal|use_journal)
case "${value,,}" in
true|yes|1|on)
if check_logger_available; then
USE_JOURNAL="true"
else
echo "Warning: logger command not found, journal logging disabled (config line $line_num)" >&2
fi
;;
false|no|0|off)
USE_JOURNAL="false"
;;
*)
echo "Warning: Invalid journal value '$value' at line $line_num, expected true/false" >&2
;;
esac
;;
tag|journal_tag)
JOURNAL_TAG="$value"
;;
utc|use_utc)
case "${value,,}" in
true|yes|1|on)
USE_UTC="true"
;;
false|no|0|off)
USE_UTC="false"
;;
*)
echo "Warning: Invalid utc value '$value' at line $line_num, expected true/false" >&2
;;
esac
;;
color|colour|colors|colours|use_colors)
case "${value,,}" in
auto)
USE_COLORS="auto"
;;
always|true|yes|1|on)
USE_COLORS="always"
;;
never|false|no|0|off)
USE_COLORS="never"
;;
*)
echo "Warning: Invalid color value '$value' at line $line_num, expected auto/always/never" >&2
;;
esac
;;
stderr_level|stderr-level)
LOG_STDERR_LEVEL=$(get_log_level_value "$value")
;;
quiet|console_log)
case "${key,,}" in
quiet)
# quiet=true means CONSOLE_LOG=false
case "${value,,}" in
true|yes|1|on)
CONSOLE_LOG="false"
;;
false|no|0|off)
CONSOLE_LOG="true"
;;
*)
echo "Warning: Invalid quiet value '$value' at line $line_num, expected true/false" >&2
;;
esac
;;
console_log)
# console_log=true means CONSOLE_LOG=true (direct mapping)
case "${value,,}" in
true|yes|1|on)
CONSOLE_LOG="true"
;;
false|no|0|off)
CONSOLE_LOG="false"
;;
*)
echo "Warning: Invalid console_log value '$value' at line $line_num, expected true/false" >&2
;;
esac
;;
esac
;;
verbose)
case "${value,,}" in
true|yes|1|on)
VERBOSE="true"
CURRENT_LOG_LEVEL=$LOG_LEVEL_DEBUG
;;
false|no|0|off)
VERBOSE="false"
;;
*)
echo "Warning: Invalid verbose value '$value' at line $line_num, expected true/false" >&2
;;
esac
;;
*)
echo "Warning: Unknown configuration key '$key' at line $line_num" >&2
;;
esac
else
echo "Warning: Invalid syntax at line $line_num: $line" >&2
fi
done < "$config_file"
# Store the config file path for potential reload functionality
LOG_CONFIG_FILE="$config_file"
return 0
}
# Convert log level name to numeric value
get_log_level_value() {
local level_name="$1"
case "${level_name^^}" in
"DEBUG")
echo $LOG_LEVEL_DEBUG
;;
"INFO")
echo $LOG_LEVEL_INFO
;;
"NOTICE")
echo $LOG_LEVEL_NOTICE
;;
"WARN" | "WARNING")
echo $LOG_LEVEL_WARN
;;
"ERROR" | "ERR")
echo $LOG_LEVEL_ERROR
;;
"CRITICAL" | "CRIT")
echo $LOG_LEVEL_CRITICAL
;;
"ALERT")
echo $LOG_LEVEL_ALERT
;;
"EMERGENCY" | "EMERG" | "FATAL")
echo $LOG_LEVEL_EMERGENCY
;;
*)
# If it's a number between 0-7 (valid syslog levels), use it directly
if [[ "$level_name" =~ ^[0-7]$ ]]; then
echo "$level_name"
else
# Default to INFO if invalid
echo $LOG_LEVEL_INFO
fi
;;
esac
}
# Get log level name from numeric value
get_log_level_name() {
local level_value="$1"
case "$level_value" in
"$LOG_LEVEL_DEBUG")
echo "DEBUG"
;;
"$LOG_LEVEL_INFO")
echo "INFO"
;;
"$LOG_LEVEL_NOTICE")
echo "NOTICE"
;;
"$LOG_LEVEL_WARN")
echo "WARN"
;;
"$LOG_LEVEL_ERROR")
echo "ERROR"
;;
"$LOG_LEVEL_CRITICAL")
echo "CRITICAL"
;;
"$LOG_LEVEL_ALERT")
echo "ALERT"
;;
"$LOG_LEVEL_EMERGENCY")
echo "EMERGENCY"
;;
*)
echo "UNKNOWN"
;;
esac
}
# Map log level to syslog priority
get_syslog_priority() {
local level_value="$1"
case "$level_value" in
"$LOG_LEVEL_DEBUG")
echo "debug"
;;
"$LOG_LEVEL_INFO")
echo "info"
;;
"$LOG_LEVEL_NOTICE")
echo "notice"
;;
"$LOG_LEVEL_WARN")
echo "warning"
;;
"$LOG_LEVEL_ERROR")
echo "err"
;;
"$LOG_LEVEL_CRITICAL")
echo "crit"
;;
"$LOG_LEVEL_ALERT")
echo "alert"
;;
"$LOG_LEVEL_EMERGENCY")
echo "emerg"
;;
*)
echo "notice" # Default to notice for unknown levels
;;
esac
}
# Function to format log message
format_log_message() {
local level_name="$1"
local message="$2"
# Get timestamp in appropriate timezone
local current_date
local timezone_str
if [[ "$USE_UTC" == "true" ]]; then
current_date=$(date -u '+%Y-%m-%d %H:%M:%S') # UTC time
timezone_str="UTC"
else
current_date=$(date '+%Y-%m-%d %H:%M:%S') # Local time
timezone_str="LOCAL"
fi
# Replace format variables - zsh compatible method
local formatted_message="$LOG_FORMAT"
# Handle % escaping for zsh compatibility
if [[ -n "${ZSH_VERSION:-}" ]]; then
# In zsh, we need a different approach
formatted_message=${formatted_message:gs/%d/$current_date}
formatted_message=${formatted_message:gs/%l/$level_name}
formatted_message=${formatted_message:gs/%s/${SCRIPT_NAME:-unknown}}
formatted_message=${formatted_message:gs/%m/$message}
formatted_message=${formatted_message:gs/%z/$timezone_str}
else
# Bash version (original)
formatted_message="${formatted_message//%d/$current_date}"
formatted_message="${formatted_message//%l/$level_name}"
formatted_message="${formatted_message//%s/${SCRIPT_NAME:-unknown}}"
formatted_message="${formatted_message//%m/$message}"
formatted_message="${formatted_message//%z/$timezone_str}"
fi
echo "$formatted_message"
}
# Function to initialize logger with custom settings
init_logger() {
# Get the calling script's name
local caller_script
if [[ -n "${BASH_SOURCE[1]:-}" ]]; then
caller_script=$(basename "${BASH_SOURCE[1]}")
else
caller_script="unknown"
fi
# First pass: look for config file option and process it first
# This allows CLI arguments to override config file values
local args=("$@")
local i=0
while [[ $i -lt ${#args[@]} ]]; do
case "${args[$i]}" in
-c|--config)
local config_file="${args[$((i+1))]}"
if [[ -z "$config_file" ]]; then
echo "Error: --config requires a file path argument" >&2
return 1
fi
if ! parse_config_file "$config_file"; then
return 1
fi
break
;;
esac
((i++))
done
# Second pass: parse all command line arguments (overrides config file)
while [[ "$#" -gt 0 ]]; do
case $1 in
-c|--config)
# Already processed in first pass, skip
shift 2
;;
--color|--colour)
USE_COLORS="always"
shift
;;
--no-color|--no-colour)
USE_COLORS="never"
shift
;;
-d|--level)
local level_value
level_value=$(get_log_level_value "$2")
CURRENT_LOG_LEVEL=$level_value
# If both --verbose and --level are specified, --level takes precedence
shift 2
;;
-f|--format)
LOG_FORMAT="$2"
shift 2
;;
-j|--journal)
if check_logger_available; then
USE_JOURNAL="true"
else
echo "Warning: logger command not found, journal logging disabled" >&2
fi
shift
;;
-l|--log|--logfile|--log-file|--file)
LOG_FILE="$2"
shift 2
;;
-q|--quiet)
CONSOLE_LOG="false"
shift
;;
-t|--tag)
JOURNAL_TAG="$2"
shift 2
;;
-u|--utc)
USE_UTC="true"
shift
;;
-v|--verbose|--debug)
VERBOSE="true"
CURRENT_LOG_LEVEL=$LOG_LEVEL_DEBUG
shift
;;
-e|--stderr-level)
local stderr_level_value
stderr_level_value=$(get_log_level_value "$2")
LOG_STDERR_LEVEL=$stderr_level_value
shift 2
;;
*)
echo "Unknown parameter for logger: $1" >&2
return 1
;;
esac
done
# Set a global variable for the script name to use in log messages
SCRIPT_NAME="$caller_script"
# Set default journal tag if not specified but journal logging is enabled
if [[ "$USE_JOURNAL" == "true" && -z "$JOURNAL_TAG" ]]; then
JOURNAL_TAG="$SCRIPT_NAME"
fi
# Validate log file path if specified
if [[ -n "$LOG_FILE" ]]; then
# Get directory of log file
LOG_DIR=$(dirname "$LOG_FILE")
# Try to create directory if it doesn't exist
if [[ ! -d "$LOG_DIR" ]]; then
mkdir -p "$LOG_DIR" 2>/dev/null || {
echo "Error: Cannot create log directory '$LOG_DIR'" >&2
return 1
}
fi
# Try to touch the file to ensure we can write to it
touch "$LOG_FILE" 2>/dev/null || {
echo "Error: Cannot write to log file '$LOG_FILE'" >&2
return 1
}
# Verify one more time that file exists and is writable
if [[ ! -w "$LOG_FILE" ]]; then
echo "Error: Log file '$LOG_FILE' is not writable" >&2
return 1
fi
# Write the initialization message using the same format
local init_message
init_message=$(format_log_message "INIT" "Logger initialized by $caller_script")
echo "$init_message" >> "$LOG_FILE" 2>/dev/null || {
echo "Error: Failed to write test message to log file" >&2
return 1
}
echo "Logger: Successfully initialized with log file at '$LOG_FILE'" >&2
fi
# Log initialization success
log_debug "Logger initialized by '$caller_script' with: console=$CONSOLE_LOG, file=$LOG_FILE, journal=$USE_JOURNAL, colors=$USE_COLORS, log level=$(get_log_level_name "$CURRENT_LOG_LEVEL"), stderr level=$(get_log_level_name "$LOG_STDERR_LEVEL"), format=\"$LOG_FORMAT\""
return 0
}
# Function to change log level after initialization
set_log_level() {
local level="$1"
local old_level
old_level=$(get_log_level_name "$CURRENT_LOG_LEVEL")
CURRENT_LOG_LEVEL=$(get_log_level_value "$level")
local new_level
new_level=$(get_log_level_name "$CURRENT_LOG_LEVEL")
# Create a special log entry that bypasses level checks
local message="Log level changed from $old_level to $new_level"
local log_entry
log_entry=$(format_log_message "CONFIG" "$message")
# Always print to console if enabled
if [[ "$CONSOLE_LOG" == "true" ]]; then
if should_use_colors; then
echo -e "\e[35m${log_entry}\e[0m" # Purple for configuration changes
else
echo "${log_entry}"
fi
fi
# Always write to log file if set
if [[ -n "$LOG_FILE" ]]; then
echo "${log_entry}" >> "$LOG_FILE" 2>/dev/null
fi
# Always log to journal if enabled
if [[ "$USE_JOURNAL" == "true" ]]; then
logger -p "daemon.notice" -t "${JOURNAL_TAG:-$SCRIPT_NAME}" "CONFIG: $message"
fi
}
set_timezone_utc() {
local use_utc="$1"
local old_setting="$USE_UTC"
USE_UTC="$use_utc"
local message="Timezone setting changed from $old_setting to $USE_UTC"
local log_entry
log_entry=$(format_log_message "CONFIG" "$message")
# Always print to console if enabled
if [[ "$CONSOLE_LOG" == "true" ]]; then
if should_use_colors; then
echo -e "\e[35m${log_entry}\e[0m" # Purple for configuration changes
else
echo "${log_entry}"
fi
fi
# Always write to log file if set
if [[ -n "$LOG_FILE" ]]; then
echo "${log_entry}" >> "$LOG_FILE" 2>/dev/null
fi
# Always log to journal if enabled
if [[ "$USE_JOURNAL" == "true" ]]; then
logger -p "daemon.notice" -t "${JOURNAL_TAG:-$SCRIPT_NAME}" "CONFIG: $message"
fi
}
# Function to change log format
set_log_format() {
local old_format="$LOG_FORMAT"
LOG_FORMAT="$1"
local message="Log format changed from \"$old_format\" to \"$LOG_FORMAT\""
local log_entry
log_entry=$(format_log_message "CONFIG" "$message")
# Always print to console if enabled
if [[ "$CONSOLE_LOG" == "true" ]]; then
if should_use_colors; then
echo -e "\e[35m${log_entry}\e[0m" # Purple for configuration changes
else
echo "${log_entry}"
fi
fi
# Always write to log file if set
if [[ -n "$LOG_FILE" ]]; then
echo "${log_entry}" >> "$LOG_FILE" 2>/dev/null
fi
# Always log to journal if enabled
if [[ "$USE_JOURNAL" == "true" ]]; then
logger -p "daemon.notice" -t "${JOURNAL_TAG:-$SCRIPT_NAME}" "CONFIG: $message"
fi
}
# Function to toggle journal logging
set_journal_logging() {
local old_setting="$USE_JOURNAL"
USE_JOURNAL="$1"
# Check if logger is available when enabling
if [[ "$USE_JOURNAL" == "true" ]]; then
if ! check_logger_available; then
echo "Error: logger command not found, cannot enable journal logging" >&2
USE_JOURNAL="$old_setting"
return 1
fi
fi
local message="Journal logging changed from $old_setting to $USE_JOURNAL"
local log_entry
log_entry=$(format_log_message "CONFIG" "$message")
# Always print to console if enabled
if [[ "$CONSOLE_LOG" == "true" ]]; then
if should_use_colors; then
echo -e "\e[35m${log_entry}\e[0m" # Purple for configuration changes
else
echo "${log_entry}"
fi
fi
# Always write to log file if set
if [[ -n "$LOG_FILE" ]]; then
echo "${log_entry}" >> "$LOG_FILE" 2>/dev/null
fi
# Log to journal if it was previously enabled or just being enabled
if [[ "$old_setting" == "true" || "$USE_JOURNAL" == "true" ]]; then
logger -p "daemon.notice" -t "${JOURNAL_TAG:-$SCRIPT_NAME}" "CONFIG: $message"
fi
}
# Function to set journal tag
set_journal_tag() {
local old_tag="$JOURNAL_TAG"
JOURNAL_TAG="$1"
local message="Journal tag changed from \"$old_tag\" to \"$JOURNAL_TAG\""
local log_entry
log_entry=$(format_log_message "CONFIG" "$message")
# Always print to console if enabled
if [[ "$CONSOLE_LOG" == "true" ]]; then
if should_use_colors; then
echo -e "\e[35m${log_entry}\e[0m" # Purple for configuration changes
else
echo "${log_entry}"
fi
fi
# Always write to log file if set
if [[ -n "$LOG_FILE" ]]; then
echo "${log_entry}" >> "$LOG_FILE" 2>/dev/null
fi
# Log to journal if enabled, using the old tag
if [[ "$USE_JOURNAL" == "true" ]]; then
logger -p "daemon.notice" -t "${old_tag:-$SCRIPT_NAME}" "CONFIG: Journal tag changing to \"$JOURNAL_TAG\""
fi
}
# Function to set color mode
set_color_mode() {
local mode="$1"
local old_setting="$USE_COLORS"
case "$mode" in
true|on|yes|1)
USE_COLORS="always"
;;
false|off|no|0)
USE_COLORS="never"
;;
auto)
USE_COLORS="auto"
;;
*)
USE_COLORS="$mode" # Set directly if it's already "always", "never", or "auto"
;;
esac
local message="Color mode changed from \"$old_setting\" to \"$USE_COLORS\""
local log_entry
log_entry=$(format_log_message "CONFIG" "$message")
# Always print to console if enabled
if [[ "$CONSOLE_LOG" == "true" ]]; then
if should_use_colors; then
echo -e "\e[35m${log_entry}\e[0m" # Purple for configuration changes
else
echo "${log_entry}"
fi
fi
# Always write to log file if set
if [[ -n "$LOG_FILE" ]]; then
echo "${log_entry}" >> "$LOG_FILE" 2>/dev/null
fi
# Log to journal if enabled
if [[ "$USE_JOURNAL" == "true" ]]; then
logger -p "daemon.notice" -t "${JOURNAL_TAG:-$SCRIPT_NAME}" "CONFIG: $message"
fi
}
# Function to log messages with different severity levels
log_message() {
local level_name="$1"
local level_value="$2"
local message="$3"
local skip_file="${4:-false}"
local skip_journal="${5:-false}"
# Skip logging if message level is more verbose than current log level
# With syslog-style levels, HIGHER values are LESS severe (more verbose)
if [[ "$level_value" -gt "$CURRENT_LOG_LEVEL" ]]; then
return
fi
# Format the log entry
local log_entry
log_entry=$(format_log_message "$level_name" "$message")
# If CONSOLE_LOG is true, print to console
if [[ "$CONSOLE_LOG" == "true" ]]; then
# Determine if output should go to stderr based on configured threshold
local use_stderr=false
if should_use_stderr "$level_value"; then
use_stderr=true
fi
if should_use_colors; then
# Color output for console based on log level
case "$level_name" in
"DEBUG")
if [[ "$use_stderr" == true ]]; then
echo -e "\e[34m${log_entry}\e[0m" >&2 # Blue
else
echo -e "\e[34m${log_entry}\e[0m" # Blue
fi
;;
"INFO")
if [[ "$use_stderr" == true ]]; then
echo -e "${log_entry}" >&2 # Default color
else
echo -e "${log_entry}" # Default color
fi
;;
"NOTICE")
if [[ "$use_stderr" == true ]]; then
echo -e "\e[32m${log_entry}\e[0m" >&2 # Green
else
echo -e "\e[32m${log_entry}\e[0m" # Green
fi
;;
"WARN")
if [[ "$use_stderr" == true ]]; then
echo -e "\e[33m${log_entry}\e[0m" >&2 # Yellow
else
echo -e "\e[33m${log_entry}\e[0m" # Yellow
fi
;;
"ERROR")
if [[ "$use_stderr" == true ]]; then
echo -e "\e[31m${log_entry}\e[0m" >&2 # Red
else
echo -e "\e[31m${log_entry}\e[0m" # Red
fi
;;
"CRITICAL")
if [[ "$use_stderr" == true ]]; then
echo -e "\e[31;1m${log_entry}\e[0m" >&2 # Bright Red
else
echo -e "\e[31;1m${log_entry}\e[0m" # Bright Red
fi
;;
"ALERT")
if [[ "$use_stderr" == true ]]; then
echo -e "\e[37;41m${log_entry}\e[0m" >&2 # White on Red background
else
echo -e "\e[37;41m${log_entry}\e[0m" # White on Red background
fi
;;
"EMERGENCY"|"FATAL")
if [[ "$use_stderr" == true ]]; then
echo -e "\e[1;37;41m${log_entry}\e[0m" >&2 # Bold White on Red background
else
echo -e "\e[1;37;41m${log_entry}\e[0m" # Bold White on Red background
fi
;;
"INIT")
if [[ "$use_stderr" == true ]]; then
echo -e "\e[35m${log_entry}\e[0m" >&2 # Purple for init
else
echo -e "\e[35m${log_entry}\e[0m" # Purple for init
fi
;;
"SENSITIVE")
if [[ "$use_stderr" == true ]]; then
echo -e "\e[36m${log_entry}\e[0m" >&2 # Cyan for sensitive
else
echo -e "\e[36m${log_entry}\e[0m" # Cyan for sensitive
fi
;;
*)
if [[ "$use_stderr" == true ]]; then
echo "${log_entry}" >&2 # Default color for unknown level
else
echo "${log_entry}" # Default color for unknown level
fi
;;
esac
else
# Plain output without colors
if [[ "$use_stderr" == true ]]; then
echo "${log_entry}" >&2
else
echo "${log_entry}"
fi
fi
fi
# If LOG_FILE is set and not empty, append to the log file (without colors)
# Skip writing to the file if skip_file is true
if [[ -n "$LOG_FILE" && "$skip_file" != "true" ]]; then
echo "${log_entry}" >> "$LOG_FILE" 2>/dev/null || {
# Only print the error once to avoid spam
if [[ -z "$LOGGER_FILE_ERROR_REPORTED" ]]; then
echo "ERROR: Failed to write to log file: $LOG_FILE" >&2
LOGGER_FILE_ERROR_REPORTED="yes"
fi
# Print the original message to stderr to not lose it
echo "${log_entry}" >&2
}
fi
# If journal logging is enabled and logger is available, log to the system journal
# Skip journal logging if skip_journal is true
if [[ "$USE_JOURNAL" == "true" && "$skip_journal" != "true" ]]; then
if check_logger_available; then
# Map our log level to syslog priority
local syslog_priority
syslog_priority=$(get_syslog_priority "$level_value")
# Use the logger command to send to syslog/journal
# Strip any ANSI color codes from the message
local plain_message="${message//\e\[[0-9;]*m/}"
logger -p "daemon.${syslog_priority}" -t "${JOURNAL_TAG:-$SCRIPT_NAME}" "$plain_message"
fi
fi
}
# Helper functions for different log levels
log_debug() {
log_message "DEBUG" $LOG_LEVEL_DEBUG "$1"
}
log_info() {
log_message "INFO" $LOG_LEVEL_INFO "$1"
}
log_notice() {
log_message "NOTICE" $LOG_LEVEL_NOTICE "$1"
}
log_warn() {
log_message "WARN" $LOG_LEVEL_WARN "$1"
}
log_error() {
log_message "ERROR" $LOG_LEVEL_ERROR "$1"
}
log_critical() {
log_message "CRITICAL" $LOG_LEVEL_CRITICAL "$1"
}
log_alert() {
log_message "ALERT" $LOG_LEVEL_ALERT "$1"
}
log_emergency() {
log_message "EMERGENCY" $LOG_LEVEL_EMERGENCY "$1"
}
# Alias for backward compatibility
log_fatal() {
log_message "FATAL" $LOG_LEVEL_EMERGENCY "$1"
}
log_init() {
log_message "INIT" -1 "$1" # Using -1 to ensure it always shows
}
# Function for sensitive logging - console only, never to file or journal
log_sensitive() {
log_message "SENSITIVE" $LOG_LEVEL_INFO "$1" "true" "true"
}
# Only execute initialization if this script is being run directly
# If it's being sourced, the sourcing script should call init_logger
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
echo "This script is designed to be sourced by other scripts, not executed directly."
echo "Usage: source logging.sh"
exit 1
fi
@anoduck
Copy link

anoduck commented Dec 18, 2025

This was EXACTLY the solution I was looking for. Straight up simple, bash based logging. Needs to be made into a package installable via basher, because it is better than what else is available.

@ApprenticeofEnder
Copy link

This is AWESOME, really appreciate the work you've put into this. Plus it's extensible enough that you can log to things like SEQ.

@ApprenticeofEnder
Copy link

Actually, I have run into some issues with this script and how it logs warning and below to stdout vs stderr. Is there a specific reason for the split? It seems to me that logging to stderr by default is sensible, or maybe having a configuration option to log to stdout vs stderr, etc.

@GingerGraham
Copy link
Author

GingerGraham commented Jan 14, 2026

Actually, I have run into some issues with this script and how it logs warning and below to stdout vs stderr. Is there a specific reason for the split? It seems to me that logging to stderr by default is sensible, or maybe having a configuration option to log to stdout vs stderr, etc.

Interesting, I've not seen that as an issue. Can you provide any details on where you are having issues?

The split in streams was a design consideration at the time around warnings are not strictly errors and as such the calling script might be considered to be running normally still.

I can have a look at adding a configuration option for the behaviour. Perhaps a switch at init time for the level at which redirect to stderr occurs

@GingerGraham
Copy link
Author

This was EXACTLY the solution I was looking for. Straight up simple, bash based logging. Needs to be made into a package installable via basher, because it is better than what else is available.

Sorry, not checked comments for a while, I'm glad it's helpful for you. As I understand it basher is looking for actionable scripts, rather than modules or libraries that are to be sourced, so on that note it might not be applicable.

I'm open to ideas and suggestions, or it's MIT licenced so happy for someone else to package it up.

@GingerGraham
Copy link
Author

Actually, I have run into some issues with this script and how it logs warning and below to stdout vs stderr. Is there a specific reason for the split? It seems to me that logging to stderr by default is sensible, or maybe having a configuration option to log to stdout vs stderr, etc.

Interesting, I've not seen that as an issue. Can you provide any details on where you are having issues?

The split in streams was a design consideration at the time around warnings are not strictly errors and as such the calling script might be considered to be running normally still.

I can have a look at adding a configuration option for the behaviour. Perhaps a switch at init time for the level at which redirect to stderr occurs

I went ahead and added a new --stderr-level option that lets you configure which log levels go to stderr vs stdout. It will default to ERROR and above go to stderr (preserving the original behaviour), but you can now customize this:

# Default: ERROR and above to stderr
init_logger

# Send WARN and above to stderr
init_logger --stderr-level WARN

# Send everything to stderr (useful for CLI tools)
init_logger --stderr-level DEBUG

# Send only EMERGENCY to stderr (almost everything to stdout)
init_logger --stderr-level EMERGENCY

@GingerGraham
Copy link
Author

While I was at it, I added an option to support passing configuration as an INI file, rather than a growing list of command arguments.

@ApprenticeofEnder
Copy link

While I was at it, I added an option to support passing configuration as an INI file, rather than a growing list of command arguments.

Oh that's actually huge.

@ApprenticeofEnder
Copy link

I went ahead and made a fork of this because I found a way to extract the console logging to its own function: https://gist.github.com/ApprenticeofEnder/dfd3c22070876b8f9896730b4e2645f1

I tested it with log_demo.sh and it seems to work like a charm. Feel free to merge into your own codebase, although it might not be the worst idea to make this into a proper repo as opposed to just a gist.

@GingerGraham
Copy link
Author

Yeah, what started as a here's a snippet you can use is definitely grown out to where a repo would make more sense.

I'll take a look at that.

I like the function abstraction work you added too @ApprenticeofEnder definitely worth wrapping up in it

@GingerGraham
Copy link
Author

GingerGraham commented Jan 17, 2026

As discussed above, I've migrated the work from here to a full repo, bash-logger

I've not yet merged in the improvements from @ApprenticeofEnder but I'll get that looked at shortly

From now on I will not be updating this gist further, all future improvements will be addressed in the repo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment