Skip to content

Instantly share code, notes, and snippets.

@GingerGraham
Last active January 16, 2026 10:10
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.

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"

Initialization Options

The init_logger function accepts the following options:

Option Description
-c, --config FILE Load configuration from an INI file (CLI args override config values)
-l, --log, --logfile, --log-file, --file FILE Specify a log file to write logs to
-q, --quiet Disable console output
-v, --verbose, --debug Set log level to DEBUG (most verbose)
-d, --level LEVEL Set log level (DEBUG, INFO, NOTICE, WARN, ERROR, CRITICAL, ALERT, EMERGENCY or 0-7)
-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
-f, --format FORMAT Set custom log format
-u, --utc Use UTC time instead of local time
-j, --journal Enable logging to systemd journal
-t, --tag TAG Set custom tag for journal logs (default: script name)
--color --colour Explicitly enable color output (default: auto-detect)
--no-color --no-colour Disable color output

Example:

# Initialize logger with file output, journal logging, and DEBUG level
init_logger --log "/var/log/myscript.log" --level DEBUG --journal --tag "myapp"

# Initialize logger from a configuration file
init_logger --config /etc/myapp/logging.conf

# Config file with CLI overrides (CLI takes precedence)
init_logger --config logging.conf --level DEBUG --color

Log Levels

The module supports standard syslog levels, from most to least severe:

Level Numeric Value Function Syslog Priority
EMERGENCY 0 log_emergency emerg
ALERT 1 log_alert alert
CRITICAL 2 log_critical crit
ERROR 3 log_error err
WARN 4 log_warn warning
NOTICE 5 log_notice notice
INFO 6 log_info info
DEBUG 7 log_debug debug
SENSITIVE - log_sensitive (not sent to syslog)

Messages with a level lower than the current log level are suppressed.

Sensitive messages are logged at the INFO level but are not written to log files or the journal. They are only displayed on the console.

Custom Log Format

You can customize the log format using special placeholders:

Placeholder Description Example
%d Date and time 2025-03-03 12:34:56
%l Log level INFO
%s Script name myscript.sh
%m Log message Operation completed successfully
%z Timezone UTC or LOCAL

The default format is: %d [%l] [%s] %m

Example of custom format:

init_logger --format "[%l] %d %z [%s] %m"

Output Stream Configuration

By default, the logging module splits console output between stdout and stderr based on severity:

  • stdout: DEBUG, INFO, NOTICE, WARN (normal operation messages)
  • stderr: ERROR, CRITICAL, ALERT, EMERGENCY (error messages)

This follows the Unix convention where stdout contains normal output that can be piped or captured, while stderr contains error output that should be visible even when stdout is redirected.

Configuring the Stderr Threshold

You can change which log levels go to stderr using the --stderr-level option:

# Default behavior: ERROR and above to stderr
init_logger

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

# Send everything to stderr (useful for scripts where all output is diagnostic)
init_logger --stderr-level DEBUG

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

Practical Use Cases

Separating normal output from errors:

# Run script, capturing normal logs to file while errors show on screen
./myscript.sh > output.log

Suppressing errors while keeping normal output:

# Run script, showing only normal operation (hide errors)
./myscript.sh 2>/dev/null

Capturing only errors:

# Run script, capturing only error messages
./myscript.sh 2> errors.log 1>/dev/null

Sending all output to stderr (common for CLI tools):

init_logger --stderr-level DEBUG
# Now all log messages go to stderr, leaving stdout free for program output

Configuration File

The logging module supports loading configuration from INI-style files. This is useful for:

  • Centralising logging configuration across multiple scripts
  • Allowing users to customise logging without modifying scripts
  • Separating configuration from code

Configuration File Format

# logging.conf - Example configuration file
# Lines starting with # or ; are comments
# Blank lines are ignored

[logging]
# Log level: DEBUG, INFO, NOTICE, WARN, ERROR, CRITICAL, ALERT, EMERGENCY
level = INFO

# Log message format
# Variables: %d=datetime, %z=timezone, %l=level, %s=script, %m=message
format = %d [%l] [%s] %m

# Log file path (leave empty to disable file logging)
log_file = /var/log/myapp.log

# Enable systemd journal logging: true/false
journal = false

# Journal/syslog tag (defaults to script name)
tag = myapp

# Use UTC timestamps: true/false
utc = false

# Color mode: auto, always, never
color = auto

# Minimum level for stderr output
stderr_level = ERROR

# Disable console output: true/false
quiet = false

# Enable debug logging: true/false
verbose = false

Configuration Keys

Key Aliases Values Description
level log_level DEBUG, INFO, NOTICE, WARN, ERROR, CRITICAL, ALERT, EMERGENCY, 0-7 Minimum log level
format log_format Format string Log message format
log_file logfile, file Path Log file path
journal use_journal true/false, yes/no, on/off, 1/0 Enable journal logging
tag journal_tag String Journal/syslog tag
utc use_utc true/false, yes/no, on/off, 1/0 Use UTC timestamps
color colour, colors, colours, use_colors auto, always, never Color mode
stderr_level stderr-level Log level Minimum level for stderr
quiet - true/false, yes/no, on/off, 1/0 Disable console output
console_log - true/false, yes/no, on/off, 1/0 Enable console output (inverse of quiet)
verbose - true/false, yes/no, on/off, 1/0 Enable DEBUG level

Using Configuration Files

#!/bin/bash
source /path/to/logging.sh

# Load configuration from file
init_logger --config /etc/myapp/logging.conf

# Config file with CLI overrides (CLI arguments take precedence)
init_logger --config logging.conf --level DEBUG

# Config file with multiple overrides
init_logger --config logging.conf --level WARN --color --log /tmp/app.log

Configuration Precedence

When using both a configuration file and CLI arguments:

  1. Default values are set first
  2. Configuration file values override defaults
  3. CLI arguments override configuration file values

This allows you to set baseline configuration in a file while still allowing runtime customisation.

Example: Application with User-Configurable Logging

#!/bin/bash
source /path/to/logging.sh

# Default config location
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/myapp/logging.conf"

# Allow user to specify different config
while [[ "$#" -gt 0 ]]; do
    case $1 in
        --log-config)
            CONFIG_FILE="$2"
            shift 2
            ;;
        *)
            shift
            ;;
    esac
done

# Initialize logger (use config if it exists)
if [[ -f "$CONFIG_FILE" ]]; then
    init_logger --config "$CONFIG_FILE"
else
    # Fall back to defaults
    init_logger --level INFO
fi

log_info "Application started"

Example Configuration File

An example configuration file (logging.conf.example) is included with the module. Copy and customise it for your needs:

cp logging.conf.example /etc/myapp/logging.conf

Runtime Configuration

You can change configuration at runtime using these functions:

# Change log level
set_log_level DEBUG      # Set to DEBUG level
set_log_level NOTICE     # Set to NOTICE level
set_log_level WARN       # Set to WARN level
set_log_level CRITICAL   # Set to CRITICAL level

# Change timezone setting
set_timezone_utc true   # Use UTC time
set_timezone_utc false  # Use local time

# Change log format
set_log_format "[%l] %d [%s] - %m"

# Enable/disable journal logging
set_journal_logging true   # Enable journal logging
set_journal_logging false  # Disable journal logging

# Change journal tag
set_journal_tag "new-tag"  # Set new tag for journal logs

Journal Logging

The module can log to the systemd journal using the logger command. This is particularly useful for applications running as systemd services or on systems like Fedora Linux.

Requirements

  • The logger command must be installed (typically part of the util-linux package)
  • The system should use systemd (standard on most modern Linux distributions)

Configuration

Enable journal logging with the -j or --journal flag during initialization:

init_logger --journal

You can specify a custom tag with -t or --tag:

init_logger --journal --tag "myapp"

If no tag is specified, the script name is used as the default tag.

Viewing Journal Logs

Journal logs can be viewed using the journalctl command:

# View logs with specific tag
journalctl -t myapp

# Follow logs in real-time
journalctl -f -t myapp

# View logs for the current boot
journalctl -b -t myapp

Log Level Mapping

Log levels are mapped to syslog priorities as follows:

Log Level Syslog Priority
DEBUG debug
INFO info
NOTICE notice
WARN warning
ERROR err
CRITICAL crit
ALERT alert
EMERGENCY emerg

Example Use Cases

Basic Script Logging

#!/bin/bash

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

# Initialize with default settings
init_logger

log_info "Script starting"
log_debug "Debug information" 
# ... script operations ...
log_warn "Warning: resource usage high"
log_info "Script completed"

Logging to File with Verbose Output

#!/bin/bash

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

# Initialize with file output and verbose mode
init_logger --log "/tmp/myapp.log" --verbose

log_info "Application starting"
log_debug "Configuration loaded" # This will be logged due to verbose mode
# ... application operations ...
log_info "Application completed"

Logging to System Journal (for systemd-based systems)

#!/bin/bash

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

# Initialize with journal logging
init_logger --journal --tag "myservice"

log_info "Service starting"
# ... service operations ...
log_error "Error encountered: $error_message"
log_info "Service completed"

Comprehensive Logging Configuration

#!/bin/bash

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

# Initialize with multiple outputs and custom format
init_logger \
  --log "/var/log/myapp.log" \
  --journal \
  --tag "myapp" \
  --format "%d %z [%l] [%s] %m" \
  --utc \
  --level INFO

log_info "Application initialized with comprehensive logging"

Using a Configuration File

#!/bin/bash

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

# Initialize from configuration file
# All settings are defined in the INI file
init_logger --config /etc/myapp/logging.conf

log_info "Application started with config file settings"

# You can still override specific settings via CLI
# init_logger --config /etc/myapp/logging.conf --level DEBUG

Example configuration file (/etc/myapp/logging.conf):

[logging]
level = INFO
format = %d %z [%l] [%s] %m
log_file = /var/log/myapp/app.log
journal = true
tag = myapp
utc = true
color = auto
stderr_level = ERROR

CLI Tool with Separate Output Streams

When building a CLI tool that produces both program output and diagnostic logs, you can send all logs to stderr to keep stdout clean for the actual output:

#!/bin/bash

source /path/to/logging.sh

# Send all log messages to stderr, keeping stdout for program output
init_logger --stderr-level DEBUG --level INFO

log_info "Processing input..."

# Program output goes to stdout (can be piped)
echo "result1"
echo "result2"

log_info "Processing complete"

# Usage: ./mytool.sh > results.txt
# Log messages appear on screen, results go to file

Script with Configurable Error Verbosity

#!/bin/bash

source /path/to/logging.sh

# Default: only errors to stderr
STDERR_LEVEL="ERROR"

# Parse arguments
while [[ "$#" -gt 0 ]]; do
  case $1 in
    --warnings-to-stderr)
      STDERR_LEVEL="WARN"
      shift
      ;;
    --all-to-stderr)
      STDERR_LEVEL="DEBUG"
      shift
      ;;
  esac
done

init_logger --stderr-level "$STDERR_LEVEL" --level DEBUG

log_debug "Debug info"
log_info "Starting operation"
log_warn "Warning: disk space low"
log_error "Error: file not found"

Changing Log Level Based on Command-line Arguments

#!/bin/bash

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

# Basic initialization
init_logger

# Parse command line arguments
while [[ "$#" -gt 0 ]]; do
  case $1 in
    --debug)
      set_log_level DEBUG
      shift
      ;;
    # Other arguments...
  esac
done

log_debug "Debug mode enabled"  # Only shows if --debug was passed
log_info "Normal operation"

Note: For clarity, the logger provides in logging.sh enables DEBUG logging through the --verbose option when called using init_logger --verbose however the provided set_log_level function accepts log levels based on their common names (DEBUG, INFO, WARN, ERROR, FATAL) or their numeric values (0, 1, 2, 3, 4). The example above uses a command line parser in the calling script to optionally enable DEBUG logging by accepting a local argument --debug and then using the set_log_level function to enable DEBUG logging.

Advanced Usage with Custom Format and UTC Time

#!/bin/bash

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

# Initialize with custom format and UTC time
init_logger --format "%d %z [%l] [%s] %m" --utc

log_info "Starting processing job"

# Later, change format for a specific part of the script
set_log_format "[%l] %m"
log_info "Using simplified format"

# Return to original format
set_log_format "%d %z [%l] [%s] %m"
log_info "Back to detailed format"

Logging in Functions

#!/bin/bash

source /path/to/logging.sh
init_logger --log "/var/log/myapp.log"

function process_item() {
  local item=$1
  log_debug "Processing item: $item"
  
  # Processing logic...
  if [[ "$item" == "important" ]]; then
    log_info "Found important item"
  fi
  
  # Error handling
  if [[ "$?" -ne 0 ]]; then
    log_error "Failed to process item: $item"
    return 1
  fi
  
  log_debug "Completed processing item: $item"
  return 0
}

log_info "Starting batch processing"
process_item "test"
process_item "important"
log_info "Batch processing complete"

Sensitive Data

For sensitive data that should never be written to log files or the journal, use the log_sensitive function:

#!/bin/bash

source /path/to/logging.sh

init_logger --log "/var/log/myapp.log" --journal --tag "myapp"

# This will ONLY appear in the console, not in log files or journal
log_sensitive "Sensitive data: $SECRET"

The log_sensitive function will only output to the console and never to log files or the system journal. It is your responsibility to ensure that your console session is not being recorded or that any console logging is not accessible to unauthorized users.

Exit Codes

The init_logger function returns:

  • 0 on successful initialization
  • 1 on error (e.g., unable to create log file)

Troubleshooting

If you encounter issues:

  1. Ensure that logging.sh is sourced using the correct path
  2. Check write permissions if using file logging
  3. Verify log directory exists or can be created
  4. Ensure you're using valid log level names
  5. For journal logging, verify the logger command is available
  6. Check systemd journal logs with journalctl -f to see if logs are being received

License

This module is provided under the MIT License.

#!/bin/bash
#
# config_demo.sh - Demonstration of INI configuration file support
#
# This script demonstrates loading logger configuration from INI files,
# including CLI argument overrides and error handling.
set -e
# 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"
# Check if logger exists
if [[ ! -f "$LOGGER_PATH" ]]; then
echo "Error: Logger module not found at $LOGGER_PATH" >&2
exit 1
fi
# Create directories
LOGS_DIR="${PARENT_DIR}/logs"
CONFIG_DIR="${LOGS_DIR}/config_tests"
mkdir -p "$CONFIG_DIR"
# Source the logger module
source "$LOGGER_PATH"
echo "=========================================="
echo "Configuration File Demo for logging.sh"
echo "=========================================="
echo
# ====================================================
# Test 1: Basic configuration file
# ====================================================
echo "--- Test 1: Basic Configuration File ---"
CONFIG_FILE="${CONFIG_DIR}/basic.conf"
cat > "$CONFIG_FILE" << 'EOF'
# Basic logging configuration
[logging]
level = DEBUG
format = %d [%l] %m
color = auto
EOF
echo "Config file: $CONFIG_FILE"
echo "Contents:"
cat "$CONFIG_FILE"
echo
init_logger --config "$CONFIG_FILE" || exit 1
log_debug "Debug message from basic config"
log_info "Info message from basic config"
log_warn "Warning message from basic config"
log_error "Error message from basic config"
echo
# ====================================================
# Test 2: Configuration with file logging
# ====================================================
echo "--- Test 2: Configuration with File Logging ---"
LOG_FILE="${CONFIG_DIR}/app.log"
CONFIG_FILE="${CONFIG_DIR}/file_logging.conf"
cat > "$CONFIG_FILE" << EOF
# Configuration with file logging enabled
[logging]
level = INFO
log_file = ${LOG_FILE}
format = %d [%l] [%s] %m
EOF
echo "Config file: $CONFIG_FILE"
echo "Contents:"
cat "$CONFIG_FILE"
echo
init_logger --config "$CONFIG_FILE" || exit 1
log_info "This message goes to console and file"
log_warn "Warning also logged to file"
log_error "Error logged to file"
echo "Log file contents:"
cat "$LOG_FILE"
echo
# ====================================================
# Test 3: CLI arguments override config values
# ====================================================
echo "--- Test 3: CLI Override of Config Values ---"
CONFIG_FILE="${CONFIG_DIR}/override_test.conf"
cat > "$CONFIG_FILE" << 'EOF'
# Config sets DEBUG level, CLI will override
[logging]
level = DEBUG
format = [FROM_CONFIG] %l: %m
color = never
EOF
echo "Config file sets: level=DEBUG, format=[FROM_CONFIG]..."
echo "CLI will override: --level WARN"
echo
init_logger --config "$CONFIG_FILE" --level WARN || exit 1
log_debug "DEBUG - should NOT appear (CLI set level to WARN)"
log_info "INFO - should NOT appear (CLI set level to WARN)"
log_warn "WARN - should appear (matches CLI level)"
log_error "ERROR - should appear"
echo
# ====================================================
# Test 4: UTC time and custom format
# ====================================================
echo "--- Test 4: UTC Time and Custom Format ---"
CONFIG_FILE="${CONFIG_DIR}/utc_format.conf"
cat > "$CONFIG_FILE" << 'EOF'
# Configuration with UTC time and detailed format
[logging]
level = INFO
format = %d %z | %l | %s | %m
utc = true
color = auto
EOF
echo "Config file: $CONFIG_FILE"
echo "Contents:"
cat "$CONFIG_FILE"
echo
init_logger --config "$CONFIG_FILE" || exit 1
log_info "Message with UTC timestamp"
log_warn "Another UTC message"
echo
# ====================================================
# Test 5: Stderr level configuration
# ====================================================
echo "--- Test 5: Stderr Level Configuration ---"
CONFIG_FILE="${CONFIG_DIR}/stderr_level.conf"
cat > "$CONFIG_FILE" << 'EOF'
# Configuration with custom stderr threshold
[logging]
level = DEBUG
stderr_level = WARN
format = [%l] %m
color = never
EOF
echo "Config sets stderr_level=WARN (WARN and above to stderr)"
echo
init_logger --config "$CONFIG_FILE" || exit 1
echo "Suppressing stderr (2>/dev/null) - should see DEBUG and INFO only:"
(
log_debug "DEBUG goes to stdout"
log_info "INFO goes to stdout"
log_warn "WARN goes to stderr (hidden)"
log_error "ERROR goes to stderr (hidden)"
) 2>/dev/null
echo
# ====================================================
# Test 6: Quiet mode via config
# ====================================================
echo "--- Test 6: Quiet Mode via Config ---"
LOG_FILE="${CONFIG_DIR}/quiet_mode.log"
CONFIG_FILE="${CONFIG_DIR}/quiet_mode.conf"
cat > "$CONFIG_FILE" << EOF
# Quiet mode - no console output
[logging]
level = DEBUG
log_file = ${LOG_FILE}
quiet = true
format = %d [%l] %m
EOF
echo "Config sets quiet=true (no console output)"
echo "Messages will only go to log file"
echo
# Clear log file
> "$LOG_FILE"
init_logger --config "$CONFIG_FILE" || exit 1
log_info "This should NOT appear on console"
log_warn "This warning is also silent on console"
log_error "This error is only in the log file"
echo "Console output above should be empty."
echo "Log file contents:"
cat "$LOG_FILE"
echo
# ====================================================
# Test 7: Journal logging via config
# ====================================================
echo "--- Test 7: Journal Logging via Config ---"
if command -v logger &>/dev/null; then
CONFIG_FILE="${CONFIG_DIR}/journal.conf"
cat > "$CONFIG_FILE" << 'EOF'
# Configuration with journal logging
[logging]
level = INFO
journal = true
tag = config-demo
format = %d [%l] %m
EOF
echo "Config enables journal logging with tag 'config-demo'"
echo
init_logger --config "$CONFIG_FILE" || exit 1
log_info "Message sent to journal via config"
log_warn "Warning also sent to journal"
echo "View journal entries with: journalctl -t config-demo --since '1 minute ago'"
else
echo "Skipping journal test - 'logger' command not available"
fi
echo
# ====================================================
# Test 8: Boolean value variations
# ====================================================
echo "--- Test 8: Boolean Value Variations ---"
CONFIG_FILE="${CONFIG_DIR}/booleans.conf"
cat > "$CONFIG_FILE" << 'EOF'
# Test various boolean formats
[logging]
level = INFO
verbose = yes
utc = on
color = always
quiet = no
journal = off
EOF
echo "Config uses various boolean formats: yes, on, no, off"
echo
init_logger --config "$CONFIG_FILE" || exit 1
log_debug "DEBUG should appear (verbose=yes sets DEBUG level)"
log_info "UTC and colors should be enabled"
echo
# ====================================================
# Test 9: Error handling - missing file
# ====================================================
echo "--- Test 9: Error Handling - Missing File ---"
echo "Attempting to load non-existent config file..."
if init_logger --config "/nonexistent/path/config.conf" 2>&1; then
echo "ERROR: Should have failed!"
else
echo "Correctly failed with error (as expected)"
fi
echo
# ====================================================
# Test 10: Error handling - invalid values
# ====================================================
echo "--- Test 10: Error Handling - Invalid Values ---"
CONFIG_FILE="${CONFIG_DIR}/invalid.conf"
cat > "$CONFIG_FILE" << 'EOF'
# Configuration with some invalid values
[logging]
level = INFO
utc = maybe
color = sometimes
unknown_key = value
EOF
echo "Config has invalid values - should show warnings:"
echo
init_logger --config "$CONFIG_FILE" 2>&1
log_info "Logger still works despite warnings"
echo
# ====================================================
# Cleanup
# ====================================================
echo "--- Cleanup ---"
rm -rf "$CONFIG_DIR"
echo "Removed test config directory: $CONFIG_DIR"
echo
echo "=========================================="
echo "Configuration File Demo Complete"
echo "=========================================="
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!"
# logging.conf.example - Example configuration file for logging.sh
#
# Copy this file to logging.conf (or any name) and customize as needed.
# Use with: init_logger -c /path/to/logging.conf
#
# Lines starting with # or ; are comments
# Blank lines are ignored
# CLI arguments override values set in this file
[logging]
# Log level (default: INFO)
# Valid values: DEBUG, INFO, NOTICE, WARN, ERROR, CRITICAL, ALERT, EMERGENCY
# Can also use numeric values 0-7 (0=EMERGENCY, 7=DEBUG)
level = INFO
# Log message format (default: %d [%l] [%s] %m)
# Format variables:
# %d = date and time (YYYY-MM-DD HH:MM:SS)
# %z = timezone indicator (UTC or LOCAL)
# %l = log level name (DEBUG, INFO, WARN, ERROR, etc.)
# %s = script name
# %m = message
# Examples:
# "[%l] %d [%s] %m" => "[INFO] 2025-03-03 12:34:56 [myscript.sh] Hello"
# "%d %z [%l] %m" => "2025-03-03 12:34:56 UTC [INFO] Hello"
format = %d [%l] [%s] %m
# Log file path (default: empty/disabled)
# Leave empty or comment out to disable file logging
# The directory will be created if it doesn't exist
# log_file = /var/log/myapp/app.log
log_file =
# Enable systemd journal logging (default: false)
# Requires the 'logger' command to be available
journal = false
# Journal/syslog tag (default: script name)
# Used as the identifier in syslog/journal entries
# tag = myapp
tag =
# Use UTC timestamps (default: false)
# Set to true to use UTC time instead of local time
utc = false
# Color mode for console output (default: auto)
# Valid values:
# auto - Detect terminal color support automatically
# always - Always use colors
# never - Never use colors
color = auto
# Minimum level for stderr output (default: ERROR)
# Messages at this level and above (more severe) go to stderr
# Messages below this level go to stdout
# Valid values: DEBUG, INFO, NOTICE, WARN, ERROR, CRITICAL, ALERT, EMERGENCY
stderr_level = ERROR
# Disable console output (default: false)
# Set to true to suppress all console output (useful for daemon scripts)
quiet = false
# Enable verbose/debug logging (default: false)
# When true, sets log level to DEBUG
# Note: If both 'verbose' and 'level' are set, 'level' takes precedence
verbose = false
#!/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

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