lumberjack/lumberjack
Darren 'Tadgy' Austin 1adf0ee2b9 Version 0.2.2.
Remove -g option as it was broken and would never work.
Open and read from file descriptor for input rather than keep accessing a file.
Re-worked code around the user changing facility - it now actually works \o/.
Minor tweeks to the help text.
2020-06-14 16:03:43 +01:00

749 lines
26 KiB
Bash
Executable file

#!/bin/bash
# Copyright (c) 2018-2020:
# Darren 'Tadgy' Austin <darren (at) afterdark.org.uk>
# Licensed under the terms of the GNU General Public License version 3.
#
#........1.........2.........3.........4.........5.........6.........7.........8.........9.........0.........1..:......2.........3.........4.........5.........:
# Script details.
NAME="${0##*/}"
VERSION="0.2.2"
# Functions.
check_leading_dirs() {
# $1 The virtual host being processed.
# $2 The path of which to validate the leading directories.
[[ -z "$1" || -z "$2" ]] && return 1
if ! is_dir "$(remove_expansions "$2")"; then
(( FLAGS[${1}_template-prefix] == 0 )) && {
syslog "warn" "prefix directories of template do not exist: $(remove_expansions "$2")"
FLAGS[${1}_template-prefix]=1
}
return 1
else
(( FLAGS[${1}_template-prefix] == 1 )) && {
syslog "info" "prefix directories of template reappeared: $(remove_expansions "$2")"
FLAGS[${1}_template-prefix]=0
}
fi
return 0
}
close_fd() {
# $1 The site of which to close the file descriptor.
[[ -z "$1" ]] && return 1
# shellcheck disable=SC1083
{ exec {FDS[$1]}>&-; } 2>/dev/null || syslog "warn" "failed to close FD ${FDS[$1]} for $1"
unset "FDS[$1]" "FLAGS[${1}_template-prefix]" "FLAGS[${1}_make-dir-fail]" "FLAGS[${1}_fix-link]"
}
create_missing_dirs() {
# $1 The virtual host being processed.
# $2 The directory to insure exists.
[[ -z "$1" || -z "$2" ]] && return 1
if ! make_dir "$2"; then
(( FLAGS[${1}_make-dir-fail] == 0 )) && {
syslog "warn" "error creating log file's directory: $2"
FLAGS[${1}_make-dir-fail]=1
}
return 1
else
(( FLAGS[${1}_make-dir-fail] == 1 )) && {
syslog "info" "created log file's directory: $2"
FLAGS[${1}_make-dir-fail]=0
}
fi
return 0
}
die() {
# $1 The text of the error message to display on stderr.
printf "%s: %s\\n" "$NAME" "$1" >&2
exit 1
}
display_help() {
# |........1.........2.........3.........4.........5.........6.........7.........8
cat <<-EOF
Usage: $NAME [options] <basedir> <template>
Process input (possibly including an httpd VirtualHost site identifier) from
stdin or a pipe/FIFO and write a log line to a log file based upon the <basedir>
and <template>.
Options (all of which are optional):
-ca <arg> Set the compression command arguments. Default: ${COMPRESSOR_ARGS[@]}.
Quotes are required if more than one <arg> is supplied.
-cc <util> Set the compression command to use. Default: $COMPRESSOR.
-f Request flushing of the log file to disk after every write.
This may significantly reduce performance and result in a lot of
disk writes. Best to let the kernel do appropriate buffering.
-h Display this help.
-i <pipe> Read input from the pipe/FIFO at <pipe>, rather than stdin.
If the pipe/FIFO does not exist, it will be created. Use '-o'
to set the ownership of the pipe/FIFO. The pipe/FIFO is created
before any user or group switching is performed.
-j <jobs> Maximum number of compression jobs to have active at once.
Default: $MAXJOBS. Don't set this too high.
-l <link> Create a symlink named <link> to the currently active log file.
The <link> is created relative to <basedir>. In normal mode,
the link name may include the same '{}' sequence and %-escaped
formatting as the <template> (see below). In raw mode (-r), the
'{}' is not allowed, but % escape sequences can still be used.
WARNING: The (expanded) location of this link will be WIPED OUT!
-md <umask> Set the umask used when creating directories. Default: $DIR_UMASK.
Useful umasks are: 077, 066, 026 and 022.
-mf <umask> Set the umask used when creating files. Default: $FILE_UMASK.
Useful umasks are: 066 and 022.
-mp <umask> Set the umask used when creating the pipe. Default: $PIPE_UMASK.
Useful umasks are: 066 and 006.
-o <owner> Set the owner of the pipe/FIFO automatically created if none
already exists, and '-i' is used. The <owner> should be in the
format [user]:[group], where [user] or [group] is optional, but
not both. The ownership is changed before any user or group
switching is performed. This option is only available to root.
-p Make all parents. Normally, all directories up to - but not
including - the first directory with non-escaped %-format
strings of the <template> (see below) must already exist for the
log lines to be written to the file. With this option, the
parent directories of the logfile will be created automatically.
WARNING: This option can be unsafe with certain <template>
formats - it can result in creation of arbitrary directories
based on unclean input from outside sources.
-r Raw logging mode. In this mode, no processing of the log line
for an httpd VirtualHost site identifier is performed - log
lines are written verbatim to the log filename constructed from
<basedir> and <template>.
-s <facility> Set the syslog facility to be used for logging. Default: $SYSLOG_FACILITY.
-u <user> Set name of the user to run with. With this option, as soon as
$NAME starts it will re-exec itself to run as this user.
Log files created by $NAME will be owned by this user and
its primary group. The default is to run as the user that
executed $NAME, which is usually root. This option is
only available to root.
-v Display version and copyright information.
-z Enable compression of the old log files.
-- Cease option processing and begin argument parsing.
Option processing ceases with the first non-option argument or --.
Arguments (all of which are mandatory):
<basedir> The base directory of where to write the log files. The base
directory must exist.
<template> The filename template. When in normal mode, the template must
include at least one occurrance of '{}', which is replaced with
the site name from the VirtualHost identifier. In raw mode
(-r), the '{}' should not be included in the template. The
template may also include any %-prefixed format strings
recognised by the strftime(3) function. See below for examples.
The template can not start with a / - it is relative to the
<basedir>. Unless the '-p' option is used, all directories up
to - but not including - the first directory with non-escaped
%-format strings of the <template> must already exist for the
log lines to be written to the file.
Examples:
When used with the httpd CustomLog directive, using %v as the first log format
string:
"|$NAME '/path/to/logsdir' '{}/logs/access-log-%Y-%m'"
Where the httpd VirtualHost identifier is 'example.com', would write logs
(with the site identifier stripped) to the filename:
/path/to/logsdir/example.com/logs/access-log-<year>-<month>
"|$NAME '/path/to/logsdir' '{}/logs/()-access-log-%Y-%m'"
Where the httpd VirtualHost identifier is 'example.com', would write logs
(with the site identifier steipped) to the filename:
/path/to/logsdir/example.com/logs/example.com-access-log-<year>-<month>
When used with the httpd ErrorLog directive (both examples are equilivent):
"|$NAME -r '/path/to/logsdir' 'logs/error-log-%Y-%m'"
Would write raw log lines to the filename:
/path/to/logsdir/logs/error-log-<year>-<month>
"|$NAME -r '/path/to/logsdir/logs' 'error-log-%Y-%m'"
Equilivant to the above; would write raw log lines to the filename:
/path/to/logsdir/logs/error-log-<year>-<month>
EOF
}
display_version() {
# |........1.........2.........3.........4.........5.........6.........7.........8
cat <<-EOF
$NAME v$VERSION.
Copyright (c) 2018-2020 Darren 'Tadgy' Austin <darren (at) afterdark.org.uk>.
Licensed under the terms of the GNU GPL v3 <http://gnu.org/licenses/gpl.html>.
This program is free software; you can modify or redistribute it in accordence
with the GNU GPL. However, it comes with ABSOLUTELY NO WARRANTY; not even the
implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
EOF
}
exit_handler() {
(( INPUTFD != 0 )) && { exec {INPUTFD}>&-; } 2>/dev/null
(( FLAGS[created-fifo] == 1 )) && {
rm -f "$INPUT" 2>/dev/null || syslog "warn" "failed to remove pipe/fifo: $INPUT"
}
}
is_dir() {
# $1 The path to verify is a directory.
[[ -z "$1" ]] && return 1
[[ ! -d "$1" ]] && return 1
return 0
}
make_dir() {
# $1 The directory to create.
[[ -z "$1" ]] && return 1
if [[ ! -e "$1" ]]; then
umask "$DIR_UMASK"
mkdir -p "$1" 2>/dev/null || return 1
else
is_dir "$1" || return 1
fi
return 0
}
open_fd() {
# $1 The site/vhost identifier in the array.
# $2 The log file path to open.
[[ -z "$1" || -z "$2" ]] && return 1
umask "$FILE_UMASK"
# shellcheck disable=SC1083
if ! { exec {FDS[$1]}>>"$2"; } 2>/dev/null; then
(( FLAGS[${1}_open-fd-fail] == 0 )) && {
syslog "error" "failed to open log file for writing: $2"
FLAGS[${1}_open-fd-fail]=1
}
return 1
else
(( FLAGS[${1}_open-fd-fail] == 1 )) && {
syslog "info" "opened log file for writing: $2"
FLAGS[${1}_open-fd-fail]=0
}
fi
return 0
}
remove_expansions() {
# This function takes a template path as input and will output a path with elements
# starting with the first (non-escaped) %-sequence to the end of the path, removed.
# That is, it will return the path with the variable parts removed.
# $1 The path to parse.
local IFS='/' ITEMS INDEX
read -r -a ITEMS <<<"${1//+(\/)/\/}"
for INDEX in "${!ITEMS[@]}"; do
# Thanks to Marc Eberhard for helping with the regex that I couldn't quite get right.
if [[ "${ITEMS[INDEX]}" =~ ^((%%)*[^%]*)*[%]?$ ]]; then
printf "%s" "${ITEMS[INDEX]}"
[[ -n "${ITEMS[INDEX+1]}" ]] && [[ "${ITEMS[INDEX+1]}" =~ ^((%%)*[^%]*)*[%]?$ ]] && printf "%s" "/"
else
break
fi
done
}
sigchld_handler() {
local JOB
for JOB in "${!JOBS[@]}"; do
[[ "${JOBS[$JOB]}" ]] && {
! kill -0 "${JOBS[$JOB]}" >/dev/null 2>&1 && {
wait "${JOBS[$JOB]}"
unset "JOBS[$JOB]"
(( RUNNING_JOBS-- ))
}
}
done
start_compression_jobs
(( RUNNING_JOBS == 0 )) && set +bm
}
sighup_handler() {
local SITE
syslog "info" "closing all file descriptors"
for SITE in "${!FDS[@]}"; do
close_fd "$SITE"
done
}
sigterm_handler() {
local SITE
for SITE in "${!FDS[@]}"; do
close_fd "$SITE"
done
disown -a
exit 0
}
start_compression_jobs() {
local JOB
while (( RUNNING_JOBS < MAXJOBS )); do
for JOB in "${!JOBS[@]}"; do
[[ ! "${JOBS[$JOB]}" ]] && {
set -bm
"$COMPRESSOR" "${COMPRESSOR_ARGS[@]}" "$JOB" >/dev/null 2>&1 &
JOBS[$JOB]="$!"
(( RUNNING_JOBS++ ))
continue 2
}
done
break
done
}
syslog() {
# $1 The syslog level at which to log the message.
# $2 The text of the message to log.
[[ -z "$1" || -z "$2" ]] && return 1
logger --id="$$" -p "$SYSLOG_FACILITY.$1" -t "$NAME" "$1: $2" 2>/dev/null
}
# Extended globs are required.
shopt -s extglob
# Some variables.
# The array of file descriptors corresponding to each path.
declare -A FDS
# The array of jobs needing to be compressed.
declare -A JOBS
# The array of flags.
declare -A FLAGS
# The number of compression jobs currently active.
RUNNING_JOBS=0
# The original arguments to the script.
ORIG_ARGS=()
# Some detaults.
COMPRESSOR_ARGS=( "-9" )
COMPRESSOR="gzip" # Use gzip by default as log processing utils can often natively read gzipped files.
INPUT=""
INPUTFD="0"
MAXJOBS="4"
LINKFILE=""
DIR_UMASK="022"
FILE_UMASK="022"
PIPE_UMASK="066"
PIPE_OWNER=""
SYSLOG_FACILITY="user"
RUNAS_USER=""
FLAGS=([flush]=0 [raw]=0 [compress]=0 [make-parents]=0 [created-fifo]=0 [timed-out]=0 [basedir-vanished]=0 [basedir-notdir]=0)
# trap signals.
trap 'sigchld_handler' SIGCHLD
trap 'sighup_handler' SIGHUP
trap 'syslog "info" "received SIGUSR1 ping request"' SIGUSR1
trap 'sigterm_handler' SIGTERM
trap 'exit_handler' EXIT
# Parse command line options.
while :; do
case "$1" in
-ca)
# Set the compression command arguments.
[[ ! "$2" ]] && die "missing argument to -ca"
read -r -a COMPRESSOR_ARGS <<<"$2"
ORIG_ARGS+=("$1" "$2")
shift 2
continue
;;
-cc)
# Set the compression command to use.
[[ ! "$2" ]] && die "missing argument to -cc"
"$2" --help >/dev/null 2>&1 || die "invalid compressor command: $2"
COMPRESSOR="$2"
ORIG_ARGS+=("$1" "$2")
shift 2
continue
;;
-f)
# Flush files after every write.
FLAGS[flush]=1
ORIG_ARGS+=("$1")
shift
continue
;;
-h|-help|--help)
# Show the help screen and exit.
display_help
exit 0
;;
-i)
# Use a pipe/FIFO instead of stdin.
[[ ! "$2" ]] && die "missing argument to -i"
[[ "${2:0:1}" != "/" ]] && die "must be an absolute path for -i: $2"
INPUT="$2"
ORIG_ARGS+=("$1" "$2")
shift 2
continue
;;
-j)
# Set the maximum number of concurrent compression jobs to have active at once.
[[ ! "$2" =~ [0-9]+ ]] && die "invalid number of jobs: $2"
(( $2 == 0 )) && die "invalid number of jobs: $2"
MAXJOBS="$2"
ORIG_ARGS+=("$1" "$2")
shift 2
continue
;;
-l)
# Set the link name to use.
[[ ! "$2" ]] && die "missing argument to -l"
[[ "${2:0:1}" == "/" ]] && die "link name cannot begin with '/': $2"
[[ "${2: -1:1}" == "/" ]] && die "link name cannot end with '/': $2"
LINKFILE="$2"
ORIG_ARGS+=("$1" "$2")
shift 2
continue
;;
-md)
# Set the directory umask.
[[ ! "$2" ]] && die "missing argument to -md"
[[ ! "$2" =~ [0-7]{3,4} ]] && die "invalid umask given for -md: $2"
DIR_UMASK="$2"
ORIG_ARGS+=("$1" "$2")
shift 2
continue
;;
-mf)
# Set the file umask.
[[ ! "$2" ]] && die "missing argument to -mf"
[[ ! "$2" =~ [0-7]{3} ]] && die "invalid umask given for -mf: $2"
FILE_UMASK="$2"
ORIG_ARGS+=("$1" "$2")
shift 2
continue
;;
-mp)
# Set the pipe umask.
[[ ! "$2" ]] && die "missing argument to -mp"
[[ ! "$2" =~ [0-7]{3} ]] && die "invalid umask given for -mp: $2"
PIPE_UMASK="$2"
shift 2
continue
;;
-o)
# Set the ownership of the pipe/FIFO.
(( UID != 0 )) && die "only root can use -o"
[[ ! "$2" ]] && die "missing argument to -o"
[[ ! "$2" =~ ^.*:.*$ ]] && die "option -o must include a ':': $2"
[[ -z "${2%%:*}" ]] && [[ -z "${2##*:}" ]] && die "both owner and group missing from -o: $2"
[[ -n "${2%%:*}" ]] && { getent passwd "${2%%:*}" >/dev/null 2>&1 || die "invalid user part given for -o: $2"; }
[[ -n "${2##*:}" ]] && { getent group "${2##*:}" >/dev/null 2>&1 || die "invalid group part given for -o: $2"; }
PIPE_OWNER="$2"
shift 2
continue
;;
-p)
# Create parent directories.
FLAGS[make-parents]=1
ORIG_ARGS+=("$1")
shift
continue
;;
-r)
# Set raw mode.
FLAGS[raw]=1
ORIG_ARGS+=("$1")
shift
continue
;;
-s)
# Set the syslog facility.
[[ ! "${2,,}" =~ (auth|authpriv|cron|daemon|ftp|kern|lpr|mail|news|syslog|user|uucp|local[0-7]) ]] && die "invalid syslog facility: $2"
SYSLOG_FACILITY="${2,,}"
ORIG_ARGS+=("$1" "$2")
shift 2
continue
;;
-u)
# Set the user to run as.
(( UID != 0 )) && die "only root can use -u"
getent passwd "$2" >/dev/null 2>&1 || die "invalid user given for -u: $2"
RUNAS_USER="$2"
shift 2
continue
;;
-v)
# Show the version and exit.
display_version
exit 0
;;
-z)
# Compress logs once they are rotated.
FLAGS[compress]=1
ORIG_ARGS+=("$1")
shift
continue
;;
--)
# Stop processing options. Everything further is taken as an argument.
shift
break
;;
*)
break
;;
esac
done
# If there isn't 2 arguments left, exit.
(( $# != 2 )) && {
printf "%s\\n" "$NAME: incorrect number of non-option arguments" >&2
printf "%s\\n" "Try: $NAME -h" >&2
exit 1
}
# The remaining arguments should be the base directory and the template.
BASEDIR="${1/%\//}"
TEMPLATE="$2"
# Santy checking.
[[ "${BASEDIR:0:1}" != "/" ]] && die "must be an absolute path: $BASEDIR"
[[ ! -e "$BASEDIR" ]] && die "base directory does not exist: $BASEDIR"
[[ ! -d "$BASEDIR" ]] && die "not a directory: $BASEDIR"
(( "${FLAGS[make-parents]}" == 1 )) && [[ ! -w "$BASEDIR" ]] && die "no write permission: $BASEDIR"
[[ "${TEMPLATE: 0:1}" == "/" ]] && die "template cannot start with '/' - must be a relative path: $TEMPLATE"
[[ "${TEMPLATE: -1:1}" == "/" ]] && die "template cannot end with '/' - path must be a filename: $TEMPLATE"
(( FLAGS[raw] == 0 )) && [[ ! "$TEMPLATE" =~ .*\{\} ]] && die "template must include at least one '{}': $TEMPLATE"
(( FLAGS[raw] != 0 )) && [[ "$TEMPLATE" =~ .*\{\} ]] && die "template cannot include '{}': $TEMPLATE"
(( FLAGS[raw] != 0 )) && [[ "$LINKFILE" =~ .*\{\} ]] && die "link name cannot include '{}': $LINKFILE"
# If input is to be a pipe/FIFO, create it if necessary.
[[ -n "$INPUT" ]] && {
if [[ ! -e "$INPUT" ]]; then
umask "$PIPE_UMASK"
mkfifo "$INPUT" 2>/dev/null || die "failed to create pipe/FIFO: $INPUT"
FLAGS[created-fifo]=1
[[ -n "$PIPE_OWNER" ]] && { chown "$PIPE_OWNER" "$INPUT" >/dev/null 2>&1 || die "failed to chown pipe/FIFO: $INPUT"; }
elif [[ ! -p "$INPUT" ]]; then
die "not a pipe/FIFO: $INPUT"
fi
}
# Apply user and setting.
# shellcheck disable=SC2093
[[ -n "$RUNAS_USER" ]] && { exec -a "su" /bin/su - "$RUNAS_USER" -- "$0" "${ORIG_ARGS[@]}" "$BASEDIR" "$TEMPLATE" || die "failed to exec to change user"; }
# If input is to be a pipe/FIFO, open it.
[[ -n "$INPUT" ]] && { exec {INPUTFD}<"$INPUT" || die "failed to open pipe/FIFO for reading: $INPUT"; }
# Main loop
while :; do
# Reset used variables.
unset LOG_VHOST LOG_DATA
FLAGS[timed-out]=0
# Start compression jobs if there's any in the queue.
start_compression_jobs
# The time until the top of the next minute - this is used for the 'read' timeout so that
# closing log files and compression can still occur even if no log lines are written.
# Note: This does mean we can't have per second log files, but I can't see that being a requirement.
# shellcheck disable=SC2183
TTNM="$(( 60 - 10#$(printf "%(%S)T") ))"
# Read the log line.
# Note: The $(...) expansion should *not* be quoted in this instance, and the space between
# $( and (( is necessary to quiet shellcheck.
# shellcheck disable=SC2046
read -r -t "$TTNM" -u "$INPUTFD" $( (( FLAGS[raw] == 0 )) && printf "%s" "LOG_VHOST") LOG_DATA
ERR="$?"
# Determine how the read above was exited.
if (( ERR > 128 )); then
# If 'read' timed out, set a marker.
FLAGS[timed-out]=1
elif (( ERR == 1 )); then
(( INPUTFD == 0 )) && {
# stdin has been closed by the parent, quit gracefully by raising a SIGTERM.
kill -TERM "$$"
}
elif (( ERR != 0 )); then
# Unhandled error - log the issue and continue.
syslog "error" "unhandled return code from 'read': $ERR"
continue
fi
# Make sure the base path still exists - it could have disappeared while we were blocked in 'read'.
# Note: We won't make this directory ourselves - as it's the base directory it should exist on the system to start with.
if [[ ! -e "$BASEDIR" ]]; then
(( FLAGS[basedir-vanished] == 0 )) && {
syslog "error" "base directory has vanished"
FLAGS[basedir-vanished]=1
}
continue
else
(( FLAGS[basedir-vanished] == 1 )) && {
syslog "info" "base directory has reappeared"
FLAGS[basedir-vanished]=0
}
fi
# Make sure the base path is a directory.
if ! is_dir "$BASEDIR"; then
(( FLAGS[basedir-notdir] == 0 )) && {
syslog "error" "base path is no longer a directory"
FLAGS[basedir-notdir]=1
}
continue
else
(( FLAGS[basedir-notdir] == 1 )) && {
syslog "info" "base path has become directory again"
FLAGS[basedir-notdir]=0
}
fi
# Expand the strftime-encoded strings in the template.
EXPANDED_TEMPLATE="$(printf "%($TEMPLATE)T")"
# The old expanded template needs to be seeded if it's not already set from a previous loop.
# Set it to the same as the current expanded template so that no rotation is done the first time around.
[[ -z "$OLD_TEMPLATE" ]] && OLD_TEMPLATE="$EXPANDED_TEMPLATE"
# If the 'read' timed out and the exapnded template is the same as the old expanded template, there is no need to do anything.
(( FLAGS[timed-out] == 1 )) && [[ "$EXPANDED_TEMPLATE" == "$OLD_TEMPLATE" ]] && continue
# If the 'read' did not time out but the line read is empty, don't do anything.
(( FLAGS[timed-out] == 0 )) && [[ "$LOG_DATA" =~ ^[[:space:]]*$ ]] && continue
# If the new expanded template is different from the old, close and reopen all the logs and queue for compression (if required).
[[ "$EXPANDED_TEMPLATE" != "$OLD_TEMPLATE" ]] && {
# Loop through all the open FDs.
for SITE in "${!FDS[@]}"; do
# Generate the fully expanded filename from the strftime-expanded template and the site name from the array.
FILENAME="$BASEDIR/${EXPANDED_TEMPLATE//\{\}/$SITE}"
# Close the file descriptor for the old log file path.
close_fd "$SITE"
# Make sure the directory leading up to the expanded part of the template exists.
# Note: We don't create this part of the template's path tree as doing so would
# potentially allow any path to be created by use of a custom Host: header.
check_leading_dirs "$SITE" "$BASEDIR/${TEMPLATE//\{\}/$SITE}" || continue
# Create what's missing from the full log file's directory based on the expanded template.
create_missing_dirs "$SITE" "${FILENAME%/*}" || continue
# Open the new log file.
open_fd "$SITE" "$FILENAME" || continue
# Fix the now broken symlink - point it to the currently active log file.
[[ "$LINKFILE" ]] && {
LINKFILE_EXPANDED="$(printf "%($LINKFILE)T")"
# Note: This will clobber anything that already exists with the link name.
rm -rf "${BASEDIR:?}/${LINKFILE_EXPANDED//\{\}/$SITE}"
if ! ln -sfr "$FILENAME" "$BASEDIR/${LINKFILE_EXPANDED//\{\}/$SITE}"; then
(( FLAGS[${SITE}_fix-link] == 0 )) && {
syslog "error" "failed to fix link: $BASEDIR/${LINKFILE_EXPANDED//\{\}/$SITE}"
FLAGS[${SITE}_fix-link]=1
}
continue
else
(( FLAGS[${SITE}_fix-link] == 1 )) && {
syslog "info" "fixed link: $BASEDIR/${LINKFILE_EXPANDED//\{\}/$SITE}"
FLAGS[${SITE}_fix-link]=0
}
fi
}
# Add the old log file to the compression jobs task list.
(( FLAGS[compress] != 0 )) && {
JOBS+=([$BASEDIR/${OLD_TEMPLATE//\{\}/$SITE}]="")
}
done
}
# If the 'read' did not time out, there must be a log line to write.
(( FLAGS[timed-out] == 0 )) && {
# If not in raw mode, an unset LOG_VHOST is an error.
# If in raw mode, we need a placeholder for the FDS array element as LOG_VHOST would normally be unset.
if (( FLAGS[raw] == 0 )); then
[[ ! "$LOG_VHOST" ]] && {
syslog "error" "empty VirtualHost site identifier"
continue
}
else
LOG_VHOST="_raw_"
fi
# Generate the fully expanded filename from the strftime-expanded template.
FILENAME="$BASEDIR/${EXPANDED_TEMPLATE//\{\}/$LOG_VHOST}"
# Unless the -p option has been used, make sure the directory leading up to the
# expanded part of the template exists.
(( FLAGS[make-parents] == 0 )) && {
check_leading_dirs "$LOG_VHOST" "$BASEDIR/${TEMPLATE//\{\}/$LOG_VHOST}" || continue
}
# Create what's missing from the full log file's directory based on the expanded template.
create_missing_dirs "$LOG_VHOST" "${FILENAME%/*}" || continue
# If no FD is open for the LOG_VHOST, open it.
[[ -z "${FDS[$LOG_VHOST]}" ]] && {
open_fd "$LOG_VHOST" "$FILENAME" || continue
}
# Write the log entry.
printf "%s\\n" "$LOG_DATA" >&"${FDS[$LOG_VHOST]}"
# Flush data to disk if requested.
(( FLAGS[flush] == 1 )) && {
if ! sync "$FILENAME" 2>/dev/null; then
(( FLAGS[sync-fail] == 0 )) && {
syslog "warn" "failed to sync: $FILENAME"
FLAGS[sync-fail]=1
}
continue
else
(( FLAGS[sync-fail] == 1 )) && {
syslog "info" "sync successful: $FILENAME"
FLAGS[sync-fail]=0
}
fi
}
# Create symlink to the currently active log file.
[[ "$LINKFILE" ]] && {
LINKFILE_EXPANDED="$(printf "%($LINKFILE)T")"
[[ "$(stat -L --printf="%d:%i" "$BASEDIR/${LINKFILE_EXPANDED//\{\}/$LOG_VHOST}" 2>/dev/null)" != \
"$(stat --printf="%d:%i" "$FILENAME" 2>/dev/null)" ]] && {
# Note: This will clobber anything that already exists with the link name.
rm -rf "${BASEDIR:?}/${LINKFILE_EXPANDED//\{\}/$LOG_VHOST}"
if ! ln -sfr "$FILENAME" "$BASEDIR/${LINKFILE_EXPANDED//\{\}/$LOG_VHOST}" 2>/dev/null; then
(( FLAGS[${LOG_VHOST}_create-link] == 0 )) && {
syslog "error" "failed to create link: $BASEDIR/${LINKFILE_EXPANDED//\{\}/$LOG_VHOST}"
FLAGS[${LOG_VHOST}_create-link]=1
}
continue
else
(( FLAGS[${LOG_VHOST}_create-link] == 1 )) && {
syslog "info" "created link: $BASEDIR/${LINKFILE_EXPANDED//\{\}/$LOG_VHOST}"
FLAGS[${LOG_VHOST}_create-link]=0
}
fi
}
}
}
# Store the last used filename.
OLD_TEMPLATE="$EXPANDED_TEMPLATE"
done