Files
fa f34d24a30e fix(docker): follow rotated router logs in entrypoint
- replace single follow_log usage with dedicated wrapper and router log followers
- follow newest log-router-*.txt and switch tails when router rotates/restarts
- add log_debug helper and DEBUG_ENTRYPOINT-gated wait/attach/switch diagnostics
- simplify entrypoint log follower calls to use built-in WRAPPER/ROUTER labels
2026-07-16 13:20:36 +00:00

416 lines
13 KiB
Bash

#!/bin/sh
#####################################################################
## Purpose
# This file contains common shell functions used by scripts of the
# docker entrypoint
log() {
msg="$*"
printf '%s [ENTRYPOINT] %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$msg"
}
log_info() {
msg="$*"
log "INFO: " "$msg"
}
log_warn() {
msg="$*"
log "WARN: " "$msg"
}
log_error() {
msg="$*"
log "ERROR: " "$msg"
}
log_debug() {
msg="$*"
log "DEBUG: " "$msg"
}
##
# @function follow_wrapper_log
# @description Streams a single log file with UTC timestamps.
# @details Waits until the log file exists, then tails from the first line in a
# background process and prefixes each emitted line with timestamp and label.
# @param $1 Path to the log file to follow.
# @returns 0 when the background follower is started.
follow_wrapper_log() {
logfile="$1"
label="WRAPPER"
sh -c '
logfile="$1"
label="$2"
while [ ! -f "$logfile" ]; do sleep 0.5; done
tail -n +1 -F "$logfile" 2>/dev/null | while IFS= read -r line; do
printf "%s [%s] %s\n" "$(date -u "+%Y-%m-%dT%H:%M:%SZ")" "$label" "$line"
done
' sh "$logfile" "$label" &
}
##
# @function follow_router_log
# @description Streams the active router log file and switches when rotation changes files.
# @details Watches the given directory for files matching log-router-*.txt,
# follows the most recently modified file, and switches when a different file
# becomes newer.
# @param $1 Path to the router log directory.
# @returns 0 when the background watcher is started.
follow_router_log() {
logdir="$1"
label="ROUTER"
(
current_log=""
current_tail_pid=""
waiting_for_router_log_notice=0
cleanup_tail() {
if [ -n "$current_tail_pid" ] && kill -0 "$current_tail_pid" 2>/dev/null; then
kill "$current_tail_pid" 2>/dev/null || true
wait "$current_tail_pid" 2>/dev/null || true
fi
current_tail_pid=""
}
trap 'cleanup_tail; exit 0' INT TERM HUP QUIT
while :; do
latest_log="$(ls -1t "$logdir"/log-router-*.txt 2>/dev/null | head -n 1)"
if [ -z "$latest_log" ]; then
if [ -n "${DEBUG_ENTRYPOINT:-}" ] && [ "$waiting_for_router_log_notice" -eq 0 ]; then
log_debug "Follow_router_log waiting for log-router-*.txt in $logdir"
waiting_for_router_log_notice=1
fi
sleep 1
continue
fi
waiting_for_router_log_notice=0
if [ -n "$latest_log" ] && [ "$latest_log" != "$current_log" ]; then
previous_log="$current_log"
cleanup_tail
current_log="$latest_log"
if [ -n "${DEBUG_ENTRYPOINT:-}" ]; then
if [ -n "$previous_log" ]; then
log_debug "Follow_router_log switching file ${previous_log##*/} -> ${current_log##*/}"
else
log_debug "Follow_router_log attaching to ${current_log##*/}"
fi
fi
sh -c '
logfile="$1"
label="$2"
tail -n +1 -F "$logfile" 2>/dev/null | while IFS= read -r line; do
printf "%s [%s] %s\n" "$(date -u "+%Y-%m-%dT%H:%M:%SZ")" "$label" "$line"
done
' sh "$current_log" "$label" &
current_tail_pid=$!
fi
sleep 1
done
) &
}
##
# @function forward_router_signal
# @description Forwards a signal to the router child process when it is still running.
# @details Expects ROUTER_PID to be set by the caller script.
# @param $1 Signal name (for example TERM, INT, HUP, QUIT).
# @returns 0 when handled.
forward_router_signal() {
sig="$1"
if kill -0 "$ROUTER_PID" 2>/dev/null; then
kill -"$sig" "$ROUTER_PID" 2>/dev/null || true
fi
}
##
# @function has_ipv4_connectivity
# @description Returns success when at least one non-loopback global IPv4 address
# is configured on an interface that is up.
# @returns 0 if a global IPv4 address is present, otherwise 1.
has_ipv4_connectivity() {
ip -4 -o addr show up scope global 2>/dev/null | grep -q 'inet '
}
##
# @function has_ipv6_connectivity
# @description Returns success when IPv6 is enabled and at least one non-loopback
# global IPv6 address is configured on an interface that is up.
# @details If /proc/sys/net/ipv6/conf/all/disable_ipv6 is 1, this returns failure.
# @returns 0 if global IPv6 connectivity is available, otherwise 1.
has_ipv6_connectivity() {
if [ -r /proc/sys/net/ipv6/conf/all/disable_ipv6 ] && \
[ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null)" = "1" ]; then
return 1
fi
ip -6 -o addr show up scope global 2>/dev/null | grep -q 'inet6 '
}
##
# @function has_host_network_mode
# @description Returns success when interface names indicate Docker host/network bridge plumbing.
# @details This keeps the existing heuristic based on docker, br-, or veth link names.
# @returns 0 when host-network indicators are present, otherwise 1.
has_host_network() {
ip link show 2>/dev/null | grep -qE 'docker|br-|veth'
}
##
# @function get_last_hostname_ipv4
# @description Prints the last IPv4 token returned by `hostname -i`.
# @details Compatibility helper only; `hostname -i` output ordering is not guaranteed.
# @returns 0 and prints an IPv4 address when found, otherwise 1.
get_last_hostname_ipv4() {
hostname -i 2>/dev/null | awk '
{
for (i = 1; i <= NF; i++) {
if ($i ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) {
ip = $i
}
}
}
END {
if (ip != "") {
print ip
exit 0
}
exit 1
}
'
}
##
# @function get_default_route_ipv4
# @description Prints the IPv4 source address chosen by kernel routing.
# @details Uses `ip -4 route get 1.1.1.1` and extracts the `src` value.
# @returns 0 and prints an IPv4 address when found, otherwise 1.
get_default_route_ipv4() {
ip -4 route get 1.1.1.1 2>/dev/null | awk '
{
for (i = 1; i <= NF; i++) {
if ($i == "src" && (i + 1) <= NF) {
print $(i + 1)
found = 1
exit
}
}
}
END {
if (!found) {
exit 1
}
}
'
}
##
# @function get_private_bind_ipv4
# @description Picks a conservative IPv4 bind address for containerized services.
# @details In host network mode, returns 127.0.0.1 to keep services local.
# In bridged mode, prefers the default-route source address.
# Falls back to the last IPv4 from `hostname -i`, then 127.0.0.1.
# @returns 0 and prints the selected IPv4 address.
get_private_bind_ipv4() {
selected_ipv4=""
if has_host_network; then
printf '%s\n' "127.0.0.1"
return 0
fi
if selected_ipv4="$(get_default_route_ipv4)" && [ -n "$selected_ipv4" ]; then
case "$selected_ipv4" in
127.*) ;;
*)
printf '%s\n' "$selected_ipv4"
return 0
;;
esac
fi
if selected_ipv4="$(get_last_hostname_ipv4)" && [ -n "$selected_ipv4" ]; then
case "$selected_ipv4" in
127.*) ;;
*)
printf '%s\n' "$selected_ipv4"
return 0
;;
esac
fi
printf '%s\n' "127.0.0.1"
}
##
# @function update_config_value
# @description Updates matching key/value entries in a config file.
# @details Only non-comment lines with `key=value` format are considered. A line is
# rewritten when the key matches `key_regex` and the existing value matches the
# optional `current_value_regex` (defaults to matching any value).
# @param $1 Path to the config file.
# @param $2 Regular expression for the key (left side of `=`).
# @param $3 New value to write (right side of `=`).
# @param $4 Optional regular expression for current value filtering.
# @returns 0 on success, otherwise 1.
update_config_value() {
# Usage:
# update_config_value <config_file> <key_regex> <new_value> [current_value_regex]
#
# Examples:
# update_config_value "/path/file.conf" '^foo\.bar$' '10.0.0.1'
# update_config_value "/path/file.conf" '^foo\.bar$' '10.0.0.1' '^127\.0\.0\.1$'
if [ $# -lt 3 ] || [ $# -gt 4 ]; then
log_error "Usage: update_config_value <config_file> <key_regex> <new_value> [current_value_regex]"
return 1
fi
cfg="$1"
key_re="$2"
new_value="$3"
current_re="${4:-.*}" # default: match any current value
if [ ! -f "$cfg" ]; then
log_error "Config file not found: $cfg"
return 1
fi
awk_bin="$(command -v awk)"
if [ -z "$awk_bin" ]; then
log_error "awk is required for update_config_value"
return 1
fi
if ! tmpfile="$(mktemp "${cfg}.tmp.XXXXXX")"; then
log_error "Failed to create temporary file for $cfg"
return 1
fi
if "$awk_bin" -v key_re="$key_re" -v new_value="$new_value" -v current_re="$current_re" -v debug_entrypoint="${DEBUG_ENTRYPOINT:-}" '
BEGIN { updated = 0 }
# Keep comments untouched
/^[[:space:]]*#/ { print; next }
{
eq = index($0, "=")
if (eq > 0) {
key = substr($0, 1, eq - 1)
val = substr($0, eq + 1)
# Update only matching key, and only matching current value (if provided)
if (key ~ key_re && val ~ current_re) {
print key "=" new_value
updated++
next
}
}
print
}
END {
if (debug_entrypoint != "") {
printf "update_config_value: updated %d line(s)\n", updated > "/dev/stderr"
}
}
' "$cfg" > "$tmpfile"; then
if ! mv "$tmpfile" "$cfg"; then
log_error "Failed to replace config file: $cfg"
rm -f "$tmpfile"
return 1
fi
else
log_error "awk failed while updating '$cfg' with key regex '$key_re'"
rm -f "$tmpfile"
return 1
fi
}
##
# @function update_jetty_xml_set_value
# @description Updates XML lines in the form `<Set name="...">value</Set>`.
# @details A line is rewritten when the Set name equals `set_name` and the
# existing inner value matches the optional `current_value_regex` (defaults to
# matching any value). The helper is intended for simple single-line Set tags.
# @param $1 Path to the XML file.
# @param $2 Set name to match (for example `host`).
# @param $3 New inner value to write.
# @param $4 Optional regular expression for current value filtering.
# @returns 0 on success, otherwise 1.
update_jetty_xml_set_value() {
# Usage:
# update_jetty_xml_set_value <xml_file> <set_name> <new_value> [current_value_regex]
#
# Examples:
# update_jetty_xml_set_value "/path/jetty.xml" 'host' '0.0.0.0'
# update_jetty_xml_set_value "/path/jetty.xml" 'host' '0.0.0.0' '^127\.0\.0\.1$'
if [ $# -lt 3 ] || [ $# -gt 4 ]; then
log_error "Usage: update_jetty_xml_set_value <xml_file> <set_name> <new_value> [current_value_regex]"
return 1
fi
xml_file="$1"
set_name="$2"
new_value="$3"
current_re="${4:-.*}"
if [ ! -f "$xml_file" ]; then
log_error "XML file not found: $xml_file"
return 1
fi
awk_bin="$(command -v awk)"
if [ -z "$awk_bin" ]; then
log_error "awk is required for update_jetty_xml_set_value"
return 1
fi
if ! tmpfile="$(mktemp "${xml_file}.tmp.XXXXXX")"; then
log_error "Failed to create temporary file for $xml_file"
return 1
fi
if "$awk_bin" -v set_name="$set_name" -v new_value="$new_value" -v current_re="$current_re" -v debug_entrypoint="${DEBUG_ENTRYPOINT:-}" '
BEGIN { updated = 0 }
{
# Match simple, single-line Set tags: <Set name="host">value</Set>
line_re = "^[[:space:]]*<Set[[:space:]]+name=\"" set_name "\">[^<]*</Set>[[:space:]]*$"
if ($0 ~ line_re) {
value = $0
sub("^[[:space:]]*<Set[[:space:]]+name=\"" set_name "\">", "", value)
sub("</Set>[[:space:]]*$", "", value)
if (value ~ current_re) {
sub(/>[^<]*</, ">" new_value "<")
updated++
}
}
print
}
END {
if (debug_entrypoint != "") {
printf "update_jetty_xml_set_value: updated %d line(s)\n", updated > "/dev/stderr"
}
}
' "$xml_file" > "$tmpfile"; then
if ! mv "$tmpfile" "$xml_file"; then
log_error "Failed to replace XML file: $xml_file"
rm -f "$tmpfile"
return 1
fi
else
log_error "awk failed while updating '$xml_file' for Set name '$set_name'"
rm -f "$tmpfile"
return 1
fi
}