Logging with shell script - catch STDERR to log with timestamp
Logging with Shell Script: Capturing STDERR to Log with Timestamp - A Comprehensive Guide by revWhiteShadow
At revWhiteShadow, we understand the importance of robust logging in shell scripting. Effective logging is crucial for debugging, monitoring, and auditing shell scripts, especially in complex automation tasks. One particularly useful technique is capturing STDERR
(Standard Error) along with timestamps to a log file. This approach allows us to trace errors and diagnose issues within our scripts more effectively. This article expands on the initial exploration of this topic, providing a more in-depth and refined method suitable for production environments, going beyond the simple examples and incorporating best practices.
Why Log STDERR with Timestamps? The Need for Comprehensive Error Tracking
The rationale behind meticulously logging STDERR
with timestamps is multifaceted:
- Detailed Debugging:
STDERR
often contains crucial information about errors, warnings, and exceptions encountered during script execution. By logging these messages along with timestamps, we can precisely pinpoint when and where issues occurred. This significantly simplifies the debugging process. - Root Cause Analysis: When an error occurs, the timestamp helps us correlate the error message with other events happening in the system, providing invaluable context for root cause analysis. We can examine system logs, network traffic, or other relevant data to understand the conditions leading to the error.
- Auditing and Compliance: In many environments, it is essential to maintain an audit trail of all script activities. Logging
STDERR
helps ensure compliance with auditing requirements by providing a record of any errors or failures that occurred. - Proactive Monitoring: By monitoring the log file for
STDERR
entries, we can proactively identify and address potential issues before they escalate into major problems. Automated alerts can be set up to notify administrators when specific error patterns are detected. - Script Improvement: Analyzing logged
STDERR
data over time can reveal patterns and trends in script errors. This information can be used to identify areas where the script can be improved, making it more robust and reliable.
A Refined Approach to STDERR Logging in Shell Scripts
We can implement a more robust and flexible logging solution, with features like configurable log levels, centralized logging functions, and error handling. Here’s how we approach it:
1. Defining Logging Functions
Instead of relying solely on printf
, we encapsulate our logging logic within dedicated functions. These functions allow us to:
- Standardize Log Format: Ensure consistency in the log output, making it easier to parse and analyze.
- Implement Log Levels: Introduce different log levels (e.g., DEBUG, INFO, WARNING, ERROR) to control the verbosity of the logging.
- Centralize Logging Logic: Modify the logging behavior in one place instead of having to change multiple lines of code throughout the script.
- Add Function Name: Include the function name in log messages, for better traceability.
#!/bin/bash
# Global variables for log configuration
LOG_FILE="/var/log/my_script.log"
LOG_LEVEL="INFO" # Default log level
# Function to determine if a message should be logged based on log level
should_log() {
local level="$1"
case "$LOG_LEVEL" in
DEBUG) return 0 ;; # Log everything
INFO) if [[ "$level" == "DEBUG" ]]; then return 1; else return 0; fi ;;
WARNING) if [[ "$level" == "DEBUG" || "$level" == "INFO" ]]; then return 1; else return 0; fi ;;
ERROR) if [[ "$level" == "DEBUG" || "$level" == "INFO" || "$level" == "WARNING" ]]; then return 1; else return 0; fi ;;
*) return 1 ;; # Don't log anything (invalid log level)
esac
}
# Logging function with log level
log_message() {
local level="$1"
local message="$2"
local func_name="${FUNCNAME[1]}" # Get the calling function's name
if should_log "$level"; then
printf "%(%Y-%m-%d %H:%M:%S)T [%s] %s: %s\n" -1 "$level" "$func_name" "$message" >> "$LOG_FILE"
fi
}
# Function to log STDERR
log_stderr() {
local message="$1"
log_message "ERROR" "$message"
}
# Function to log STDOUT
log_stdout() {
local message="$1"
log_message "INFO" "$message"
}
# Example function with error handling
example_function() {
log_message "INFO" "Starting example_function"
# Simulate an error
if ! ls /nonexistent_file; then
log_stderr "Failed to list /nonexistent_file"
return 1 # Indicate failure
fi
log_message "INFO" "Finished example_function successfully"
return 0 # Indicate success
}
# Main script logic
main() {
log_message "INFO" "Script started"
# Run the example function
example_function
log_message "INFO" "Script finished"
}
# Execute the main function
main
In this example:
LOG_FILE
specifies the path to the log file.LOG_LEVEL
defines the current log level, controlling which messages are logged.log_message
is the core logging function, taking a log level and message as input. It formats the message with a timestamp and log level before appending it to the log file.log_stderr
andlog_stdout
are convenience functions that calllog_message
with the “ERROR” and “INFO” log levels, respectively.example_function
demonstrates how to use the logging functions within a script. It simulates an error and logs it usinglog_stderr
.
2. Capturing STDERR and STDOUT with Process Substitution
Process substitution allows us to treat the output of a command as a file. We can use this to redirect STDERR
and STDOUT
to our logging functions:
# Redirect STDERR and STDOUT to logging functions
exec 2> >(while read -r line; do log_stderr "$line"; done)
exec 1> >(while read -r line; do log_stdout "$line"; done)
exec
command with redirections, ensures that all subsequent commands in the script will have their standard output and standard error redirected as specified.
This snippet redirects STDERR
and STDOUT
to the log_stderr
and log_stdout
functions, respectively. Any output written to STDERR
or STDOUT
will be captured, formatted, and written to the log file.
3. Integrating Logging into Script Execution
With the logging functions and redirection in place, we can execute our script logic:
# Your script logic here
command1
command2
command3
Any errors generated by these commands will be automatically captured and logged to the log file, along with timestamps and the “ERROR” log level.
4. Improved Error Handling
The logging setup is integrated into the functions, allowing for better error handling. In this case, checking exit codes of the commands that can error, is important. Here is one of the many ways you can approach:
run_command() {
local command="$1"
shift
"$command" "$@"
local result=$?
if [[ $result -ne 0 ]]; then
log_stderr "Command '$command $@' failed with exit code $result"
fi
return $result
}
# Usage example
run_command ls -l /path/to/file
if [[ $? -ne 0 ]]; then
echo "The previous command failed"
fi
5. Creating Log Files
The following script creates a log file or gives an error if the user doesn’t have the correct user rights.
create_log_file() {
if [[ ! -f "$LOG_FILE" ]]; then
touch "$LOG_FILE"
if [[ $? -ne 0 ]]; then
echo "Error: Could not create log file '$LOG_FILE'. Ensure you have write permissions."
exit 1
fi
echo "Log file created: $LOG_FILE"
else
echo "Log file already exists: $LOG_FILE"
fi
# Check write permissions
if [[ ! -w "$LOG_FILE" ]]; then
echo "Error: Log file '$LOG_FILE' is not writable. Ensure you have write permissions."
exit 1
fi
}
# Call the function
create_log_file
Complete Example Script
Putting it all together, here’s a complete example script that demonstrates the refined approach to STDERR
logging with timestamps:
#!/bin/bash
# Global variables for log configuration
LOG_FILE="/var/log/my_script.log"
LOG_LEVEL="INFO" # Default log level
# Function to determine if a message should be logged based on log level
should_log() {
local level="$1"
case "$LOG_LEVEL" in
DEBUG) return 0 ;; # Log everything
INFO) if [[ "$level" == "DEBUG" ]]; then return 1; else return 0; fi ;;
WARNING) if [[ "$level" == "DEBUG" || "$level" == "INFO" ]]; then return 1; else return 0; fi ;;
ERROR) if [[ "$level" == "DEBUG" || "$level" == "INFO" || "$level" == "WARNING" ]]; then return 1; else return 0; fi ;;
*) return 1 ;; # Don't log anything (invalid log level)
esac
}
# Logging function with log level and function name
log_message() {
local level="$1"
local message="$2"
local func_name="${FUNCNAME[1]}" # Get the calling function's name
if should_log "$level"; then
printf "%(%Y-%m-%d %H:%M:%S)T [%s] %s: %s\n" -1 "$level" "$func_name" "$message" >> "$LOG_FILE"
fi
}
# Function to log STDERR
log_stderr() {
local message="$1"
log_message "ERROR" "$message"
}
# Function to log STDOUT
log_stdout() {
local message="$1"
log_message "INFO" "$message"
}
run_command() {
local command="$1"
shift
"$command" "$@"
local result=$?
if [[ $result -ne 0 ]]; then
log_stderr "Command '$command $@' failed with exit code $result"
fi
return $result
}
create_log_file() {
if [[ ! -f "$LOG_FILE" ]]; then
touch "$LOG_FILE"
if [[ $? -ne 0 ]]; then
echo "Error: Could not create log file '$LOG_FILE'. Ensure you have write permissions."
exit 1
fi
echo "Log file created: $LOG_FILE"
else
echo "Log file already exists: $LOG_FILE"
fi
# Check write permissions
if [[ ! -w "$LOG_FILE" ]]; then
echo "Error: Log file '$LOG_FILE' is not writable. Ensure you have write permissions."
exit 1
fi
}
# Example function with error handling
example_function() {
log_message "INFO" "Starting example_function"
# Simulate an error
run_command ls /nonexistent_file
if [[ $? -ne 0 ]]; then
log_stderr "Failed to list /nonexistent_file"
return 1 # Indicate failure
fi
log_message "INFO" "Finished example_function successfully"
return 0 # Indicate success
}
# Main script logic
main() {
create_log_file
# Redirect STDERR and STDOUT to logging functions
exec 2> >(while read -r line; do log_stderr "$line"; done)
exec 1> >(while read -r line; do log_stdout "$line"; done)
log_message "INFO" "Script started"
# Run the example function
example_function
log_message "INFO" "Script finished"
}
# Execute the main function
main
Advanced Considerations and Best Practices
- Log Rotation: Implement log rotation to prevent the log file from growing indefinitely. Tools like
logrotate
can be used to automate this process. - Centralized Logging: For larger environments, consider using a centralized logging server to collect logs from multiple machines. This simplifies log analysis and correlation.
- Security: Protect the log file from unauthorized access. Ensure that only authorized users can read or write to the log file.
- Performance: Be mindful of the performance impact of logging, especially in high-volume environments. Avoid excessive logging and use efficient logging techniques.
- Customization: Adapt the logging functions and format to meet the specific needs of your environment. Consider adding additional information to the log messages, such as the hostname or process ID.
- Error Codes: Consistently use and log error codes to streamline debugging and automate error handling processes.
- Configuration Files: Store log file paths and log levels in configuration files to enhance flexibility and ease of script management.
Conclusion: Mastering Shell Script Logging for Enhanced Reliability
By implementing a robust and well-designed logging strategy, we can significantly improve the reliability, maintainability, and security of our shell scripts. Capturing STDERR
with timestamps is a crucial aspect of this strategy, providing invaluable insights into script behavior and facilitating rapid problem resolution. The methods detailed here offer a comprehensive foundation for implementing advanced logging practices in any shell scripting environment. At revWhiteShadow, we believe that meticulous attention to logging is an essential component of professional software development and systems administration. We encourage you to adopt these techniques and tailor them to your specific requirements, ensuring that your scripts are well-monitored, easily debugged, and always operating at peak performance.