Skip to content

Instantly share code, notes, and snippets.

@GingerGraham
Last active September 3, 2025 17:22
Show Gist options
  • Save GingerGraham/99af97eed2cd89cd047a2088947a5405 to your computer and use it in GitHub Desktop.
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
  • Optional file output
  • Optional systemd journal logging
  • Customizable log format
  • UTC or local time 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
-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)
-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"

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"

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"

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.

Copyright © 2025 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
# 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: Combined Features Demo
# ====================================================
echo -e "\n========== PART 6: 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 7: Quiet Mode Demo
# ====================================================
echo -e "\n========== PART 7: 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 =========="
# ====================================================
# 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
#
# 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 [-l|--log FILE] [-q|--quiet] [-v|--verbose] [-d|--level LEVEL] [-f|--format FORMAT] [-j|--journal] [-t|--tag TAG] [--color] [--no-color]
#
# 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"
# 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
}
# Check if logger command is available
check_logger_available() {
command -v logger &>/dev/null
}
# 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
# Parse command line arguments
while [[ "$#" -gt 0 ]]; do
case $1 in
--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
;;
*)
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), 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
if should_use_colors; then
# Color output for console based on log level
case "$level_name" in
"DEBUG")
echo -e "\e[34m${log_entry}\e[0m" # Blue
;;
"INFO")
echo -e "${log_entry}" # Default color
;;
"NOTICE")
echo -e "\e[32m${log_entry}\e[0m" # Green
;;
"WARN")
echo -e "\e[33m${log_entry}\e[0m" # Yellow
;;
"ERROR")
echo -e "\e[31m${log_entry}\e[0m" >&2 # Red, to stderr
;;
"CRITICAL")
echo -e "\e[31;1m${log_entry}\e[0m" >&2 # Bright Red, to stderr
;;
"ALERT")
echo -e "\e[37;41m${log_entry}\e[0m" >&2 # White on Red background, to stderr
;;
"EMERGENCY"|"FATAL")
echo -e "\e[1;37;41m${log_entry}\e[0m" >&2 # Bold White on Red background, to stderr
;;
"INIT")
echo -e "\e[35m${log_entry}\e[0m" # Purple for init
;;
"SENSITIVE")
echo -e "\e[36m${log_entry}\e[0m" # Cyan for sensitive
;;
*)
echo "${log_entry}" # Default color for unknown level
;;
esac
else
# Plain output without colors
if [[ "$level_name" == "ERROR" || "$level_name" == "CRITICAL" || "$level_name" == "ALERT" || "$level_name" == "EMERGENCY" || "$level_name" == "FATAL" ]]; then
echo "${log_entry}" >&2 # Error messages to stderr
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment