|
#!/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 |
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.