From c7297a512c31de6c50907a80054cf1a6e89ba922 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Sun, 19 Jul 2026 15:44:40 -0600 Subject: [PATCH 01/60] [version] Update version, modified date --- cfddns.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cfddns.sh b/cfddns.sh index add4876..a634291 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -3,8 +3,8 @@ # # update Cloudflare DNS records with current (dynamic) IP address # Script by Asif Bacchus -# Last modified: May 10, 2021 -# Version 2.2 +# Last modified: July 19, 2026 +# Version 3.0 # ### text formatting presets using tput From 5c6c8ec4ea80d74cc492cc3b5e8a6c704b7f9572 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Sun, 19 Jul 2026 18:49:34 -0600 Subject: [PATCH 02/60] [params] Create journal logging switch Create boolean variable to store journal logging preference. Set to 'false' by default. Add parameter to activate this preference. Update help to include this new parameter. --- cfddns.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cfddns.sh b/cfddns.sh index a634291..e09f0b6 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -157,6 +157,10 @@ scriptHelp() { textBlock "Switch value. Output log to console (stdout) instead of a log file. Can be combined with --nc if desired." textBlockDefaults "(disabled: output to log file)" newline + textBlockSwitches "--log-journal" + textBlock "Switch value. Output a structured log entry to your journaling system instead of a log file. Implies --nc." + textBlockDefaults "(disabled: output to log file)" + newline textBlockSwitches "--no-log" textBlock "Switch value. Do not create a log (i.e. no console, no file). You will not have *any* output from the script if you choose this option, so you will not know if updates succeeded or failed." textBlockDefaults "(disabled: output to log file)" @@ -253,6 +257,7 @@ writeLog() { scriptPath="$(CDPATH='' \cd -- "$(dirname -- "$0")" && pwd -P)" scriptName="$(basename "$0")" logFile="$scriptPath/${scriptName%.*}.log" +logToJournal=0 accountFile="$scriptPath/cloudflare.credentials" colourizeLogFile=1 dnsRecords="" @@ -292,6 +297,10 @@ while [ $# -gt 0 ]; do # log to the console instead of a file logFile="/dev/stdout" ;; + --log-journal) + # log to the journal system instead of a file + logToJournal=1 + ;; --no-log) # do not log anything logFile="/dev/null" From 3be6eaee0c93fa7989fd6ec7ae419724835f058d Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Sun, 19 Jul 2026 18:52:34 -0600 Subject: [PATCH 03/60] [preflight] Add check for logger dependency 'logger' is required to facilitate logging to the journaling system. --- cfddns.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cfddns.sh b/cfddns.sh index e09f0b6..5589725 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -373,6 +373,10 @@ fi if [ ! -f "$accountFile" ] || [ ! -s "$accountFile" ]; then badParam errMsg "Cannot find Cloudflare credentials file (${accountFile}). Exiting." fi +if ! command -v logger >/dev/null 2>&1; then + printf "\n%sThis script requires 'logger' be installed to write entries to your journaling system. Exiting.%s\n\n" "$err" "$norm" + exit 2 +fi # turn off log file colourization if parameter is set if [ "$colourizeLogFile" -eq 0 ]; then bold="" From 71da487aa8e1a7aee0f23a5bb6ee8a5fdc827395 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Sun, 19 Jul 2026 23:33:56 -0600 Subject: [PATCH 04/60] style(fmt): Reformat shell script --- cfddns.sh | 831 +++++++++++++++++++++++++++--------------------------- 1 file changed, 420 insertions(+), 411 deletions(-) diff --git a/cfddns.sh b/cfddns.sh index 5589725..dc4ae72 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -9,248 +9,248 @@ ### text formatting presets using tput if command -v tput >/dev/null; then - [ -z "$TERM" ] && export TERM=xterm - bold=$(tput bold) - cyan=$(tput setaf 6) - err=$(tput bold)$(tput setaf 1) - magenta=$(tput setaf 5) - norm=$(tput sgr0) - ok=$(tput setaf 2) - warn=$(tput bold)$(tput setaf 3) - yellow=$(tput setaf 3) - width=$(tput cols) + [ -z "$TERM" ] && export TERM=xterm + bold=$(tput bold) + cyan=$(tput setaf 6) + err=$(tput bold)$(tput setaf 1) + magenta=$(tput setaf 5) + norm=$(tput sgr0) + ok=$(tput setaf 2) + warn=$(tput bold)$(tput setaf 3) + yellow=$(tput setaf 3) + width=$(tput cols) else - bold="" - cyan="" - err="" - magenta="" - norm="" - ok="" - warn="" - yellow="" - width=80 + bold="" + cyan="" + err="" + magenta="" + norm="" + ok="" + warn="" + yellow="" + width=80 fi ### functions badParam() { - if [ "$1" = "null" ]; then - printf "\n%sERROR: '%s' cannot have a NULL (empty) value.\n" "$err" "$2" - printf "%sPlease use '--help' for assistance.%s\n\n" "$cyan" "$norm" - exit 1 - elif [ "$1" = "dne" ]; then - printf "\n%sERROR: '%s %s'\n" "$err" "$2" "$3" - printf "file or directory does not exist or is empty.%s\n\n" "$norm" - exit 1 - elif [ "$1" = "errMsg" ]; then - printf "\n%sERROR: %s%s\n\n" "$err" "$2" "$norm" - exit 1 - fi + if [ "$1" = "null" ]; then + printf "\n%sERROR: '%s' cannot have a NULL (empty) value.\n" "$err" "$2" + printf "%sPlease use '--help' for assistance.%s\n\n" "$cyan" "$norm" + exit 1 + elif [ "$1" = "dne" ]; then + printf "\n%sERROR: '%s %s'\n" "$err" "$2" "$3" + printf "file or directory does not exist or is empty.%s\n\n" "$norm" + exit 1 + elif [ "$1" = "errMsg" ]; then + printf "\n%sERROR: %s%s\n\n" "$err" "$2" "$norm" + exit 1 + fi } exitError() { - case "$1" in - 3) - errMsg="Unable to connect to Cloudflare servers. This is probably a temporary networking issue. Please try again later." - ;; - 10) - errMsg="Unable to auto-detect IP address. Try again later or supply the IP address to be used." - ;; - 20) - errMsg="Cloudflare authorized email address (cfEmail) is either null or undefined. Please check your Cloudflare credentials file." - ;; - 21) - errMsg="Cloudflare authorized API key (cfKey) is either null or undefined. Please check your Cloudflare credentials file." - ;; - 22) - errMsg="Cloudflare zone id (cfZoneId) is either null or undefined. Please check your Cloudflare credentials file." - ;; - 25) - errMsg="Cloudflare API error. Please review any 'CF-ERR:' lines in this log for details." - ;; - 26) - errMsg="${failedHostCount} host update(s) failed. Any 'CF-ERR:' lines noted in this log may help determine what went wrong." - ;; - *) - writeLog error "An unspecified error occurred. (code: 99)" - printf "%s[%s] -- Cloudflare DDNS update-script: completed with error(s) --%s\n" "$err" "$(stamp)" "$norm" >>"$logFile" - exit 99 - ;; - esac - writeLog error "$errMsg" "$1" + case "$1" in + 3) + errMsg="Unable to connect to Cloudflare servers. This is probably a temporary networking issue. Please try again later." + ;; + 10) + errMsg="Unable to auto-detect IP address. Try again later or supply the IP address to be used." + ;; + 20) + errMsg="Cloudflare authorized email address (cfEmail) is either null or undefined. Please check your Cloudflare credentials file." + ;; + 21) + errMsg="Cloudflare authorized API key (cfKey) is either null or undefined. Please check your Cloudflare credentials file." + ;; + 22) + errMsg="Cloudflare zone id (cfZoneId) is either null or undefined. Please check your Cloudflare credentials file." + ;; + 25) + errMsg="Cloudflare API error. Please review any 'CF-ERR:' lines in this log for details." + ;; + 26) + errMsg="${failedHostCount} host update(s) failed. Any 'CF-ERR:' lines noted in this log may help determine what went wrong." + ;; + *) + writeLog error "An unspecified error occurred. (code: 99)" printf "%s[%s] -- Cloudflare DDNS update-script: completed with error(s) --%s\n" "$err" "$(stamp)" "$norm" >>"$logFile" - exit "$1" + exit 99 + ;; + esac + writeLog error "$errMsg" "$1" + printf "%s[%s] -- Cloudflare DDNS update-script: completed with error(s) --%s\n" "$err" "$(stamp)" "$norm" >>"$logFile" + exit "$1" } exitOK() { - printf "%s[%s] -- Cloudflare DDNS update-script: completed successfully --%s\n" "$ok" "$(stamp)" "$norm" >>"$logFile" - exit 0 + printf "%s[%s] -- Cloudflare DDNS update-script: completed successfully --%s\n" "$ok" "$(stamp)" "$norm" >>"$logFile" + exit 0 } listCFErrors() { - # extract error codes and messages in separate variables, replace newlines with underscores - codes="$(printf "%s" "$1" | jq -r '.errors | .[] | .code' | tr '\n' '_')" - messages="$(printf "%s" "$1" | jq -r '.errors | .[] | .message' | tr '\n' '_')" + # extract error codes and messages in separate variables, replace newlines with underscores + codes="$(printf "%s" "$1" | jq -r '.errors | .[] | .code' | tr '\n' '_')" + messages="$(printf "%s" "$1" | jq -r '.errors | .[] | .message' | tr '\n' '_')" - # iterate codes and messages and assemble into coherent messages in log - while [ -n "$codes" ] && [ -n "$messages" ]; do - # get first code and message in respective sets - code="${codes%%_*}" - message="${messages%%_*}" + # iterate codes and messages and assemble into coherent messages in log + while [ -n "$codes" ] && [ -n "$messages" ]; do + # get first code and message in respective sets + code="${codes%%_*}" + message="${messages%%_*}" - # update codes and messages sets by removing first item in each set - codes="${codes#*_}" - messages="${messages#*_}" + # update codes and messages sets by removing first item in each set + codes="${codes#*_}" + messages="${messages#*_}" - # output to log - writeLog cf "$message" "$code" - done + # output to log + writeLog cf "$message" "$code" + done } scriptExamples() { - newline - printf "Update Cloudflare DNS host A/AAAA records with current IP address.\n" - printf "%sUsage: %s --records host.domain.tld[,host2.domain.tld,...] [parameters]%s\n\n" "$bold" "$scriptName" "$norm" - textBlock "${magenta}--- usage examples ---${norm}" - newline - textBlockSwitches "${scriptName} -r myserver.mydomain.net" - textBlock "Update Cloudflare DNS records for myserver.mydomain.net with the auto-detected public IP4 address. Credentials will be expected in the default location and the log will be written in the default location also." - newline - textBlockSwitches "${scriptName} -r myserver.mydomain.net -6" - textBlock "Same as above, but update AAAA host records with the auto-detected public IP6 address." - newline - textBlockSwitches "${scriptName} -r myserver.mydomain.net,myserver2.mydomain.net -l /var/log/cfddns.log --nc" - textBlock "Update DNS entries for both listed hosts using auto-detected IP4 address. Write a non-coloured log to '/var/log/cfddns.log'." - newline - textBlockSwitches "${scriptName} -r myserver.mydomain.net,myserver2.mydomain.net -l /var/log/cfddns.log --ip6 --ip fd21:7a62:2737:9c3a::a151" - textBlock "Update DNS AAAA entries for listed hosts using the *specified* IP address. Write a colourful log to the location specified." - newline - textBlockSwitches "${scriptName} -r myserver.mydomain.net -c /root/cloudflare.creds -l /var/log/cfddns.log --ip 1.2.3.4" - textBlock "Update DNS A entry for listed hostname with the provided IP address. Read cloudflare credentials file from specified location, save log in specified location." - newline - textBlockSwitches "${scriptName} -r myserver.mydomain.net -c /root/cloudflare.creds -l /var/log/cfddns.log -6 -i fd21:7a62:2737:9c3a::a151" - textBlock "Exact same as above, but change the AAAA record. This is how you run the script once for IP4 and again for IP6." - exit 0 + newline + printf "Update Cloudflare DNS host A/AAAA records with current IP address.\n" + printf "%sUsage: %s --records host.domain.tld[,host2.domain.tld,...] [parameters]%s\n\n" "$bold" "$scriptName" "$norm" + textBlock "${magenta}--- usage examples ---${norm}" + newline + textBlockSwitches "${scriptName} -r myserver.mydomain.net" + textBlock "Update Cloudflare DNS records for myserver.mydomain.net with the auto-detected public IP4 address. Credentials will be expected in the default location and the log will be written in the default location also." + newline + textBlockSwitches "${scriptName} -r myserver.mydomain.net -6" + textBlock "Same as above, but update AAAA host records with the auto-detected public IP6 address." + newline + textBlockSwitches "${scriptName} -r myserver.mydomain.net,myserver2.mydomain.net -l /var/log/cfddns.log --nc" + textBlock "Update DNS entries for both listed hosts using auto-detected IP4 address. Write a non-coloured log to '/var/log/cfddns.log'." + newline + textBlockSwitches "${scriptName} -r myserver.mydomain.net,myserver2.mydomain.net -l /var/log/cfddns.log --ip6 --ip fd21:7a62:2737:9c3a::a151" + textBlock "Update DNS AAAA entries for listed hosts using the *specified* IP address. Write a colourful log to the location specified." + newline + textBlockSwitches "${scriptName} -r myserver.mydomain.net -c /root/cloudflare.creds -l /var/log/cfddns.log --ip 1.2.3.4" + textBlock "Update DNS A entry for listed hostname with the provided IP address. Read cloudflare credentials file from specified location, save log in specified location." + newline + textBlockSwitches "${scriptName} -r myserver.mydomain.net -c /root/cloudflare.creds -l /var/log/cfddns.log -6 -i fd21:7a62:2737:9c3a::a151" + textBlock "Exact same as above, but change the AAAA record. This is how you run the script once for IP4 and again for IP6." + exit 0 } scriptHelp() { - newline - printf "Update Cloudflare DNS host A/AAAA records with current IP address.\n" - printf "%sUsage: %s --records host.domain.tld[,host2.domain.tld,...] [parameters]%s\n\n" "$bold" "$scriptName" "$norm" - textBlock "The only required parameter is '--records' which is a comma-delimited list of hostnames to update. However, there are several other options which may be useful to implement." - textBlock "Parameters are listed below and followed by a description of their effect. If a default value exists, it will be listed on the following line in (parentheses)." - newline - textBlock "${magenta}--- script related parameters ---${norm}" - newline - textBlockSwitches "-c | --cred | --creds | --credentials | -f (deprecated, backward-compatibility)" - textBlock "Path to file containing your Cloudflare *token* credentials. Please refer to the repo README for more information on format, etc." - textBlockDefaults "(${accountFile})" - newline - textBlockSwitches "-l | --log" - textBlock "Path where the log file should be written." - textBlockDefaults "(${logFile})" - newline - textBlockSwitches "--nc | --no-color | --no-colour" - textBlock "Switch value. Disables ANSI colours in the log. Useful if you review the logs using a reader that does not parse ANSI colour codes." - textBlockDefaults "(disabled: print logs in colour)" - newline - textBlockSwitches "--log-console" - textBlock "Switch value. Output log to console (stdout) instead of a log file. Can be combined with --nc if desired." - textBlockDefaults "(disabled: output to log file)" - newline - textBlockSwitches "--log-journal" - textBlock "Switch value. Output a structured log entry to your journaling system instead of a log file. Implies --nc." - textBlockDefaults "(disabled: output to log file)" - newline - textBlockSwitches "--no-log" - textBlock "Switch value. Do not create a log (i.e. no console, no file). You will not have *any* output from the script if you choose this option, so you will not know if updates succeeded or failed." - textBlockDefaults "(disabled: output to log file)" - newline - textBlockSwitches "-h | --help | -?" - textBlock "Display this help screen." - newline - textBlockSwitches "--examples" - textBlock "Show some usage examples." - newline - textBlock "${magenta}--- DNS related parameters ---${norm}" - newline - textBlockSwitches "-r | --record | --records" - textBlock "Comma-delimited list of hostnames for which IP addresses should be updated in Cloudflare DNS. This parameter is REQUIRED. Note that this script will only *update* records, it will not create new ones. If you supply hostnames that are not already defined in DNS, the script will log a warning and will skip those hostnames." - newline - textBlockSwitches "-i | --ip | --ip-address | -a | --address" - textBlock "New IP address for DNS host records. If you omit this, the script will attempt to auto-detect your public IP address and use that." - newline - textBlockSwitches "-4 | --ip4 | --ipv4" - textBlock "Switch value. Update Host 'A' records (IP4) only. Note that this script can only update either A *or* AAAA records. If you need to update both, you'll have to run the script once in IP4 mode and again in IP6 mode. If you specify both this switch and the IP6 switch, the last one specified will take effect." - textBlockDefaults "(enabled: update A records)" - newline - textBlockSwitches "-6 | --ip6 | --ipv6" - textBlock "Switch value. Update Host 'AAAA' records (IP6) only. Note that this script can only update either A *or* AAAA records. If you need to update both, you'll have to run the script once in IP4 mode and again in IP6 mode. If you specify both this switch and the IP4 switch, the last one specified will take effect." - textBlockDefaults "(disabled: update A records)" - newline - textBlock "Please refer to the repo README for more detailed information regarding this script and how to automate and monitor it." - newline - exit 0 + newline + printf "Update Cloudflare DNS host A/AAAA records with current IP address.\n" + printf "%sUsage: %s --records host.domain.tld[,host2.domain.tld,...] [parameters]%s\n\n" "$bold" "$scriptName" "$norm" + textBlock "The only required parameter is '--records' which is a comma-delimited list of hostnames to update. However, there are several other options which may be useful to implement." + textBlock "Parameters are listed below and followed by a description of their effect. If a default value exists, it will be listed on the following line in (parentheses)." + newline + textBlock "${magenta}--- script related parameters ---${norm}" + newline + textBlockSwitches "-c | --cred | --creds | --credentials | -f (deprecated, backward-compatibility)" + textBlock "Path to file containing your Cloudflare *token* credentials. Please refer to the repo README for more information on format, etc." + textBlockDefaults "(${accountFile})" + newline + textBlockSwitches "-l | --log" + textBlock "Path where the log file should be written." + textBlockDefaults "(${logFile})" + newline + textBlockSwitches "--nc | --no-color | --no-colour" + textBlock "Switch value. Disables ANSI colours in the log. Useful if you review the logs using a reader that does not parse ANSI colour codes." + textBlockDefaults "(disabled: print logs in colour)" + newline + textBlockSwitches "--log-console" + textBlock "Switch value. Output log to console (stdout) instead of a log file. Can be combined with --nc if desired." + textBlockDefaults "(disabled: output to log file)" + newline + textBlockSwitches "--log-journal" + textBlock "Switch value. Output a structured log entry to your journaling system instead of a log file. Implies --nc." + textBlockDefaults "(disabled: output to log file)" + newline + textBlockSwitches "--no-log" + textBlock "Switch value. Do not create a log (i.e. no console, no file). You will not have *any* output from the script if you choose this option, so you will not know if updates succeeded or failed." + textBlockDefaults "(disabled: output to log file)" + newline + textBlockSwitches "-h | --help | -?" + textBlock "Display this help screen." + newline + textBlockSwitches "--examples" + textBlock "Show some usage examples." + newline + textBlock "${magenta}--- DNS related parameters ---${norm}" + newline + textBlockSwitches "-r | --record | --records" + textBlock "Comma-delimited list of hostnames for which IP addresses should be updated in Cloudflare DNS. This parameter is REQUIRED. Note that this script will only *update* records, it will not create new ones. If you supply hostnames that are not already defined in DNS, the script will log a warning and will skip those hostnames." + newline + textBlockSwitches "-i | --ip | --ip-address | -a | --address" + textBlock "New IP address for DNS host records. If you omit this, the script will attempt to auto-detect your public IP address and use that." + newline + textBlockSwitches "-4 | --ip4 | --ipv4" + textBlock "Switch value. Update Host 'A' records (IP4) only. Note that this script can only update either A *or* AAAA records. If you need to update both, you'll have to run the script once in IP4 mode and again in IP6 mode. If you specify both this switch and the IP6 switch, the last one specified will take effect." + textBlockDefaults "(enabled: update A records)" + newline + textBlockSwitches "-6 | --ip6 | --ipv6" + textBlock "Switch value. Update Host 'AAAA' records (IP6) only. Note that this script can only update either A *or* AAAA records. If you need to update both, you'll have to run the script once in IP4 mode and again in IP6 mode. If you specify both this switch and the IP4 switch, the last one specified will take effect." + textBlockDefaults "(disabled: update A records)" + newline + textBlock "Please refer to the repo README for more detailed information regarding this script and how to automate and monitor it." + newline + exit 0 } stamp() { - (date +%F" "%T) + (date +%F" "%T) } newline() { - printf "\n" + printf "\n" } textBlock() { - printf "%s\n" "$1" | fold -w "$width" -s + printf "%s\n" "$1" | fold -w "$width" -s } textBlockDefaults() { - printf "%s%s%s\n" "$yellow" "$1" "$norm" + printf "%s%s%s\n" "$yellow" "$1" "$norm" } textBlockSwitches() { - printf "%s%s%s\n" "$cyan" "$1" "$norm" + printf "%s%s%s\n" "$cyan" "$1" "$norm" } writeLog() { - case "$1" in - cf) - printf "[%s] CF-ERR: %s (code: %s)\n" "$(stamp)" "$2" "$3" >>"$logFile" - ;; - err) - printf "%s[%s] ERR: %s%s\n" "$err" "$(stamp)" "$2" "$norm" >>"$logFile" - ;; - error) - printf "%s[%s] ERROR: %s (code: %s)%s\n" "$err" "$(stamp)" "$2" "$3" "$norm" >>"$logFile" - ;; - process) - printf "%s[%s] %s... %s" "$cyan" "$(stamp)" "$2" "$norm" >>"$logFile" - ;; - process-done) - printf "%s%s%s\n" "$cyan" "$2" "$norm" >>"$logFile" - ;; - process-error) - printf "%sERROR%s\n" "$err" "$norm" >>"$logFile" - ;; - process-warning) - printf "%s%s%s\n" "$warn" "$2" "$norm" >>"$logFile" - ;; - stamped) - printf "[%s] %s\n" "$(stamp)" "$2" >>"$logFile" - ;; - success) - printf "%s[%s] SUCCESS: %s%s\n" "$ok" "$(stamp)" "$2" "$norm" >>"$logFile" - ;; - warn) - printf "%s[%s] WARN: %s%s\n" "$warn" "$(stamp)" "$2" "$norm" >>"$logFile" - ;; - warning) - printf "%s[%s] WARNING: %s%s\n" "$warn" "$(stamp)" "$2" "$norm" >>"$logFile" - ;; - *) - printf "%s\n" "$2" >>"$logFile" - ;; - esac + case "$1" in + cf) + printf "[%s] CF-ERR: %s (code: %s)\n" "$(stamp)" "$2" "$3" >>"$logFile" + ;; + err) + printf "%s[%s] ERR: %s%s\n" "$err" "$(stamp)" "$2" "$norm" >>"$logFile" + ;; + error) + printf "%s[%s] ERROR: %s (code: %s)%s\n" "$err" "$(stamp)" "$2" "$3" "$norm" >>"$logFile" + ;; + process) + printf "%s[%s] %s... %s" "$cyan" "$(stamp)" "$2" "$norm" >>"$logFile" + ;; + process-done) + printf "%s%s%s\n" "$cyan" "$2" "$norm" >>"$logFile" + ;; + process-error) + printf "%sERROR%s\n" "$err" "$norm" >>"$logFile" + ;; + process-warning) + printf "%s%s%s\n" "$warn" "$2" "$norm" >>"$logFile" + ;; + stamped) + printf "[%s] %s\n" "$(stamp)" "$2" >>"$logFile" + ;; + success) + printf "%s[%s] SUCCESS: %s%s\n" "$ok" "$(stamp)" "$2" "$norm" >>"$logFile" + ;; + warn) + printf "%s[%s] WARN: %s%s\n" "$warn" "$(stamp)" "$2" "$norm" >>"$logFile" + ;; + warning) + printf "%s[%s] WARNING: %s%s\n" "$warn" "$(stamp)" "$2" "$norm" >>"$logFile" + ;; + *) + printf "%s\n" "$2" >>"$logFile" + ;; + esac } ### default variable values @@ -272,284 +272,293 @@ failedHostCount=0 ### process startup parameters if [ -z "$1" ]; then - scriptHelp + scriptHelp fi while [ $# -gt 0 ]; do - case "$1" in - -h | -\? | --help) - # display help - scriptHelp - ;; - --examples) - # display sample commands - scriptExamples - ;; - -l | --log) - # set log file location - if [ -n "$2" ]; then - logFile="${2%/}" - shift - else - badParam null "$@" - fi - ;; - --log-console) - # log to the console instead of a file - logFile="/dev/stdout" - ;; - --log-journal) - # log to the journal system instead of a file - logToJournal=1 - ;; - --no-log) - # do not log anything - logFile="/dev/null" - ;; - --nc | --no-color | --no-colour) - # do not colourize log file - colourizeLogFile=0 - ;; - -c | --cred* | -f) - # path to Cloudflare credentials file - if [ -n "$2" ]; then - if [ -f "$2" ] && [ -s "$2" ]; then - accountFile="${2%/}" - shift - else - badParam dne "$@" - fi - else - badParam null "$@" - fi - ;; - -r | --record | --records) - # DNS records to update - if [ -n "$2" ]; then - dnsRecords=$(printf "%s" "$2" | sed -e 's/ //g') - shift - else - badParam null "$@" - fi - ;; - -i | --ip | --ip-address | -a | --address) - # IP address to use (not parsed for correctness) - if [ -n "$2" ]; then - ipAddress="$2" - shift - else - badParam null "$@" - fi - ;; - -4 | --ip4 | --ipv4) - # operate in IP4 mode (default) - ip4=1 - ip6=0 - ;; - -6 | --ip6 | --ipv6) - # operate in IP6 mode - ip6=1 - ip4=0 - ;; - *) - printf "\n%sUnknown option: %s\n" "$err" "$1" - printf "%sUse '--help' for valid options.%s\n\n" "$cyan" "$norm" - exit 1 - ;; - esac - shift + case "$1" in + -h | -\? | --help) + # display help + scriptHelp + ;; + --examples) + # display sample commands + scriptExamples + ;; + -l | --log) + # set log file location + if [ -n "$2" ]; then + logFile="${2%/}" + shift + else + badParam null "$@" + fi + ;; + --log-console) + # log to the console instead of a file + logFile="/dev/stdout" + ;; + --log-journal) + # log to the journal system instead of a file + logToJournal=1 + ;; + --no-log) + # do not log anything + logFile="/dev/null" + ;; + --nc | --no-color | --no-colour) + # do not colourize log file + colourizeLogFile=0 + ;; + -c | --cred* | -f) + # path to Cloudflare credentials file + if [ -n "$2" ]; then + if [ -f "$2" ] && [ -s "$2" ]; then + accountFile="${2%/}" + shift + else + badParam dne "$@" + fi + else + badParam null "$@" + fi + ;; + -r | --record | --records) + # DNS records to update + if [ -n "$2" ]; then + dnsRecords=$(printf "%s" "$2" | sed -e 's/ //g') + shift + else + badParam null "$@" + fi + ;; + -i | --ip | --ip-address | -a | --address) + # IP address to use (not parsed for correctness) + if [ -n "$2" ]; then + ipAddress="$2" + shift + else + badParam null "$@" + fi + ;; + -4 | --ip4 | --ipv4) + # operate in IP4 mode (default) + ip4=1 + ip6=0 + ;; + -6 | --ip6 | --ipv6) + # operate in IP6 mode + ip6=1 + ip4=0 + ;; + *) + printf "\n%sUnknown option: %s\n" "$err" "$1" + printf "%sUse '--help' for valid options.%s\n\n" "$cyan" "$norm" + exit 1 + ;; + esac + shift done ### pre-flight checks if ! command -v curl >/dev/null; then - printf "\n%sThis script requires 'curl' be installed and accessible. Exiting.%s\n\n" "$err" "$norm" - exit 2 + printf "\n%sThis script requires 'curl' be installed and accessible. Exiting.%s\n\n" "$err" "$norm" + exit 2 fi if ! command -v jq >/dev/null; then - printf "\n%sThis script requires 'jq' be installed and accessible. Exiting.%s\n\n" "$err" "$norm" - exit 2 + printf "\n%sThis script requires 'jq' be installed and accessible. Exiting.%s\n\n" "$err" "$norm" + exit 2 fi [ -z "$dnsRecords" ] && badParam errMsg "You must specify at least one DNS record to update. Exiting." # verify credentials file exists and is not empty (default check) if [ ! -f "$accountFile" ] || [ ! -s "$accountFile" ]; then - badParam errMsg "Cannot find Cloudflare credentials file (${accountFile}). Exiting." + badParam errMsg "Cannot find Cloudflare credentials file (${accountFile}). Exiting." fi if ! command -v logger >/dev/null 2>&1; then - printf "\n%sThis script requires 'logger' be installed to write entries to your journaling system. Exiting.%s\n\n" "$err" "$norm" - exit 2 + printf "\n%sThis script requires 'logger' be installed to write entries to your journaling system. Exiting.%s\n\n" "$err" "$norm" + exit 2 fi # turn off log file colourization if parameter is set if [ "$colourizeLogFile" -eq 0 ]; then - bold="" - cyan="" - err="" - magenta="" - norm="" - ok="" - warn="" - yellow="" + bold="" + cyan="" + err="" + magenta="" + norm="" + ok="" + warn="" + yellow="" fi ### initial log entries { - printf "%s[%s] -- Cloudflare DDNS update-script: starting --%s\n" "$ok" "$(stamp)" "$norm" - printf "Parameters:\n" - printf "script path: %s\n" "$scriptPath/$scriptName" - printf "credentials file: %s\n" "$accountFile" + printf "%s[%s] -- Cloudflare DDNS update-script: starting --%s\n" "$ok" "$(stamp)" "$norm" + printf "Parameters:\n" + printf "script path: %s\n" "$scriptPath/$scriptName" + printf "credentials file: %s\n" "$accountFile" + + if [ "$ip4" -eq 1 ]; then + printf "mode: IP4\n" + elif [ "$ip6" -eq 1 ]; then + printf "mode: IP6\n" + fi + + # detect and report IP address + if [ -z "$ipAddress" ]; then + # detect public ip address if [ "$ip4" -eq 1 ]; then - printf "mode: IP4\n" - elif [ "$ip6" -eq 1 ]; then - printf "mode: IP6\n" + if ! ipAddress="$(curl -s $ip4DetectionSvc)"; then + printf "ddns ip address:%s ERROR%s\n" "$err" "$norm" + exitError 10 + fi fi - - # detect and report IP address - if [ -z "$ipAddress" ]; then - # detect public ip address - if [ "$ip4" -eq 1 ]; then - if ! ipAddress="$(curl -s $ip4DetectionSvc)"; then - printf "ddns ip address:%s ERROR%s\n" "$err" "$norm" - exitError 10 - fi - fi - if [ "$ip6" -eq 1 ]; then - if ! ipAddress="$(curl -s $ip6DetectionSvc)"; then - printf "ddns ip address:%s ERROR%s\n" "$err" "$norm" - exitError 10 - fi - fi - printf "ddns ip address (detected): %s\n" "$ipAddress" - else - printf "ddns ip address (supplied): %s\n" "$ipAddress" + if [ "$ip6" -eq 1 ]; then + if ! ipAddress="$(curl -s $ip6DetectionSvc)"; then + printf "ddns ip address:%s ERROR%s\n" "$err" "$norm" + exitError 10 + fi fi + printf "ddns ip address (detected): %s\n" "$ipAddress" + else + printf "ddns ip address (supplied): %s\n" "$ipAddress" + fi - # iterate DNS records to update - dnsRecordsToUpdate="$(printf '%s' "${dnsRecords}" | sed "s/${dnsSeparator}*$//")$dnsSeparator" - while [ -n "$dnsRecordsToUpdate" ] && [ "$dnsRecordsToUpdate" != "$dnsSeparator" ]; do - record="${dnsRecordsToUpdate%%${dnsSeparator}*}" - dnsRecordsToUpdate="${dnsRecordsToUpdate#*${dnsSeparator}}" + # iterate DNS records to update + dnsRecordsToUpdate="$(printf '%s' "${dnsRecords}" | sed "s/${dnsSeparator}*$//")$dnsSeparator" + while [ -n "$dnsRecordsToUpdate" ] && [ "$dnsRecordsToUpdate" != "$dnsSeparator" ]; do + record="${dnsRecordsToUpdate%%${dnsSeparator}*}" + dnsRecordsToUpdate="${dnsRecordsToUpdate#*${dnsSeparator}}" - if [ -z "$record" ]; then continue; fi - printf "updating record: %s\n" "$record" - done + if [ -z "$record" ]; then + continue + fi + printf "updating record: %s\n" "$record" + done - printf "(end of parameter list)\n" + printf "(end of parameter list)\n" } >>"$logFile" ### read Cloudflare credentials writeLog process "Reading Cloudflare credentials" case "$accountFile" in /*) - # absolute path, use as-is - # shellcheck source=./cloudflare.credentials - . "$accountFile" - ;; +# absolute path, use as-is +# shellcheck source=./cloudflare.credentials + . "$accountFile" + ;; *) - # relative path, rewrite - # shellcheck source=./cloudflare.credentials - . "./$accountFile" - ;; +# relative path, rewrite +# shellcheck source=./cloudflare.credentials + . "./$accountFile" + ;; esac if [ -z "$cfKey" ]; then - writeLog process-error - exitError 21 + writeLog process-error + exitError 21 fi if [ -z "$cfZoneId" ]; then - writeLog process-error - exitError 22 + writeLog process-error + exitError 22 fi writeLog process-done "DONE" ### connect to Cloudflare and do what needs to be done! dnsRecordsToUpdate="$dnsRecords$dnsSeparator" if [ "$ip4" -eq 1 ]; then - recordType="A" + recordType="A" elif [ "$ip6" -eq 1 ]; then - recordType="AAAA" + recordType="AAAA" fi # iterate hosts to update while [ -n "$dnsRecordsToUpdate" ] && [ "$dnsRecordsToUpdate" != "$dnsSeparator" ]; do - record="${dnsRecordsToUpdate%%${dnsSeparator}*}" - dnsRecordsToUpdate="${dnsRecordsToUpdate#*${dnsSeparator}}" + record="${dnsRecordsToUpdate%%${dnsSeparator}*}" + dnsRecordsToUpdate="${dnsRecordsToUpdate#*${dnsSeparator}}" - if [ -z "$record" ]; then continue; fi - writeLog process "Processing ${record}" + if [ -z "$record" ]; then + continue + fi + writeLog process "Processing ${record}" - # exit if curl/network error - if ! cfLookup="$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${cfZoneId}/dns_records?name=${record}&type=${recordType}" \ - -H "Authorization: Bearer ${cfKey}" \ - -H "Content-Type: application/json")"; then - writeLog process-error - exitError 3 - fi + # exit if curl/network error + if ! cfLookup="$( + curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${cfZoneId}/dns_records?name=${record}&type=${recordType}" \ + -H "Authorization: Bearer ${cfKey}" \ + -H "Content-Type: application/json" + )"; then + writeLog process-error + exitError 3 + fi - # exit if API error - # exit here since API errors on GET request probably indicates authentication error which would affect all remaining operations - # no reason to continue processing other hosts and pile-up errors which might look like a DoS attempt - cfSuccess="$(printf "%s" "$cfLookup" | jq -r '.success')" - if [ "$cfSuccess" = "false" ]; then - writeLog process-error - listCFErrors "$cfLookup" - exitError 25 - fi + # exit if API error + # exit here since API errors on GET request probably indicates authentication error which would affect all remaining operations + # no reason to continue processing other hosts and pile-up errors which might look like a DoS attempt + cfSuccess="$(printf "%s" "$cfLookup" | jq -r '.success')" + if [ "$cfSuccess" = "false" ]; then + writeLog process-error + listCFErrors "$cfLookup" + exitError 25 + fi - resultCount="$(printf "%s" "$cfLookup" | jq '.result_info.count')" + resultCount="$(printf "%s" "$cfLookup" | jq '.result_info.count')" - # skip to next host if cannot find existing host record (this script *updates* only, does not create!) - if [ "$resultCount" = "0" ]; then - # warn if record of host not found - writeLog process-warning "NOT FOUND" - writeLog warn "Cannot find existing record to update for DNS entry: ${record}" - invalidDomainCount=$((invalidDomainCount + 1)) - continue - fi + # skip to next host if cannot find existing host record (this script *updates* only, does not create!) + if [ "$resultCount" = "0" ]; then + # warn if record of host not found + writeLog process-warning "NOT FOUND" + writeLog warn "Cannot find existing record to update for DNS entry: ${record}" + invalidDomainCount=$((invalidDomainCount + 1)) + continue + fi - objectId=$(printf "%s" "$cfLookup" | jq -r '.result | .[] | .id') - currentIpAddr=$(printf "%s" "$cfLookup" | jq -r '.result | .[] | .content') - writeLog process-done "FOUND: IP = ${currentIpAddr}" + objectId=$(printf "%s" "$cfLookup" | jq -r '.result | .[] | .id') + currentIpAddr=$(printf "%s" "$cfLookup" | jq -r '.result | .[] | .content') + writeLog process-done "FOUND: IP = ${currentIpAddr}" - # skip to next hostname if record already up-to-date - if [ "$currentIpAddr" = "$ipAddress" ]; then - writeLog stamped "IP address for ${record} is already up-to-date" - continue - fi + # skip to next hostname if record already up-to-date + if [ "$currentIpAddr" = "$ipAddress" ]; then + writeLog stamped "IP address for ${record} is already up-to-date" + continue + fi - # update record - writeLog process "Updating IP address for ${record}" - updateJSON="$(jq -n --arg key0 content --arg value0 "${ipAddress}" '{($key0):$value0}')" + # update record + writeLog process "Updating IP address for ${record}" + updateJSON="$(jq -n --arg key0 content --arg value0 "${ipAddress}" '{($key0):$value0}')" - # exit if curl/network error - if ! cfResult="$(curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/${cfZoneId}/dns_records/${objectId}" \ - -H "Authorization: Bearer ${cfKey}" \ - -H "Content-Type: application/json" \ - --data "${updateJSON}")"; then - writeLog process-error - exitError 3 - fi + # exit if curl/network error + if ! cfResult="$( + curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/${cfZoneId}/dns_records/${objectId}" \ + -H "Authorization: Bearer ${cfKey}" \ + -H "Content-Type: application/json" \ + --data "${updateJSON}" + )"; then + writeLog process-error + exitError 3 + fi - # note update success or failure - cfSuccess="$(printf "%s" "$cfResult" | jq '.success')" - if [ "$cfSuccess" = "true" ]; then - writeLog process-done "DONE" - writeLog success "IP address for ${record} updated." - else - writeLog process-error - listCFErrors "$cfResult" - writeLog err "Unable to update IP address for ${record}" - # do not exit with error, API error here is probably an update issue specific to this host - # increment counter and note it after all processing finished - failedHostCount=$((failedHostCount + 1)) - fi + # note update success or failure + cfSuccess="$(printf "%s" "$cfResult" | jq '.success')" + if [ "$cfSuccess" = "true" ]; then + writeLog process-done "DONE" + writeLog success "IP address for ${record} updated." + else + writeLog process-error + listCFErrors "$cfResult" + writeLog err "Unable to update IP address for ${record}" + # do not exit with error, API error here is probably an update issue specific to this host + # increment counter and note it after all processing finished + failedHostCount=$((failedHostCount + 1)) + fi done # exit if [ "$invalidDomainCount" -ne 0 ]; then - writeLog warning "${invalidDomainCount} invalid host(s) supplied for updating." + writeLog warning "${invalidDomainCount} invalid host(s) supplied for updating." fi if [ "$failedHostCount" -ne 0 ]; then - exitError 26 + exitError 26 else - exitOK + exitOK fi ### exit return codes From 380509c966884e935948dba413231676c4dc9171 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Sun, 19 Jul 2026 23:36:58 -0600 Subject: [PATCH 05/60] chore(git): Update git control files --- .gitattributes | 25 ++++++++++++++++++++++--- .gitignore | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/.gitattributes b/.gitattributes index 810f474..c6aa9df 100644 --- a/.gitattributes +++ b/.gitattributes @@ -55,6 +55,23 @@ *.bat text eol=crlf *.cmd text eol=crlf +# web frontend stack -- force LF so SRI hashes are always correct +*.html text eol=lf +*.htm text eol=lf +*.css text eol=lf +*.min.css text eol=lf +*.js text eol=lf +*.min.js text eol=lf +*.php text eol=lf + +# Visual Studio projects (Rider also) +*.cs diff=csharp +*.sln merge=union +*.csproj merge=union +*.vbproj merge=union +*.fsproj merge=union +*.dbproj merge=union + # Serialisation *.json text *.toml text @@ -76,6 +93,8 @@ # Exclude files from exporting # -.gitattributes export-ignore -.gitignore export-ignore -.gitkeep export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.gitkeep export-ignore +.idea export-ignore +.vscode export-ignore diff --git a/.gitignore b/.gitignore index 397b4a7..7150b25 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,35 @@ +# +# JetBrains exclusions +# + +riderModule.iml +/_ReSharper.Caches/ + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml +.idea/**/GitCommitMessageStorage.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# modules +.idea_modules/ + +# Editor-based Rest Client +.idea/httpRequests + +# project-specific exclusions *.log From 1e3b545a239f10fdfe2cdd12731bb387e851b4c7 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Sun, 19 Jul 2026 23:37:13 -0600 Subject: [PATCH 06/60] chore(ide): JetBrains IDE configuration files --- .idea/git_toolbox_prj.xml | 15 +++++++++++++++ .idea/indexLayout.xml | 8 ++++++++ .idea/projectSettingsUpdater.xml | 8 ++++++++ .idea/vcs.xml | 6 ++++++ 4 files changed, 37 insertions(+) create mode 100644 .idea/git_toolbox_prj.xml create mode 100644 .idea/indexLayout.xml create mode 100644 .idea/projectSettingsUpdater.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/git_toolbox_prj.xml b/.idea/git_toolbox_prj.xml new file mode 100644 index 0000000..02b915b --- /dev/null +++ b/.idea/git_toolbox_prj.xml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/indexLayout.xml b/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/projectSettingsUpdater.xml b/.idea/projectSettingsUpdater.xml new file mode 100644 index 0000000..ef20cb0 --- /dev/null +++ b/.idea/projectSettingsUpdater.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file From 50b4cf105fd7add36eead5a11a1b6c0fd6a4e082 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Wed, 22 Jul 2026 17:33:36 -0600 Subject: [PATCH 07/60] chore(ide): Update IDE settings --- .idea/developer-tools.xml | 6 ++++++ .idea/misc.xml | 8 ++++++++ 2 files changed, 14 insertions(+) create mode 100644 .idea/developer-tools.xml create mode 100644 .idea/misc.xml diff --git a/.idea/developer-tools.xml b/.idea/developer-tools.xml new file mode 100644 index 0000000..1fe627e --- /dev/null +++ b/.idea/developer-tools.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..0bfb71a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file From 1824d9f13903e9bc5d25599f40d1212dcd82d0b8 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Wed, 22 Jul 2026 17:33:46 -0600 Subject: [PATCH 08/60] feat(logger): Framework test file --- logger.sh | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 logger.sh diff --git a/logger.sh b/logger.sh new file mode 100644 index 0000000..ec8ef0a --- /dev/null +++ b/logger.sh @@ -0,0 +1,126 @@ +#!/bin/sh + +getTimeStamp() { + (date +%F" "%T) +} + +# $1: message, $2: level, $3: operation, $4: code +writeJsonLog() { + PROGRAM_NAME="cfddns" + DEFAULT_CODE=99 + DEFAULT_LEVEL="info" + DEFAULT_MESSAGE="An unspecified error has occurred" + DEFAULT_OPERATION="unspecified" + + if [ -z "$1" ]; then + PROPS="$( + jq --null-input \ + --arg level "err" \ + --arg pName "$PROGRAM_NAME" \ + --arg operation "$DEFAULT_OPERATION" \ + --arg code "$DEFAULT_CODE" \ + --arg message "$DEFAULT_MESSAGE" \ + '{"level":$level, "programName":$pName, "operation":$operation, "code":$code, "message":$message}' \ + )" + else + PROPS="$( + jq --null-input \ + --arg level "${2:-$DEFAULT_LEVEL}" \ + --arg pName "$PROGRAM_NAME" \ + --arg operation "${3:-$DEFAULT_OPERATION}" \ + --arg code "${4:-$DEFAULT_CODE}" \ + --arg message "$1" \ + '{"level":$level, "programName":$pName, "operation":$operation, "code":$code, "message":$message}' \ + )" + fi + + echo "$PROPS" | jq "." + return +} + +# $1: message, $2: level, $3: operation, $4: code +writeJournalLog() { + PROGRAM_NAME="cfddns" + DEFAULT_CODE=99 + DEFAULT_LEVEL="info" + DEFAULT_MESSAGE="An unspecified error has occurred" + DEFAULT_OPERATION="unspecified" + + # translate error levels + if [ -z "$2" ]; then + LOG_LEVEL=1 + else + case "$2" in + "emergency" | "emerg") + LOG_LEVEL=7 + ;; + "alert") + LOG_LEVEL=6 + ;; + "critical" | "crit") + LOG_LEVEL=5 + ;; + "error" | "err") + LOG_LEVEL=4 + ;; + "warning" | "warn") + LOG_LEVEL=3 + ;; + "notice") + LOG_LEVEL=2 + ;; + "info") + LOG_LEVEL=1 + ;; + "debug") + LOG_LEVEL=0 + ;; + esac + fi + + # log the message + if [ -z "$1" ]; then + logger --journald < Date: Wed, 22 Jul 2026 18:34:14 -0600 Subject: [PATCH 09/60] feature [logger] add status logging param --- logger.sh | 48 +++++++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/logger.sh b/logger.sh index ec8ef0a..656db44 100644 --- a/logger.sh +++ b/logger.sh @@ -4,13 +4,18 @@ getTimeStamp() { (date +%F" "%T) } -# $1: message, $2: level, $3: operation, $4: code +uppercase() { + echo "$1" | tr '[:lower:] [:upper:]' +} + +# $1: message, $2: level, $3: status, $4: operation, $5: code writeJsonLog() { PROGRAM_NAME="cfddns" DEFAULT_CODE=99 DEFAULT_LEVEL="info" DEFAULT_MESSAGE="An unspecified error has occurred" DEFAULT_OPERATION="unspecified" + DEFAULT_STATUS="ERR" if [ -z "$1" ]; then PROPS="$( @@ -18,19 +23,21 @@ writeJsonLog() { --arg level "err" \ --arg pName "$PROGRAM_NAME" \ --arg operation "$DEFAULT_OPERATION" \ + --arg status "$DEFAULT_STATUS" \ --arg code "$DEFAULT_CODE" \ --arg message "$DEFAULT_MESSAGE" \ - '{"level":$level, "programName":$pName, "operation":$operation, "code":$code, "message":$message}' \ + '{"level":$level, "programName":$pName, "status":$status, "operation":$operation, "code":$code, "message":$message}' \ )" else PROPS="$( jq --null-input \ --arg level "${2:-$DEFAULT_LEVEL}" \ --arg pName "$PROGRAM_NAME" \ - --arg operation "${3:-$DEFAULT_OPERATION}" \ - --arg code "${4:-$DEFAULT_CODE}" \ + --arg status "$(uppercase "${3:-$DEFAULT_STATUS}")" \ + --arg operation "${4:-$DEFAULT_OPERATION}" \ + --arg code "${5:-$DEFAULT_CODE}" \ --arg message "$1" \ - '{"level":$level, "programName":$pName, "operation":$operation, "code":$code, "message":$message}' \ + '{"level":$level, "programName":$pName, "status":$status, "operation":$operation, "code":$code, "message":$message}' \ )" fi @@ -38,13 +45,14 @@ writeJsonLog() { return } -# $1: message, $2: level, $3: operation, $4: code +# $1: message, $2: level, $3: status, $4: operation, $5: code writeJournalLog() { PROGRAM_NAME="cfddns" DEFAULT_CODE=99 DEFAULT_LEVEL="info" DEFAULT_MESSAGE="An unspecified error has occurred" DEFAULT_OPERATION="unspecified" + DEFAULT_STATUS="ERR" # translate error levels if [ -z "$2" ]; then @@ -81,25 +89,27 @@ writeJournalLog() { # log the message if [ -z "$1" ]; then logger --journald < Date: Wed, 22 Jul 2026 21:52:53 -0600 Subject: [PATCH 10/60] refactor(logger): Use ISO8601 timestamp --- logger.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/logger.sh b/logger.sh index 656db44..2a0c241 100644 --- a/logger.sh +++ b/logger.sh @@ -1,11 +1,11 @@ #!/bin/sh getTimeStamp() { - (date +%F" "%T) + (date -u +"%Y-%m-%dT%H:%M:%SZ") } uppercase() { - echo "$1" | tr '[:lower:] [:upper:]' + echo "$1" | tr '[:lower:] [:upper:]' } # $1: message, $2: level, $3: status, $4: operation, $5: code From 37928815295b004995d8f694815a10b218d4a14c Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Wed, 22 Jul 2026 21:58:14 -0600 Subject: [PATCH 11/60] feature(logger): Add plain-text logger function --- logger.sh | 77 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/logger.sh b/logger.sh index 2a0c241..9198790 100644 --- a/logger.sh +++ b/logger.sh @@ -4,38 +4,47 @@ getTimeStamp() { (date -u +"%Y-%m-%dT%H:%M:%SZ") } +getHostname() { + (hostname -s) +} + uppercase() { echo "$1" | tr '[:lower:] [:upper:]' } +# modified syslog fmt: [pid]: [LEVEL:$2] [STATUS:$3] (:) +writePlainTextLog() { + # if no message is supplied, there is no purpose in generating a log message! + if [ -z "$1" ]; then + return + fi + + printf "%s %s %s[%s]: [%s] [%s] %s (%s:%s)\n" \ + "$(getTimeStamp)" "$(getHostname)" "$LOG_PROGRAM_NAME" "$$" \ + "$2" "$3" "$1" "${4:UNSPECIFIED}" "${5:999}" +} + # $1: message, $2: level, $3: status, $4: operation, $5: code writeJsonLog() { - PROGRAM_NAME="cfddns" - DEFAULT_CODE=99 - DEFAULT_LEVEL="info" - DEFAULT_MESSAGE="An unspecified error has occurred" - DEFAULT_OPERATION="unspecified" - DEFAULT_STATUS="ERR" - if [ -z "$1" ]; then PROPS="$( jq --null-input \ --arg level "err" \ - --arg pName "$PROGRAM_NAME" \ - --arg operation "$DEFAULT_OPERATION" \ - --arg status "$DEFAULT_STATUS" \ - --arg code "$DEFAULT_CODE" \ - --arg message "$DEFAULT_MESSAGE" \ + --arg pName "$LOG_PROGRAM_NAME" \ + --arg operation "$LOG_DEFAULT_OPERATION" \ + --arg status "$LOG_DEFAULT_STATUS" \ + --arg code "$LOG_DEFAULT_CODE" \ + --arg message "$LOG_DEFAULT_MESSAGE" \ '{"level":$level, "programName":$pName, "status":$status, "operation":$operation, "code":$code, "message":$message}' \ )" else PROPS="$( jq --null-input \ - --arg level "${2:-$DEFAULT_LEVEL}" \ - --arg pName "$PROGRAM_NAME" \ - --arg status "$(uppercase "${3:-$DEFAULT_STATUS}")" \ - --arg operation "${4:-$DEFAULT_OPERATION}" \ - --arg code "${5:-$DEFAULT_CODE}" \ + --arg level "${2:-$LOG_DEFAULT_LEVEL}" \ + --arg pName "$LOG_PROGRAM_NAME" \ + --arg status "$(uppercase "${3:-$LOG_DEFAULT_STATUS}")" \ + --arg operation "${4:-$LOG_DEFAULT_OPERATION}" \ + --arg code "${5:-$LOG_DEFAULT_CODE}" \ --arg message "$1" \ '{"level":$level, "programName":$pName, "status":$status, "operation":$operation, "code":$code, "message":$message}' \ )" @@ -45,15 +54,8 @@ writeJsonLog() { return } -# $1: message, $2: level, $3: status, $4: operation, $5: code +# $1: message, $2: level, $3: status, $4: operation, $5: code writeJournalLog() { - PROGRAM_NAME="cfddns" - DEFAULT_CODE=99 - DEFAULT_LEVEL="info" - DEFAULT_MESSAGE="An unspecified error has occurred" - DEFAULT_OPERATION="unspecified" - DEFAULT_STATUS="ERR" - # translate error levels if [ -z "$2" ]; then LOG_LEVEL=1 @@ -92,30 +94,37 @@ writeJournalLog() { MESSAGE_ID="$(systemd-id128 new)" SYSLOG_TIMESTAMP="$(getTimeStamp)" SYSLOG_FACILITY=3 -SYSLOG_IDENTIFIER="${PROGRAM_NAME}" +SYSLOG_IDENTIFIER="${LOG_PROGRAM_NAME}" PRIORITY=4 -ERRNO="${DEFAULT_CODE}" -OPERATION="${DEFAULT_OPERATION}" -MESSAGE="${DEFAULT_MESSAGE}" -STATUS="${DEFAULT_STATUS}" +ERRNO="${LOG_DEFAULT_CODE}" +OPERATION="${LOG_DEFAULT_OPERATION}" +MESSAGE="${LOG_DEFAULT_MESSAGE}" +STATUS="${LOG_DEFAULT_STATUS}" end else logger --journald < Date: Wed, 22 Jul 2026 22:49:13 -0600 Subject: [PATCH 12/60] feat(logger): Handle plain-text insufficient information Return without logging if message, level and/or status is not supplied. --- logger.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/logger.sh b/logger.sh index 9198790..853cf7c 100644 --- a/logger.sh +++ b/logger.sh @@ -14,8 +14,8 @@ uppercase() { # modified syslog fmt: [pid]: [LEVEL:$2] [STATUS:$3] (:) writePlainTextLog() { - # if no message is supplied, there is no purpose in generating a log message! - if [ -z "$1" ]; then + # not enough information to generate a meaningful log message + if [ -z "$1" ] || [ $# -lt 3 ]; then return fi From 5b55ac95a39d34d2b8f8a0d760cdb0cf7b6d3674 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Wed, 22 Jul 2026 22:53:38 -0600 Subject: [PATCH 13/60] feat(logger): Update JSON logger Require message, level, and status. Reorganize JSON output to mirror modified syslog-style structure. --- logger.sh | 42 +++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/logger.sh b/logger.sh index 853cf7c..129cf43 100644 --- a/logger.sh +++ b/logger.sh @@ -24,30 +24,26 @@ writePlainTextLog() { "$2" "$3" "$1" "${4:UNSPECIFIED}" "${5:999}" } -# $1: message, $2: level, $3: status, $4: operation, $5: code +# $1: message, $2: level, $3: status, [$4: operation], [$5: code] writeJsonLog() { - if [ -z "$1" ]; then - PROPS="$( - jq --null-input \ - --arg level "err" \ - --arg pName "$LOG_PROGRAM_NAME" \ - --arg operation "$LOG_DEFAULT_OPERATION" \ - --arg status "$LOG_DEFAULT_STATUS" \ - --arg code "$LOG_DEFAULT_CODE" \ - --arg message "$LOG_DEFAULT_MESSAGE" \ - '{"level":$level, "programName":$pName, "status":$status, "operation":$operation, "code":$code, "message":$message}' \ - )" - else - PROPS="$( - jq --null-input \ - --arg level "${2:-$LOG_DEFAULT_LEVEL}" \ - --arg pName "$LOG_PROGRAM_NAME" \ - --arg status "$(uppercase "${3:-$LOG_DEFAULT_STATUS}")" \ - --arg operation "${4:-$LOG_DEFAULT_OPERATION}" \ - --arg code "${5:-$LOG_DEFAULT_CODE}" \ - --arg message "$1" \ - '{"level":$level, "programName":$pName, "status":$status, "operation":$operation, "code":$code, "message":$message}' \ - )" + # not enough information to generate a meaningful log message + if [ -z "$1" ] || [ $# -lt 3 ]; then + return + fi + + PROPS="$( + jq --null-input \ + --arg timestamp "$(getTimeStamp)" \ + --arg hostname "$(getHostname)" \ + --arg pName "$LOG_PROGRAM_NAME" \ + --arg pid = "$$" \ + --arg level "$2" \ + --arg status "$(uppercase "$3")" \ + --arg message "$1" \ + --arg operation "${4:UNSPECIFIED}" \ + --arg code "${5:999}" \ + '{"level":$level, "programName":$pName, "status":$status, "operation":$operation, "code":$code, "message":$message}' \ + )" fi echo "$PROPS" | jq "." From 17c33d237faa9ee30b3243744f75429d415881e3 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Wed, 22 Jul 2026 22:54:25 -0600 Subject: [PATCH 14/60] fix(logger): Ensure plain-text status is uppercase --- logger.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logger.sh b/logger.sh index 129cf43..9f35c97 100644 --- a/logger.sh +++ b/logger.sh @@ -21,7 +21,7 @@ writePlainTextLog() { printf "%s %s %s[%s]: [%s] [%s] %s (%s:%s)\n" \ "$(getTimeStamp)" "$(getHostname)" "$LOG_PROGRAM_NAME" "$$" \ - "$2" "$3" "$1" "${4:UNSPECIFIED}" "${5:999}" + "$2" "$(uppercase "$3")" "$1" "${4:UNSPECIFIED}" "${5:999}" } # $1: message, $2: level, $3: status, [$4: operation], [$5: code] From 0e787be45615af9261d00e1f9a4b92557314ee2e Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Wed, 22 Jul 2026 23:06:34 -0600 Subject: [PATCH 15/60] feat(logger): Add lowercase function --- logger.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/logger.sh b/logger.sh index 9f35c97..ec31859 100644 --- a/logger.sh +++ b/logger.sh @@ -8,6 +8,10 @@ getHostname() { (hostname -s) } +lowercase() { + echo "$1" | tr '[:upper:]' '[:lower:]' +} + uppercase() { echo "$1" | tr '[:lower:] [:upper:]' } From e411a35b93dce269a14259d2a216fabc785e60bf Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Wed, 22 Jul 2026 23:13:58 -0600 Subject: [PATCH 16/60] feat(logger): Add log level translator functions --- logger.sh | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/logger.sh b/logger.sh index ec31859..24b6619 100644 --- a/logger.sh +++ b/logger.sh @@ -16,6 +16,53 @@ uppercase() { echo "$1" | tr '[:lower:] [:upper:]' } +standardizeLogLevels() { + LEVEL_TO_TRANSLATE="$1" + case "$LEVEL_TO_TRANSLATE" in + "critical" | "crit" | "fatal") + echo "CRIT" + ;; + "error" | "err") + echo "ERR" + ;; + "warning" | "warn") + echo "WARN" + ;; + "information" | "info") + echo "INFO" + ;; + "debug" | "verbose") + echo "DEBUG" + ;; + *) + echo "INFO" + ;; + esac +} + +translateLogLevelsToJournalD() { + LEVEL_TO_TRANSLATE="$(standardizeLogLevels "$1")" + case "$LEVEL_TO_TRANSLATE" in + "CRIT") + return 5 + ;; + "ERR") + return 4 + ;; + "WARN") + return 3 + ;; + "INFO") + return 1 + ;; + "DEBUG") + return 0 + ;; + *) + return 0 + esac +} + # modified syslog fmt: [pid]: [LEVEL:$2] [STATUS:$3] (:) writePlainTextLog() { # not enough information to generate a meaningful log message From 0fcd0d9c13b97ee8d958f6b6e261a78d5dd00b95 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Wed, 22 Jul 2026 23:15:11 -0600 Subject: [PATCH 17/60] refactor(logger): Use standardized log levels for plain-text --- logger.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logger.sh b/logger.sh index 24b6619..42ac5e0 100644 --- a/logger.sh +++ b/logger.sh @@ -72,7 +72,7 @@ writePlainTextLog() { printf "%s %s %s[%s]: [%s] [%s] %s (%s:%s)\n" \ "$(getTimeStamp)" "$(getHostname)" "$LOG_PROGRAM_NAME" "$$" \ - "$2" "$(uppercase "$3")" "$1" "${4:UNSPECIFIED}" "${5:999}" + "$2" "$(standardizeLogLevels "$3")" "$1" "${4:UNSPECIFIED}" "${5:999}" } # $1: message, $2: level, $3: status, [$4: operation], [$5: code] From cae57ccf6b803c80d0f9b1121f92d4502b2e6014 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Wed, 22 Jul 2026 23:15:44 -0600 Subject: [PATCH 18/60] feat(logger): Use standardized log levels for JSON --- logger.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logger.sh b/logger.sh index 42ac5e0..c4a5887 100644 --- a/logger.sh +++ b/logger.sh @@ -89,7 +89,7 @@ writeJsonLog() { --arg pName "$LOG_PROGRAM_NAME" \ --arg pid = "$$" \ --arg level "$2" \ - --arg status "$(uppercase "$3")" \ + --arg status "$(standardizeLogLevels "$3")" \ --arg message "$1" \ --arg operation "${4:UNSPECIFIED}" \ --arg code "${5:999}" \ From ce1ab86d227b36386f824e9d337a21b1beb26669 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Wed, 22 Jul 2026 23:18:36 -0600 Subject: [PATCH 19/60] fix(logger): Fix JSON logger Level and status were swapped. JSON output was missing new prefix information (timestamp, hostname, etc.) --- logger.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/logger.sh b/logger.sh index c4a5887..503c5b8 100644 --- a/logger.sh +++ b/logger.sh @@ -88,12 +88,12 @@ writeJsonLog() { --arg hostname "$(getHostname)" \ --arg pName "$LOG_PROGRAM_NAME" \ --arg pid = "$$" \ - --arg level "$2" \ - --arg status "$(standardizeLogLevels "$3")" \ + --arg level "$(standardizeLogLevels "$2")" \ + --arg status "$(uppercase "$3")" \ --arg message "$1" \ --arg operation "${4:UNSPECIFIED}" \ --arg code "${5:999}" \ - '{"level":$level, "programName":$pName, "status":$status, "operation":$operation, "code":$code, "message":$message}' \ + '{"timestamp":$timestamp, "hostname":$hostname, "programName":$pName, "pid":$pid, "level":$level, "status":$status, "message":$message, "operation":$operation, "code":$code}' \ )" fi From 56a5ad59fef49055df39c64f53ba74ad24878017 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Wed, 22 Jul 2026 23:19:51 -0600 Subject: [PATCH 20/60] refactor(logger): Rename writeJournalLog function Rename from 'writeJournalLog' to 'writeJournalDLog' for specificity. --- logger.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/logger.sh b/logger.sh index 503c5b8..8a9ff05 100644 --- a/logger.sh +++ b/logger.sh @@ -102,7 +102,7 @@ writeJsonLog() { } # $1: message, $2: level, $3: status, $4: operation, $5: code -writeJournalLog() { +writeJournalDLog() { # translate error levels if [ -z "$2" ]; then LOG_LEVEL=1 @@ -179,11 +179,11 @@ if [ "$1" = "json" ]; then writeJsonLog "This is a test message" "error" "testing" # generate this message at the 'error' level, part of a 'test' operation, and with the default code writeJsonLog "This is a test message" "alert" "testing" 10 # generate this message at the 'alert' level with all parameters set elif [ "$1" = "journal" ]; then - writeJournalLog # this will generate a generic error message - writeJournalLog "This is a test message" # generate this message at the default level with the default code - writeJournalLog "This is a test message" "warning" # generate this message at the 'warning' level with the default code - writeJournalLog "This is a test message" "error" "testing" # generate this message at the 'error' level as part of a 'test' operation, and with the default code - writeJournalLog "This is a test message" "alert" "testing" 10 # generate this message at the 'alert' level with all parameters set + writeJournalDLog # this will generate a generic error message + writeJournalDLog "This is a test message" # generate this message at the default level with the default code + writeJournalDLog "This is a test message" "warning" # generate this message at the 'warning' level with the default code + writeJournalDLog "This is a test message" "error" "testing" # generate this message at the 'error' level as part of a 'test' operation, and with the default code + writeJournalDLog "This is a test message" "alert" "testing" 10 # generate this message at the 'alert' level with all parameters set else printf "\nPlease specify either 'json' or 'journal' after the script name. Exiting.\n\n" exit 1 From aa446c00e67169e654aee4a1942fd413bf38904e Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Wed, 22 Jul 2026 23:24:52 -0600 Subject: [PATCH 21/60] feat(logger): Update journal logging function Use new standardized format fields. --- logger.sh | 59 +++++++++---------------------------------------------- 1 file changed, 9 insertions(+), 50 deletions(-) diff --git a/logger.sh b/logger.sh index 8a9ff05..0bca0bd 100644 --- a/logger.sh +++ b/logger.sh @@ -103,62 +103,21 @@ writeJsonLog() { # $1: message, $2: level, $3: status, $4: operation, $5: code writeJournalDLog() { - # translate error levels - if [ -z "$2" ]; then - LOG_LEVEL=1 - else - case "$2" in - "emergency" | "emerg") - LOG_LEVEL=7 - ;; - "alert") - LOG_LEVEL=6 - ;; - "critical" | "crit") - LOG_LEVEL=5 - ;; - "error" | "err") - LOG_LEVEL=4 - ;; - "warning" | "warn") - LOG_LEVEL=3 - ;; - "notice") - LOG_LEVEL=2 - ;; - "info") - LOG_LEVEL=1 - ;; - "debug") - LOG_LEVEL=0 - ;; - esac + # not enough information to generate a meaningful log message + if [ -z "$1" ] || [ $# -lt 3 ]; then + return fi - # log the message - if [ -z "$1" ]; then - logger --journald < Date: Wed, 22 Jul 2026 23:25:28 -0600 Subject: [PATCH 22/60] feat(logger): Remove unused global vars --- logger.sh | 5 ----- 1 file changed, 5 deletions(-) diff --git a/logger.sh b/logger.sh index 0bca0bd..b1448a4 100644 --- a/logger.sh +++ b/logger.sh @@ -125,11 +125,6 @@ end } LOG_PROGRAM_NAME="cfddns" -LOG_DEFAULT_CODE=99 -LOG_DEFAULT_LEVEL="info" -LOG_DEFAULT_MESSAGE="An unspecified error has occurred" -LOG_DEFAULT_OPERATION="unspecified" -LOG_DEFAULT_STATUS="ERR" if [ "$1" = "json" ]; then writeJsonLog # this will generate a generic error message From df144a5211ead7e20aa16c2ba0e14a4109729bce Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Wed, 22 Jul 2026 23:33:49 -0600 Subject: [PATCH 23/60] fix(logger): Remove errant 'fi' statements Remove 'fi' statements left over from previous logic checks. --- logger.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/logger.sh b/logger.sh index b1448a4..4fb1796 100644 --- a/logger.sh +++ b/logger.sh @@ -95,7 +95,6 @@ writeJsonLog() { --arg code "${5:999}" \ '{"timestamp":$timestamp, "hostname":$hostname, "programName":$pName, "pid":$pid, "level":$level, "status":$status, "message":$message, "operation":$operation, "code":$code}' \ )" - fi echo "$PROPS" | jq "." return @@ -119,7 +118,6 @@ MESSAGE="$1" OPERATION="${4:UNSPECIFIED}" ERRNO="${5:999}" end - fi return } From 544361238117368386ffa4a58bb6e730168daaad Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Wed, 22 Jul 2026 23:35:08 -0600 Subject: [PATCH 24/60] feat(logger): Add plain-text test output --- logger.sh | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/logger.sh b/logger.sh index 4fb1796..1b50716 100644 --- a/logger.sh +++ b/logger.sh @@ -124,7 +124,15 @@ end LOG_PROGRAM_NAME="cfddns" -if [ "$1" = "json" ]; then +if [ "$1" = "text" ]; then + writePlainTextLog # this will do nothing + writePlainTextLog "Testing plain-text logging output" # this will also do nothing + writePlainTextLog "Informational plain-text log message" "information" "ok" # info message + writePlainTextLog "Plain-text message forced to info level" "whoops" "ok" # level translated to default + writePlainTextLog "Warning! This is plain-text." "warning" "flagged" # warning level with 'flagged' status + writePlainTextLog "Error sample! This is plain-text." "err" "error" # error level message + writePlainTextLog "Unable to continue, critical failure. (This is plain-text)" "fatal" "error" # critical level error +elif [ "$1" = "json" ]; then writeJsonLog # this will generate a generic error message writeJsonLog "This is a test message" # generate this message at the default level with the default code writeJsonLog "This is a test message" "warning" # generate this message at the 'warning' level with the default code @@ -137,7 +145,7 @@ elif [ "$1" = "journal" ]; then writeJournalDLog "This is a test message" "error" "testing" # generate this message at the 'error' level as part of a 'test' operation, and with the default code writeJournalDLog "This is a test message" "alert" "testing" 10 # generate this message at the 'alert' level with all parameters set else - printf "\nPlease specify either 'json' or 'journal' after the script name. Exiting.\n\n" + printf "\nPlease specify either 'text', 'json', or 'journal' after the script name. Exiting.\n\n" exit 1 fi From c94f12e976ad5d05e9d0614886cd758aa9fc4998 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Wed, 22 Jul 2026 23:38:45 -0600 Subject: [PATCH 25/60] fix(logger): Fix error in uppercase func 'upper' and 'lower' translate strings were not supplied separately. --- logger.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logger.sh b/logger.sh index 1b50716..d089f70 100644 --- a/logger.sh +++ b/logger.sh @@ -13,7 +13,7 @@ lowercase() { } uppercase() { - echo "$1" | tr '[:lower:] [:upper:]' + echo "$1" | tr '[:lower:]' '[:upper:]' } standardizeLogLevels() { From 4a6c339a0751d485d8e261c0c793b2b2bb8bef6a Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Wed, 22 Jul 2026 23:42:33 -0600 Subject: [PATCH 26/60] fix(logger): Fix default subst Default variable substitution in plain-text function was incorrect. --- logger.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logger.sh b/logger.sh index d089f70..184851b 100644 --- a/logger.sh +++ b/logger.sh @@ -72,7 +72,7 @@ writePlainTextLog() { printf "%s %s %s[%s]: [%s] [%s] %s (%s:%s)\n" \ "$(getTimeStamp)" "$(getHostname)" "$LOG_PROGRAM_NAME" "$$" \ - "$2" "$(standardizeLogLevels "$3")" "$1" "${4:UNSPECIFIED}" "${5:999}" + "$(standardizeLogLevels "$2")" "$(uppercase "$3")" "$1" "${4:-UNSPECIFIED}" "${5:-999}" } # $1: message, $2: level, $3: status, [$4: operation], [$5: code] From f08583878190463070ef689a8ba805df2d61d406 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Wed, 22 Jul 2026 23:42:52 -0600 Subject: [PATCH 27/60] feat(logger): Add test with operation, error code --- logger.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logger.sh b/logger.sh index 184851b..466db8d 100644 --- a/logger.sh +++ b/logger.sh @@ -131,7 +131,7 @@ if [ "$1" = "text" ]; then writePlainTextLog "Plain-text message forced to info level" "whoops" "ok" # level translated to default writePlainTextLog "Warning! This is plain-text." "warning" "flagged" # warning level with 'flagged' status writePlainTextLog "Error sample! This is plain-text." "err" "error" # error level message - writePlainTextLog "Unable to continue, critical failure. (This is plain-text)" "fatal" "error" # critical level error + writePlainTextLog "Unable to continue, critical failure. (This is plain-text)" "fatal" "exit" "startup" "2" # critical level error with operation and error code elif [ "$1" = "json" ]; then writeJsonLog # this will generate a generic error message writeJsonLog "This is a test message" # generate this message at the default level with the default code From c4d28d1f8c6785a6c9d7fa7f4cf51dd6c2bb67c2 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Wed, 22 Jul 2026 23:44:08 -0600 Subject: [PATCH 28/60] fix(logger): Fix default subst Default parameter substitution in all functions was incorrect. --- logger.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/logger.sh b/logger.sh index 466db8d..3cbe0d2 100644 --- a/logger.sh +++ b/logger.sh @@ -91,8 +91,8 @@ writeJsonLog() { --arg level "$(standardizeLogLevels "$2")" \ --arg status "$(uppercase "$3")" \ --arg message "$1" \ - --arg operation "${4:UNSPECIFIED}" \ - --arg code "${5:999}" \ + --arg operation "${4:-UNSPECIFIED}" \ + --arg code "${5:-999}" \ '{"timestamp":$timestamp, "hostname":$hostname, "programName":$pName, "pid":$pid, "level":$level, "status":$status, "message":$message, "operation":$operation, "code":$code}' \ )" @@ -115,8 +115,8 @@ SYSLOG_PID="$$" PRIORITY="$(translateLogLevelsToJournalD "$2")" STATUS="$(uppercase "$3")" MESSAGE="$1" -OPERATION="${4:UNSPECIFIED}" -ERRNO="${5:999}" +OPERATION="${4:-UNSPECIFIED}" +ERRNO="${5:-999}" end return From d0b2c781bff1d5eec97505b75dabf0b2fa68b2b8 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Wed, 22 Jul 2026 23:48:16 -0600 Subject: [PATCH 29/60] feat(logger): Update test cases Update JSON and JOURNALD tests to match text tests and use new functions. --- logger.sh | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/logger.sh b/logger.sh index 3cbe0d2..f869a19 100644 --- a/logger.sh +++ b/logger.sh @@ -133,17 +133,21 @@ if [ "$1" = "text" ]; then writePlainTextLog "Error sample! This is plain-text." "err" "error" # error level message writePlainTextLog "Unable to continue, critical failure. (This is plain-text)" "fatal" "exit" "startup" "2" # critical level error with operation and error code elif [ "$1" = "json" ]; then - writeJsonLog # this will generate a generic error message - writeJsonLog "This is a test message" # generate this message at the default level with the default code - writeJsonLog "This is a test message" "warning" # generate this message at the 'warning' level with the default code - writeJsonLog "This is a test message" "error" "testing" # generate this message at the 'error' level, part of a 'test' operation, and with the default code - writeJsonLog "This is a test message" "alert" "testing" 10 # generate this message at the 'alert' level with all parameters set + writeJsonLog # this will do nothing + writeJsonLog "This is a test message" # this will also do nothing + writeJsonLog "Informational JSON log message" "information" "ok" # info message + writeJsonLog "JSON message forced to info level" "whoops" "ok" # level translated to default + writeJsonLog "Warning! This is JSON." "warning" "flagged" # warning level with 'flagged' status + writeJsonLog "Error sample! This is JSON." "err" "error" # error level message + writeJsonLog "Unable to continue, critical failure. (This is JSON)" "fatal" "exit" "startup" "2" # critical level error with operation and error code elif [ "$1" = "journal" ]; then - writeJournalDLog # this will generate a generic error message - writeJournalDLog "This is a test message" # generate this message at the default level with the default code - writeJournalDLog "This is a test message" "warning" # generate this message at the 'warning' level with the default code - writeJournalDLog "This is a test message" "error" "testing" # generate this message at the 'error' level as part of a 'test' operation, and with the default code - writeJournalDLog "This is a test message" "alert" "testing" 10 # generate this message at the 'alert' level with all parameters set + writeJournalDLog # this will do nothing + writeJournalDLog "This is a test message" # this will also do nothing + writeJournalDLog "Informational JOURNALD log message" "information" "ok" # info message + writeJournalDLog "JOURNALD message forced to info level" "whoops" "ok" # level translated to default + writeJournalDLog "Warning! This is a JOURNALD message." "warning" "flagged" # warning level with 'flagged' status + writeJournalDLog "Error sample! This is a JOURNALD." "err" "error" # error level message + writeJournalDLog "Unable to continue, critical failure. (This is JOURNALD message)" "fatal" "exit" "startup" "2" # critical level error with operation and error code else printf "\nPlease specify either 'text', 'json', or 'journal' after the script name. Exiting.\n\n" exit 1 From b8fb7446234e5d15479f281d9bf18654c12e9d6a Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Wed, 22 Jul 2026 23:50:33 -0600 Subject: [PATCH 30/60] fix(logger): Fix error in json logger Assignment to 'pid' argument was incorrect. --- logger.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logger.sh b/logger.sh index f869a19..8d74918 100644 --- a/logger.sh +++ b/logger.sh @@ -87,7 +87,7 @@ writeJsonLog() { --arg timestamp "$(getTimeStamp)" \ --arg hostname "$(getHostname)" \ --arg pName "$LOG_PROGRAM_NAME" \ - --arg pid = "$$" \ + --arg pid "$$" \ --arg level "$(standardizeLogLevels "$2")" \ --arg status "$(uppercase "$3")" \ --arg message "$1" \ From ec75cffbf13a97714126d4bd1eea5771bf0265a8 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Thu, 23 Jul 2026 00:16:19 -0600 Subject: [PATCH 31/60] fix(logger): Ensure log levels are lcase before standardization --- logger.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/logger.sh b/logger.sh index 8d74918..43ded13 100644 --- a/logger.sh +++ b/logger.sh @@ -17,8 +17,8 @@ uppercase() { } standardizeLogLevels() { - LEVEL_TO_TRANSLATE="$1" - case "$LEVEL_TO_TRANSLATE" in + LEVEL_TO_STANDARDIZE="$(lowercase "$1")" + case "$LEVEL_TO_STANDARDIZE" in "critical" | "crit" | "fatal") echo "CRIT" ;; From d9aa8973a3a2cf55460b06c9f82d220ab4f7d3b6 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Thu, 23 Jul 2026 00:17:42 -0600 Subject: [PATCH 32/60] fix(logger): Perform cmd subst outside heredoc Perform function and command value substitution outside the heredoc forwarded to logger for JournalD entries. --- logger.sh | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/logger.sh b/logger.sh index 43ded13..2e278d7 100644 --- a/logger.sh +++ b/logger.sh @@ -107,16 +107,21 @@ writeJournalDLog() { return fi + J_TIMESTAMP="$(getTimeStamp)" + J_PID="$$" + J_PRIO="$(translateLogLevelsToJournalD "$2")" + J_STAT="$(uppercase "$3")" + logger --journald < Date: Thu, 23 Jul 2026 00:18:03 -0600 Subject: [PATCH 33/60] feat(logger): Echo vs return values from Translate func --- logger.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/logger.sh b/logger.sh index 2e278d7..086704c 100644 --- a/logger.sh +++ b/logger.sh @@ -44,22 +44,22 @@ translateLogLevelsToJournalD() { LEVEL_TO_TRANSLATE="$(standardizeLogLevels "$1")" case "$LEVEL_TO_TRANSLATE" in "CRIT") - return 5 + echo 5 ;; "ERR") - return 4 + echo 4 ;; "WARN") - return 3 + echo 3 ;; "INFO") - return 1 + echo 1 ;; "DEBUG") - return 0 + echo 0 ;; *) - return 0 + echo 1 esac } From 92e69d1b859b5455a5371b06fc452cba2aff9818 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Thu, 23 Jul 2026 00:20:30 -0600 Subject: [PATCH 34/60] fix(logger): Fix JournalD log levels Log levels were reversed in the translation function. --- logger.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/logger.sh b/logger.sh index 086704c..7b51d04 100644 --- a/logger.sh +++ b/logger.sh @@ -44,22 +44,22 @@ translateLogLevelsToJournalD() { LEVEL_TO_TRANSLATE="$(standardizeLogLevels "$1")" case "$LEVEL_TO_TRANSLATE" in "CRIT") - echo 5 + echo 2 ;; "ERR") - echo 4 - ;; - "WARN") echo 3 ;; + "WARN") + echo 4 + ;; "INFO") - echo 1 + echo 6 ;; "DEBUG") - echo 0 + echo 7 ;; *) - echo 1 + echo 6 esac } From 450733a8d5f6aec203f0632d84eca14ff3705f3c Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Thu, 23 Jul 2026 21:55:25 -0600 Subject: [PATCH 35/60] feat(func): Generate ISO-8601 timestamp --- cfddns.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cfddns.sh b/cfddns.sh index dc4ae72..64dfaf9 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -31,6 +31,11 @@ else width=80 fi +getTimeStamp() { + (date -u +"%Y-%m-%dT%H:%M:%SZ") +} + + ### functions badParam() { if [ "$1" = "null" ]; then From 1336c8e63b2fe827cb59d715ef3f7775f9ecbad6 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Thu, 23 Jul 2026 21:55:56 -0600 Subject: [PATCH 36/60] feat(func): Retrieve machine host name --- cfddns.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cfddns.sh b/cfddns.sh index 64dfaf9..6ed7f07 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -35,6 +35,9 @@ getTimeStamp() { (date -u +"%Y-%m-%dT%H:%M:%SZ") } +getHostname() { + (hostname -s) +} ### functions badParam() { From 1da49b06db0f1035b4e19e800a69613970c2afa3 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Thu, 23 Jul 2026 21:56:19 -0600 Subject: [PATCH 37/60] feat(func): Convert text ucase, lcase --- cfddns.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cfddns.sh b/cfddns.sh index 6ed7f07..031000b 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -39,6 +39,14 @@ getHostname() { (hostname -s) } +lowercase() { + echo "$1" | tr '[:upper:]' '[:lower:]' +} + +uppercase() { + echo "$1" | tr '[:lower:]' '[:upper:]' +} + ### functions badParam() { if [ "$1" = "null" ]; then From 8a5a72e15a6699f7bb92b996aa871e0ce5eec0fd Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Thu, 23 Jul 2026 21:57:30 -0600 Subject: [PATCH 38/60] feat(func): Convert to uniform log levels Take input text with various references to log levels and return a standardized uppercase log level name. --- cfddns.sh | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/cfddns.sh b/cfddns.sh index 031000b..0fcd01f 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -47,6 +47,30 @@ uppercase() { echo "$1" | tr '[:lower:]' '[:upper:]' } +standardizeLogLevels() { + LEVEL_TO_STANDARDIZE="$(lowercase "$1")" + case "$LEVEL_TO_STANDARDIZE" in + "critical" | "crit" | "fatal") + echo "CRIT" + ;; + "error" | "err") + echo "ERR" + ;; + "warning" | "warn") + echo "WARN" + ;; + "information" | "info") + echo "INFO" + ;; + "debug" | "verbose") + echo "DEBUG" + ;; + *) + echo "INFO" + ;; + esac +} + ### functions badParam() { if [ "$1" = "null" ]; then From f8908d783079d21c0e4302be893c2a86c8344fe8 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Thu, 23 Jul 2026 21:59:58 -0600 Subject: [PATCH 39/60] feat(func): Write modified syslog fmt msg Take input parameters and generate a modified syslog-style log entry. --- cfddns.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cfddns.sh b/cfddns.sh index 0fcd01f..41756d4 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -71,6 +71,19 @@ standardizeLogLevels() { esac } +# modified syslog fmt: [pid]: [LEVEL:$2] [STATUS:$3] (:) +writePlainTextLog() { + # not enough information to generate a meaningful log message + if [ -z "$1" ] || [ $# -lt 3 ]; then + return + fi + + printf "%s %s %s[%s]: [%s] [%s] %s (%s:%s)\n" \ + "$(getTimeStamp)" "$(getHostname)" "$LOG_PROGRAM_NAME" "$$" \ + "$(standardizeLogLevels "$2")" "$(uppercase "$3")" "$1" "${4:-UNSPECIFIED}" "${5:-999}" \ + >>"$logFile" +} + ### functions badParam() { if [ "$1" = "null" ]; then From f8f3bf1539e014bfa987553f386f1999b51bbb50 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Thu, 23 Jul 2026 22:17:15 -0600 Subject: [PATCH 40/60] refactor(func): Change default err code to 0 --- cfddns.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cfddns.sh b/cfddns.sh index 41756d4..79e8cdf 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -80,7 +80,7 @@ writePlainTextLog() { printf "%s %s %s[%s]: [%s] [%s] %s (%s:%s)\n" \ "$(getTimeStamp)" "$(getHostname)" "$LOG_PROGRAM_NAME" "$$" \ - "$(standardizeLogLevels "$2")" "$(uppercase "$3")" "$1" "${4:-UNSPECIFIED}" "${5:-999}" \ + "$(standardizeLogLevels "$2")" "$(uppercase "$3")" "$1" "${4:-UNSPECIFIED}" "${5:-0}" \ >>"$logFile" } From 51b2b455a262f2b2f16cf18861c2202eca040a6a Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Thu, 23 Jul 2026 23:16:12 -0600 Subject: [PATCH 41/60] feat(log): Use plain-text logging for all messages Convert all logging output to plain-text structured messages using new function. --- cfddns.sh | 179 +++++++++++++++++++++++++++--------------------------- 1 file changed, 88 insertions(+), 91 deletions(-) diff --git a/cfddns.sh b/cfddns.sh index 79e8cdf..7ce4f08 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -139,23 +139,24 @@ exitOK() { exit 0 } +# 1: cloudflare error object, 2: operation identifier listCFErrors() { # extract error codes and messages in separate variables, replace newlines with underscores codes="$(printf "%s" "$1" | jq -r '.errors | .[] | .code' | tr '\n' '_')" messages="$(printf "%s" "$1" | jq -r '.errors | .[] | .message' | tr '\n' '_')" - # iterate codes and messages and assemble into coherent messages in log + # iterate codes and messages, assemble into a coherent log message while [ -n "$codes" ] && [ -n "$messages" ]; do - # get first code and message in respective sets + # get the first code and message in respective sets code="${codes%%_*}" message="${messages%%_*}" - # update codes and messages sets by removing first item in each set + # update sets of codes and messages by removing the first item (above) in each set codes="${codes#*_}" messages="${messages#*_}" # output to log - writeLog cf "$message" "$code" + writePlainTextLog "${message}" "err" "fail" "$2" "$code" done } @@ -415,18 +416,24 @@ done ### pre-flight checks if ! command -v curl >/dev/null; then printf "\n%sThis script requires 'curl' be installed and accessible. Exiting.%s\n\n" "$err" "$norm" + writePlainTextLog "'curl' must be installed and accessible" "fatal" "fail" "preexec" 2 exit 2 fi if ! command -v jq >/dev/null; then printf "\n%sThis script requires 'jq' be installed and accessible. Exiting.%s\n\n" "$err" "$norm" + writePlainTextLog "'jq' must be installed and accessible" "fatal" "fail" "preexec" 2 exit 2 fi -[ -z "$dnsRecords" ] && badParam errMsg "You must specify at least one DNS record to update. Exiting." -# verify credentials file exists and is not empty (default check) -if [ ! -f "$accountFile" ] || [ ! -s "$accountFile" ]; then - badParam errMsg "Cannot find Cloudflare credentials file (${accountFile}). Exiting." +if [ -z "$dnsRecords" ]; then + badParam errMsg "You must specify at least one DNS record to update. Exiting." + writePlainTextLog "At least one DNS record to update must be specified" "fatal" "fail" "params" 1 fi -if ! command -v logger >/dev/null 2>&1; then +# verify the credentials file exists and is not empty (default check) +if [ ! -f "$accountFile" ] || [ ! -s "$accountFile" ]; then + badParam errMsg "Cannot find the Cloudflare credentials file (${accountFile}). Exiting." + writePlainTextLog "Cannot find the specified Cloudflare credentials file (${accountFile})" "fatal" "fail" "params" 1 +fi +if [ "$logToJournal" -eq 1 ] && ! command -v logger >/dev/null 2>&1; then printf "\n%sThis script requires 'logger' be installed to write entries to your journaling system. Exiting.%s\n\n" "$err" "$norm" exit 2 fi @@ -443,56 +450,51 @@ if [ "$colourizeLogFile" -eq 0 ]; then fi ### initial log entries -{ +writePlainTextLog "starting '${scriptName}'" "info" "ok" "script" +writePlainTextLog "script path: ${scriptPath}/${scriptName}" "debug" "ok" "startup" +writePlainTextLog "credentials file: ${accountFile}" "debug" "ok" "startup" - printf "%s[%s] -- Cloudflare DDNS update-script: starting --%s\n" "$ok" "$(stamp)" "$norm" - printf "Parameters:\n" - printf "script path: %s\n" "$scriptPath/$scriptName" - printf "credentials file: %s\n" "$accountFile" +if [ "$ip4" -eq 1 ]; then + writePlainTextLog "mode: IPv4" "debug" "ok" "startup" +elif [ "$ip6" -eq 1 ]; then + writePlainTextLog "mode: IPv6" "debug" "ok" "startup" +fi +# detect and report IP address +if [ -z "$ipAddress" ]; then + # detect public ip address if [ "$ip4" -eq 1 ]; then - printf "mode: IP4\n" - elif [ "$ip6" -eq 1 ]; then - printf "mode: IP6\n" + if ! ipAddress="$(curl -s $ip4DetectionSvc)"; then + writePlainTextLog \ + "Unable to auto-detect this machine's public IP address; try again later or supply the IP address to be used" "err" "fail" "detectip" 10 + exit 10 + fi fi - - # detect and report IP address - if [ -z "$ipAddress" ]; then - # detect public ip address - if [ "$ip4" -eq 1 ]; then - if ! ipAddress="$(curl -s $ip4DetectionSvc)"; then - printf "ddns ip address:%s ERROR%s\n" "$err" "$norm" - exitError 10 - fi + if [ "$ip6" -eq 1 ]; then + if ! ipAddress="$(curl -s $ip6DetectionSvc)"; then + writePlainTextLog \ + "Unable to auto-detect this machine's public IP address; try again later or supply the IP address to be used" "err" "fail" "detectip" 10 + exit 10 fi - if [ "$ip6" -eq 1 ]; then - if ! ipAddress="$(curl -s $ip6DetectionSvc)"; then - printf "ddns ip address:%s ERROR%s\n" "$err" "$norm" - exitError 10 - fi - fi - printf "ddns ip address (detected): %s\n" "$ipAddress" - else - printf "ddns ip address (supplied): %s\n" "$ipAddress" fi + writePlainTextLog "DDNS IP address (detected): $ipAddress" "info" "ok" "startup" +else + writePlainTextLog "DDNS IP address (supplied): $ipAddress" "info" "ok" "startup" +fi - # iterate DNS records to update - dnsRecordsToUpdate="$(printf '%s' "${dnsRecords}" | sed "s/${dnsSeparator}*$//")$dnsSeparator" - while [ -n "$dnsRecordsToUpdate" ] && [ "$dnsRecordsToUpdate" != "$dnsSeparator" ]; do - record="${dnsRecordsToUpdate%%${dnsSeparator}*}" - dnsRecordsToUpdate="${dnsRecordsToUpdate#*${dnsSeparator}}" +# iterate DNS records to update +dnsRecordsToUpdate="$(printf '%s' "${dnsRecords}" | sed "s/${dnsSeparator}*$//")$dnsSeparator" +while [ -n "$dnsRecordsToUpdate" ] && [ "$dnsRecordsToUpdate" != "$dnsSeparator" ]; do + record="${dnsRecordsToUpdate%%${dnsSeparator}*}" + dnsRecordsToUpdate="${dnsRecordsToUpdate#*${dnsSeparator}}" - if [ -z "$record" ]; then - continue - fi - printf "updating record: %s\n" "$record" - done - - printf "(end of parameter list)\n" -} >>"$logFile" + if [ -z "$record" ]; then + continue + fi + writePlainTextLog "DNS host record '${record}' queued for update" "info" "ok" "startup" +done ### read Cloudflare credentials -writeLog process "Reading Cloudflare credentials" case "$accountFile" in /*) # absolute path, use as-is @@ -506,14 +508,14 @@ case "$accountFile" in ;; esac if [ -z "$cfKey" ]; then - writeLog process-error - exitError 21 + writePlainTextLog "Cloudflare authorized API key (cfKey) is either null or undefined; please check your Cloudflare credentials file" "err" "fail" "creds" 21 + exit 21 fi if [ -z "$cfZoneId" ]; then - writeLog process-error - exitError 22 + writePlainTextLog "Cloudflare zone id (cfZoneId) is either null or undefined; please check your Cloudflare credentials file" "err" "fail" "creds" 22 + exit 22 fi -writeLog process-done "DONE" +writePlainTextLog "Cloudflare credentials file read successfully" "debug" "ok" "creds" ### connect to Cloudflare and do what needs to be done! dnsRecordsToUpdate="$dnsRecords$dnsSeparator" @@ -531,7 +533,6 @@ while [ -n "$dnsRecordsToUpdate" ] && [ "$dnsRecordsToUpdate" != "$dnsSeparator" if [ -z "$record" ]; then continue fi - writeLog process "Processing ${record}" # exit if curl/network error if ! cfLookup="$( @@ -539,43 +540,39 @@ while [ -n "$dnsRecordsToUpdate" ] && [ "$dnsRecordsToUpdate" != "$dnsSeparator" -H "Authorization: Bearer ${cfKey}" \ -H "Content-Type: application/json" )"; then - writeLog process-error - exitError 3 + writePlainTextLog "Unable to connect to Cloudflare servers; please try again later." "err" "fail" "cflogin" 3 + exit 3 fi - # exit if API error - # exit here since API errors on GET request probably indicates authentication error which would affect all remaining operations - # no reason to continue processing other hosts and pile-up errors which might look like a DoS attempt + # an API error on this GET request likely indicates an authentication error that would affect all remaining operations + # no reason to continue processing other dns records and pile-up errors which might look like a DoS attempt cfSuccess="$(printf "%s" "$cfLookup" | jq -r '.success')" if [ "$cfSuccess" = "false" ]; then - writeLog process-error - listCFErrors "$cfLookup" - exitError 25 + listCFErrors "$cfLookup" "cflogin" + writePlainTextLog "Cloudflare API error; review any previously logged 'CF-ERR:' lines for details." "err" "fail" "cflogin" 25 + exit 25 fi resultCount="$(printf "%s" "$cfLookup" | jq '.result_info.count')" - # skip to next host if cannot find existing host record (this script *updates* only, does not create!) + # skip to the next host if an existing host record cannot be found (this script *updates* only, does not create!) if [ "$resultCount" = "0" ]; then - # warn if record of host not found - writeLog process-warning "NOT FOUND" - writeLog warn "Cannot find existing record to update for DNS entry: ${record}" + writePlainTextLog "Cannot find an existing record matching '${record}' to update" "warn" "warn" "ddns" invalidDomainCount=$((invalidDomainCount + 1)) continue fi objectId=$(printf "%s" "$cfLookup" | jq -r '.result | .[] | .id') currentIpAddr=$(printf "%s" "$cfLookup" | jq -r '.result | .[] | .content') - writeLog process-done "FOUND: IP = ${currentIpAddr}" + writePlainTextLog "The current IP address for '${record}' is ${currentIpAddr}" "debug" "ok" "ddns" - # skip to next hostname if record already up-to-date + # skip to next hostname if record already up to date if [ "$currentIpAddr" = "$ipAddress" ]; then - writeLog stamped "IP address for ${record} is already up-to-date" + writePlainTextLog "The IP address for '${record}' is already up to date" "info" "ok" "ddns" continue fi # update record - writeLog process "Updating IP address for ${record}" updateJSON="$(jq -n --arg key0 content --arg value0 "${ipAddress}" '{($key0):$value0}')" # exit if curl/network error @@ -585,19 +582,17 @@ while [ -n "$dnsRecordsToUpdate" ] && [ "$dnsRecordsToUpdate" != "$dnsSeparator" -H "Content-Type: application/json" \ --data "${updateJSON}" )"; then - writeLog process-error - exitError 3 + writePlainTextLog "Unable to connect to Cloudflare servers; please try again later." "err" "fail" "ddns" 3 + exit 3 fi # note update success or failure cfSuccess="$(printf "%s" "$cfResult" | jq '.success')" if [ "$cfSuccess" = "true" ]; then - writeLog process-done "DONE" - writeLog success "IP address for ${record} updated." + writePlainTextLog "The IP address for '${record}' successfully updated" "info" "ok" "ddns" else - writeLog process-error - listCFErrors "$cfResult" - writeLog err "Unable to update IP address for ${record}" + listCFErrors "$cfResult" "ddns" + writePlainTextLog "Unable to update the IP address for '${record}'" "err" "fail" "ddns" # do not exit with error, API error here is probably an update issue specific to this host # increment counter and note it after all processing finished failedHostCount=$((failedHostCount + 1)) @@ -606,22 +601,24 @@ done # exit if [ "$invalidDomainCount" -ne 0 ]; then - writeLog warning "${invalidDomainCount} invalid host(s) supplied for updating." + writePlainTextLog "${invalidDomainCount} invalid host(s) were supplied for updating" "warn" "warn" "ddns" fi if [ "$failedHostCount" -ne 0 ]; then - exitError 26 + writePlainTextLog \ + "${failedHostCount} host update(s) failed; review 'CF-ERR:' lines in this log to help determine what may have gone wrong" "err" "fail" "ddns" 26 + exit 26 else - exitOK + writePlainTextLog "${scriptName} completed successfully" "info" "ok" "script" fi ### exit return codes -# 0: normal exit, no errors -# 1: invalid or unknown parameter -# 2: cannot find or access required external program(s) -# 3: curl error (probably connection) -# 10: cannot auto-detect IP address -# 21: accountFile has a null or missing cfKey variable -# 22: accountFile has a null or missing cfZoneId variable -# 25: Cloudflare API error -# 26: one or more updates failed -# 99: unspecified error occurred +# 0: normal exit, no errors +# 1: invalid or unknown parameter +# 2: cannot find or access required external program(s) +# 3: curl error (probably connection) +# 10: cannot auto-detect IP address +# 21: accountFile has a null or missing cfKey variable +# 22: accountFile has a null or missing cfZoneId variable +# 25: Cloudflare API error +# 26: one or more updates failed +# 99: unspecified error occurred From 1732de343de730dd0026bed226228b57cf0c3760 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Thu, 23 Jul 2026 23:22:17 -0600 Subject: [PATCH 42/60] refactor(func): Remove unused functions Remvoe the logging functions replaced by the new structured implementation. --- cfddns.sh | 161 +++++++++++++----------------------------------------- 1 file changed, 38 insertions(+), 123 deletions(-) diff --git a/cfddns.sh b/cfddns.sh index 7ce4f08..98ffb2e 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -31,14 +31,29 @@ else width=80 fi -getTimeStamp() { - (date -u +"%Y-%m-%dT%H:%M:%SZ") +badParam() { + if [ "$1" = "null" ]; then + printf "\n%sERROR: '%s' cannot have a NULL (empty) value.\n" "$err" "$2" + printf "%sPlease use '--help' for assistance.%s\n\n" "$cyan" "$norm" + exit 1 + elif [ "$1" = "dne" ]; then + printf "\n%sERROR: '%s %s'\n" "$err" "$2" "$3" + printf "file or directory does not exist or is empty.%s\n\n" "$norm" + exit 1 + elif [ "$1" = "errMsg" ]; then + printf "\n%sERROR: %s%s\n\n" "$err" "$2" "$norm" + exit 1 + fi } getHostname() { (hostname -s) } +getTimeStamp() { + (date -u +"%Y-%m-%dT%H:%M:%SZ") +} + lowercase() { echo "$1" | tr '[:upper:]' '[:lower:]' } @@ -47,6 +62,27 @@ uppercase() { echo "$1" | tr '[:lower:]' '[:upper:]' } +# 1: cloudflare error object, 2: operation identifier +listCFErrors() { + # extract error codes and messages in separate variables, replace newlines with underscores + codes="$(printf "%s" "$1" | jq -r '.errors | .[] | .code' | tr '\n' '_')" + messages="$(printf "%s" "$1" | jq -r '.errors | .[] | .message' | tr '\n' '_')" + + # iterate codes and messages, assemble into a coherent log message + while [ -n "$codes" ] && [ -n "$messages" ]; do + # get the first code and message in respective sets + code="${codes%%_*}" + message="${messages%%_*}" + + # update sets of codes and messages by removing the first item (above) in each set + codes="${codes#*_}" + messages="${messages#*_}" + + # output to log + writePlainTextLog "${message}" "err" "fail" "$2" "$code" + done +} + standardizeLogLevels() { LEVEL_TO_STANDARDIZE="$(lowercase "$1")" case "$LEVEL_TO_STANDARDIZE" in @@ -84,82 +120,6 @@ writePlainTextLog() { >>"$logFile" } -### functions -badParam() { - if [ "$1" = "null" ]; then - printf "\n%sERROR: '%s' cannot have a NULL (empty) value.\n" "$err" "$2" - printf "%sPlease use '--help' for assistance.%s\n\n" "$cyan" "$norm" - exit 1 - elif [ "$1" = "dne" ]; then - printf "\n%sERROR: '%s %s'\n" "$err" "$2" "$3" - printf "file or directory does not exist or is empty.%s\n\n" "$norm" - exit 1 - elif [ "$1" = "errMsg" ]; then - printf "\n%sERROR: %s%s\n\n" "$err" "$2" "$norm" - exit 1 - fi -} - -exitError() { - case "$1" in - 3) - errMsg="Unable to connect to Cloudflare servers. This is probably a temporary networking issue. Please try again later." - ;; - 10) - errMsg="Unable to auto-detect IP address. Try again later or supply the IP address to be used." - ;; - 20) - errMsg="Cloudflare authorized email address (cfEmail) is either null or undefined. Please check your Cloudflare credentials file." - ;; - 21) - errMsg="Cloudflare authorized API key (cfKey) is either null or undefined. Please check your Cloudflare credentials file." - ;; - 22) - errMsg="Cloudflare zone id (cfZoneId) is either null or undefined. Please check your Cloudflare credentials file." - ;; - 25) - errMsg="Cloudflare API error. Please review any 'CF-ERR:' lines in this log for details." - ;; - 26) - errMsg="${failedHostCount} host update(s) failed. Any 'CF-ERR:' lines noted in this log may help determine what went wrong." - ;; - *) - writeLog error "An unspecified error occurred. (code: 99)" - printf "%s[%s] -- Cloudflare DDNS update-script: completed with error(s) --%s\n" "$err" "$(stamp)" "$norm" >>"$logFile" - exit 99 - ;; - esac - writeLog error "$errMsg" "$1" - printf "%s[%s] -- Cloudflare DDNS update-script: completed with error(s) --%s\n" "$err" "$(stamp)" "$norm" >>"$logFile" - exit "$1" -} - -exitOK() { - printf "%s[%s] -- Cloudflare DDNS update-script: completed successfully --%s\n" "$ok" "$(stamp)" "$norm" >>"$logFile" - exit 0 -} - -# 1: cloudflare error object, 2: operation identifier -listCFErrors() { - # extract error codes and messages in separate variables, replace newlines with underscores - codes="$(printf "%s" "$1" | jq -r '.errors | .[] | .code' | tr '\n' '_')" - messages="$(printf "%s" "$1" | jq -r '.errors | .[] | .message' | tr '\n' '_')" - - # iterate codes and messages, assemble into a coherent log message - while [ -n "$codes" ] && [ -n "$messages" ]; do - # get the first code and message in respective sets - code="${codes%%_*}" - message="${messages%%_*}" - - # update sets of codes and messages by removing the first item (above) in each set - codes="${codes#*_}" - messages="${messages#*_}" - - # output to log - writePlainTextLog "${message}" "err" "fail" "$2" "$code" - done -} - scriptExamples() { newline printf "Update Cloudflare DNS host A/AAAA records with current IP address.\n" @@ -246,10 +206,6 @@ scriptHelp() { exit 0 } -stamp() { - (date +%F" "%T) -} - newline() { printf "\n" } @@ -266,47 +222,6 @@ textBlockSwitches() { printf "%s%s%s\n" "$cyan" "$1" "$norm" } -writeLog() { - case "$1" in - cf) - printf "[%s] CF-ERR: %s (code: %s)\n" "$(stamp)" "$2" "$3" >>"$logFile" - ;; - err) - printf "%s[%s] ERR: %s%s\n" "$err" "$(stamp)" "$2" "$norm" >>"$logFile" - ;; - error) - printf "%s[%s] ERROR: %s (code: %s)%s\n" "$err" "$(stamp)" "$2" "$3" "$norm" >>"$logFile" - ;; - process) - printf "%s[%s] %s... %s" "$cyan" "$(stamp)" "$2" "$norm" >>"$logFile" - ;; - process-done) - printf "%s%s%s\n" "$cyan" "$2" "$norm" >>"$logFile" - ;; - process-error) - printf "%sERROR%s\n" "$err" "$norm" >>"$logFile" - ;; - process-warning) - printf "%s%s%s\n" "$warn" "$2" "$norm" >>"$logFile" - ;; - stamped) - printf "[%s] %s\n" "$(stamp)" "$2" >>"$logFile" - ;; - success) - printf "%s[%s] SUCCESS: %s%s\n" "$ok" "$(stamp)" "$2" "$norm" >>"$logFile" - ;; - warn) - printf "%s[%s] WARN: %s%s\n" "$warn" "$(stamp)" "$2" "$norm" >>"$logFile" - ;; - warning) - printf "%s[%s] WARNING: %s%s\n" "$warn" "$(stamp)" "$2" "$norm" >>"$logFile" - ;; - *) - printf "%s\n" "$2" >>"$logFile" - ;; - esac -} - ### default variable values scriptPath="$(CDPATH='' \cd -- "$(dirname -- "$0")" && pwd -P)" scriptName="$(basename "$0")" From c792ec8a0a0757a8679d09f9141db8f2e9968e84 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Fri, 24 Jul 2026 01:03:28 -0600 Subject: [PATCH 43/60] feat(params): Add log type, fmt params Add log type and format parameters. Update defaults. Update help script. --- cfddns.sh | 64 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/cfddns.sh b/cfddns.sh index 98ffb2e..d1ff4d3 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -153,34 +153,38 @@ scriptHelp() { textBlock "The only required parameter is '--records' which is a comma-delimited list of hostnames to update. However, there are several other options which may be useful to implement." textBlock "Parameters are listed below and followed by a description of their effect. If a default value exists, it will be listed on the following line in (parentheses)." newline - textBlock "${magenta}--- script related parameters ---${norm}" + textBlock "${magenta}--- script-related parameters ---${norm}" newline textBlockSwitches "-c | --cred | --creds | --credentials | -f (deprecated, backward-compatibility)" - textBlock "Path to file containing your Cloudflare *token* credentials. Please refer to the repo README for more information on format, etc." + textBlock "Path to the file containing your Cloudflare *token* credentials. Please refer to the repo README for more information on format, etc." textBlockDefaults "(${accountFile})" newline - textBlockSwitches "-l | --log" - textBlock "Path where the log file should be written." + textBlockSwitches "--log-file" + textBlock "Path where the log file should be written. Script will use file-based logging." textBlockDefaults "(${logFile})" newline - textBlockSwitches "--nc | --no-color | --no-colour" - textBlock "Switch value. Disables ANSI colours in the log. Useful if you review the logs using a reader that does not parse ANSI colour codes." - textBlockDefaults "(disabled: print logs in colour)" - newline textBlockSwitches "--log-console" - textBlock "Switch value. Output log to console (stdout) instead of a log file. Can be combined with --nc if desired." - textBlockDefaults "(disabled: output to log file)" + textBlock "Switch value. Script will use console-based (stdout) logging." + textBlockDefaults "(disabled: use file-based logging)" newline textBlockSwitches "--log-journal" - textBlock "Switch value. Output a structured log entry to your journaling system instead of a log file. Implies --nc." - textBlockDefaults "(disabled: output to log file)" + textBlock "Switch value. Script will use journald-based logging." + textBlockDefaults "(disabled: use file-based logging)" newline - textBlockSwitches "--no-log" - textBlock "Switch value. Do not create a log (i.e. no console, no file). You will not have *any* output from the script if you choose this option, so you will not know if updates succeeded or failed." - textBlockDefaults "(disabled: output to log file)" + textBlockSwitches "--log-none" + textBlock "Switch value. Script will not log anything. You will not have *any* output from the script if you choose this option, so you will not know if updates succeed or fail." + textBlockDefaults "(disabled: use file-based logging)" + newline + textBlockSwitches "--fmt-json" + textBlock "Switch value. Use JSON formatted key-value pair structured logging format. Best when using a log parsing tool or log management system to read your log files. Does not apply when using '--log-journal'." + textBlockDefaults "(enabled)" + newline + textBlockSwitches "--fmt-syslog" + textBlock "Switch value. Use a modified syslog (plain-text) format for log messages. Ideal when logging to the console. Does not apply when using '--log-journal'." + textBlockDefaults "(disabled: use structured JSON logging)" newline textBlockSwitches "-h | --help | -?" - textBlock "Display this help screen." + textBlock "Display this information screen." newline textBlockSwitches "--examples" textBlock "Show some usage examples." @@ -191,7 +195,7 @@ scriptHelp() { textBlock "Comma-delimited list of hostnames for which IP addresses should be updated in Cloudflare DNS. This parameter is REQUIRED. Note that this script will only *update* records, it will not create new ones. If you supply hostnames that are not already defined in DNS, the script will log a warning and will skip those hostnames." newline textBlockSwitches "-i | --ip | --ip-address | -a | --address" - textBlock "New IP address for DNS host records. If you omit this, the script will attempt to auto-detect your public IP address and use that." + textBlock "New IP address for DNS host records. If you omit this, the script will attempt to auto-detect your public IP address and use that. N.B. IP addresses are *not* parsed for correctness." newline textBlockSwitches "-4 | --ip4 | --ipv4" textBlock "Switch value. Update Host 'A' records (IP4) only. Note that this script can only update either A *or* AAAA records. If you need to update both, you'll have to run the script once in IP4 mode and again in IP6 mode. If you specify both this switch and the IP6 switch, the last one specified will take effect." @@ -227,6 +231,8 @@ scriptPath="$(CDPATH='' \cd -- "$(dirname -- "$0")" && pwd -P)" scriptName="$(basename "$0")" logFile="$scriptPath/${scriptName%.*}.log" logToJournal=0 +useJsonLogFmt=1 +useSyslogLogFmt=0 accountFile="$scriptPath/cloudflare.credentials" colourizeLogFile=1 dnsRecords="" @@ -253,8 +259,8 @@ while [ $# -gt 0 ]; do # display sample commands scriptExamples ;; - -l | --log) - # set log file location + --log-file) + # use file-based logging at the specified location if [ -n "$2" ]; then logFile="${2%/}" shift @@ -263,23 +269,29 @@ while [ $# -gt 0 ]; do fi ;; --log-console) - # log to the console instead of a file + # use console-based logging logFile="/dev/stdout" ;; --log-journal) - # log to the journal system instead of a file + # use journald-based logging logToJournal=1 ;; - --no-log) + --log-none) # do not log anything logFile="/dev/null" ;; - --nc | --no-color | --no-colour) - # do not colourize log file - colourizeLogFile=0 + --fmt-json) + # use JSON log formatting + useJsonLogFmt=1 + useSyslogLogFmt=0 + ;; + --fmt-syslog) + # use modified syslog plain-text log formatting + useSyslogLogFmt=1 + useJsonLogFmt=0 ;; -c | --cred* | -f) - # path to Cloudflare credentials file + # path to the Cloudflare credentials file if [ -n "$2" ]; then if [ -f "$2" ] && [ -s "$2" ]; then accountFile="${2%/}" From 2b0128d0c197284bfeb49916858d54773205fb5f Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Fri, 24 Jul 2026 01:08:54 -0600 Subject: [PATCH 44/60] fix(log): Prevent preflight duplicate logging Prevent writing preflight error messages to the console twice when using console logging mode. --- cfddns.sh | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cfddns.sh b/cfddns.sh index d1ff4d3..960e63d 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -343,22 +343,24 @@ done ### pre-flight checks if ! command -v curl >/dev/null; then printf "\n%sThis script requires 'curl' be installed and accessible. Exiting.%s\n\n" "$err" "$norm" - writePlainTextLog "'curl' must be installed and accessible" "fatal" "fail" "preexec" 2 + [ "$logToConsole" -eq 0 ] && writePlainTextLog "'curl' must be installed and accessible" "fatal" "fail" "preexec" 2 exit 2 fi if ! command -v jq >/dev/null; then printf "\n%sThis script requires 'jq' be installed and accessible. Exiting.%s\n\n" "$err" "$norm" - writePlainTextLog "'jq' must be installed and accessible" "fatal" "fail" "preexec" 2 + [ "$logToConsole" -eq 0 ] && writePlainTextLog "'jq' must be installed and accessible" "fatal" "fail" "preexec" 2 exit 2 fi if [ -z "$dnsRecords" ]; then badParam errMsg "You must specify at least one DNS record to update. Exiting." - writePlainTextLog "At least one DNS record to update must be specified" "fatal" "fail" "params" 1 + [ "$logToConsole" -eq 0 ] && + writePlainTextLog "At least one DNS record to update must be specified" "fatal" "fail" "params" 1 fi # verify the credentials file exists and is not empty (default check) if [ ! -f "$accountFile" ] || [ ! -s "$accountFile" ]; then badParam errMsg "Cannot find the Cloudflare credentials file (${accountFile}). Exiting." - writePlainTextLog "Cannot find the specified Cloudflare credentials file (${accountFile})" "fatal" "fail" "params" 1 + [ "$logToConsole" -eq 0 ] && + writePlainTextLog "Cannot find the specified Cloudflare credentials file (${accountFile})" "fatal" "fail" "params" 1 fi if [ "$logToJournal" -eq 1 ] && ! command -v logger >/dev/null 2>&1; then printf "\n%sThis script requires 'logger' be installed to write entries to your journaling system. Exiting.%s\n\n" "$err" "$norm" From fca36ac119549c54305919103833c812ccd50340 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Fri, 24 Jul 2026 01:13:40 -0600 Subject: [PATCH 45/60] feat(params): Add debug logging toggle --- cfddns.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cfddns.sh b/cfddns.sh index 960e63d..206b5b0 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -175,6 +175,10 @@ scriptHelp() { textBlock "Switch value. Script will not log anything. You will not have *any* output from the script if you choose this option, so you will not know if updates succeed or fail." textBlockDefaults "(disabled: use file-based logging)" newline + textBlockSwitches "--log-debug" + textBlock "Switch value. Log 'debug' level messages. This should normally remain disabled." + textBlockDefaults "(disabled: do not log 'debug' messages)" + newline textBlockSwitches "--fmt-json" textBlock "Switch value. Use JSON formatted key-value pair structured logging format. Best when using a log parsing tool or log management system to read your log files. Does not apply when using '--log-journal'." textBlockDefaults "(enabled)" @@ -231,6 +235,7 @@ scriptPath="$(CDPATH='' \cd -- "$(dirname -- "$0")" && pwd -P)" scriptName="$(basename "$0")" logFile="$scriptPath/${scriptName%.*}.log" logToJournal=0 +logDebug=0 useJsonLogFmt=1 useSyslogLogFmt=0 accountFile="$scriptPath/cloudflare.credentials" @@ -280,6 +285,10 @@ while [ $# -gt 0 ]; do # do not log anything logFile="/dev/null" ;; + --log-debug) + # log debugging messages + logDebug=1 + ;; --fmt-json) # use JSON log formatting useJsonLogFmt=1 From 5bc3e7d58725f855f18254293210ae2e4d52ae17 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Fri, 24 Jul 2026 01:18:16 -0600 Subject: [PATCH 46/60] feat(log): Enable debug level filtering --- cfddns.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cfddns.sh b/cfddns.sh index 206b5b0..24c6bff 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -114,9 +114,13 @@ writePlainTextLog() { return fi + PT_LOG_LEVEL="$(standardizeLogLevels "$2")" + + ([ "$logDebug" -eq 0 ] && [ "$PT_LOG_LEVEL" = "DEBUG" ]) && return + printf "%s %s %s[%s]: [%s] [%s] %s (%s:%s)\n" \ "$(getTimeStamp)" "$(getHostname)" "$LOG_PROGRAM_NAME" "$$" \ - "$(standardizeLogLevels "$2")" "$(uppercase "$3")" "$1" "${4:-UNSPECIFIED}" "${5:-0}" \ + "$PT_LOG_LEVEL" "$(uppercase "$3")" "$1" "${4:-UNSPECIFIED}" "${5:-0}" \ >>"$logFile" } From 95682314deab279fdfd11be44771fc53491895a9 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Fri, 24 Jul 2026 01:46:51 -0600 Subject: [PATCH 47/60] fix(params): Fix pre-flight console log dedup --- cfddns.sh | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/cfddns.sh b/cfddns.sh index 24c6bff..809c87f 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -239,6 +239,7 @@ scriptPath="$(CDPATH='' \cd -- "$(dirname -- "$0")" && pwd -P)" scriptName="$(basename "$0")" logFile="$scriptPath/${scriptName%.*}.log" logToJournal=0 +logToConsole=0 logDebug=0 useJsonLogFmt=1 useSyslogLogFmt=0 @@ -280,6 +281,7 @@ while [ $# -gt 0 ]; do --log-console) # use console-based logging logFile="/dev/stdout" + logToConsole=1 ;; --log-journal) # use journald-based logging @@ -355,24 +357,24 @@ done ### pre-flight checks if ! command -v curl >/dev/null; then - printf "\n%sThis script requires 'curl' be installed and accessible. Exiting.%s\n\n" "$err" "$norm" - [ "$logToConsole" -eq 0 ] && writePlainTextLog "'curl' must be installed and accessible" "fatal" "fail" "preexec" 2 + printf "\n%sThis script requires that 'curl' is installed and accessible. Exiting.%s\n\n" "$err" "$norm" + ([ "$logToConsole" -eq 0 ]) && writePlainTextLog "'curl' must be installed and accessible" "fatal" "fail" "preexec" 2 exit 2 fi if ! command -v jq >/dev/null; then - printf "\n%sThis script requires 'jq' be installed and accessible. Exiting.%s\n\n" "$err" "$norm" - [ "$logToConsole" -eq 0 ] && writePlainTextLog "'jq' must be installed and accessible" "fatal" "fail" "preexec" 2 + printf "\n%sThis script requires that 'jq' is installed and accessible. Exiting.%s\n\n" "$err" "$norm" + ([ "$logToConsole" -eq 0 ]) && writePlainTextLog "'jq' must be installed and accessible" "fatal" "fail" "preexec" 2 exit 2 fi if [ -z "$dnsRecords" ]; then badParam errMsg "You must specify at least one DNS record to update. Exiting." - [ "$logToConsole" -eq 0 ] && + ([ "$logToConsole" -eq 0 ]) && writePlainTextLog "At least one DNS record to update must be specified" "fatal" "fail" "params" 1 fi # verify the credentials file exists and is not empty (default check) if [ ! -f "$accountFile" ] || [ ! -s "$accountFile" ]; then badParam errMsg "Cannot find the Cloudflare credentials file (${accountFile}). Exiting." - [ "$logToConsole" -eq 0 ] && + ([ "$logToConsole" -eq 0 ]) && writePlainTextLog "Cannot find the specified Cloudflare credentials file (${accountFile})" "fatal" "fail" "params" 1 fi if [ "$logToJournal" -eq 1 ] && ! command -v logger >/dev/null 2>&1; then From 02e3e8d391869a8195f113dfa21c6be0b2b5ed42 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Fri, 24 Jul 2026 01:59:10 -0600 Subject: [PATCH 48/60] feat(log): Log parameter errors --- cfddns.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cfddns.sh b/cfddns.sh index 809c87f..d6b54a2 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -35,13 +35,19 @@ badParam() { if [ "$1" = "null" ]; then printf "\n%sERROR: '%s' cannot have a NULL (empty) value.\n" "$err" "$2" printf "%sPlease use '--help' for assistance.%s\n\n" "$cyan" "$norm" + ([ "$logToConsole" -eq 0 ]) && writePlainTextLog \ + "${2} cannot have a NULL (empty) value" "critical" "fail" "preexec" 1 exit 1 elif [ "$1" = "dne" ]; then printf "\n%sERROR: '%s %s'\n" "$err" "$2" "$3" printf "file or directory does not exist or is empty.%s\n\n" "$norm" + ([ "$logToConsole" -eq 0 ]) && writePlainTextLog \ + "${3}: file or directory does not exist or is empty" "critical" "fail" "preexec" 1 exit 1 elif [ "$1" = "errMsg" ]; then printf "\n%sERROR: %s%s\n\n" "$err" "$2" "$norm" + ([ "$logToConsole" -eq 0 ]) && writePlainTextLog \ + "$2" "critical" "fail" "preexec" 1 exit 1 fi } @@ -349,6 +355,8 @@ while [ $# -gt 0 ]; do *) printf "\n%sUnknown option: %s\n" "$err" "$1" printf "%sUse '--help' for valid options.%s\n\n" "$cyan" "$norm" + ([ "$logToConsole" -eq 0 ]) && writePlainTextLog \ + "Unknown script parameter: '${1}'" "critical" "fail" "preexec" 1 exit 1 ;; esac From aebf8042e3fda132ae54df2fc62b8789fb7ed1f4 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Fri, 24 Jul 2026 02:15:14 -0600 Subject: [PATCH 49/60] feat(params): Use badparam func to manage logging --- cfddns.sh | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/cfddns.sh b/cfddns.sh index d6b54a2..aeff1ea 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -47,7 +47,7 @@ badParam() { elif [ "$1" = "errMsg" ]; then printf "\n%sERROR: %s%s\n\n" "$err" "$2" "$norm" ([ "$logToConsole" -eq 0 ]) && writePlainTextLog \ - "$2" "critical" "fail" "preexec" 1 + "${2%%\Exit*}" "fatal" "fail" "preexec" 1 exit 1 fi } @@ -376,14 +376,10 @@ if ! command -v jq >/dev/null; then fi if [ -z "$dnsRecords" ]; then badParam errMsg "You must specify at least one DNS record to update. Exiting." - ([ "$logToConsole" -eq 0 ]) && - writePlainTextLog "At least one DNS record to update must be specified" "fatal" "fail" "params" 1 fi # verify the credentials file exists and is not empty (default check) if [ ! -f "$accountFile" ] || [ ! -s "$accountFile" ]; then - badParam errMsg "Cannot find the Cloudflare credentials file (${accountFile}). Exiting." - ([ "$logToConsole" -eq 0 ]) && - writePlainTextLog "Cannot find the specified Cloudflare credentials file (${accountFile})" "fatal" "fail" "params" 1 + badParam errMsg "Cannot find the Cloudflare credentials file (${accountFile}) or it is empty. Exiting." fi if [ "$logToJournal" -eq 1 ] && ! command -v logger >/dev/null 2>&1; then printf "\n%sThis script requires 'logger' be installed to write entries to your journaling system. Exiting.%s\n\n" "$err" "$norm" From e4e967e22741e3a295737f7856d2a54a58a17517 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Fri, 24 Jul 2026 02:16:08 -0600 Subject: [PATCH 50/60] feat(params): Cleanup journald pre-req fail msg --- cfddns.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cfddns.sh b/cfddns.sh index aeff1ea..79bf478 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -382,7 +382,7 @@ if [ ! -f "$accountFile" ] || [ ! -s "$accountFile" ]; then badParam errMsg "Cannot find the Cloudflare credentials file (${accountFile}) or it is empty. Exiting." fi if [ "$logToJournal" -eq 1 ] && ! command -v logger >/dev/null 2>&1; then - printf "\n%sThis script requires 'logger' be installed to write entries to your journaling system. Exiting.%s\n\n" "$err" "$norm" + printf "\n%sThis script requires that 'logger' is installed to write journald entries. Exiting.%s\n\n" "$err" "$norm" exit 2 fi # turn off log file colourization if parameter is set From 1506d77ee9dbc61482860f62d603245c0c679154 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Fri, 24 Jul 2026 02:17:06 -0600 Subject: [PATCH 51/60] refactor(func): Remove no-colourize functions --- cfddns.sh | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/cfddns.sh b/cfddns.sh index 79bf478..050d9ad 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -250,7 +250,6 @@ logDebug=0 useJsonLogFmt=1 useSyslogLogFmt=0 accountFile="$scriptPath/cloudflare.credentials" -colourizeLogFile=1 dnsRecords="" dnsSeparator="," ipAddress="" @@ -385,17 +384,6 @@ if [ "$logToJournal" -eq 1 ] && ! command -v logger >/dev/null 2>&1; then printf "\n%sThis script requires that 'logger' is installed to write journald entries. Exiting.%s\n\n" "$err" "$norm" exit 2 fi -# turn off log file colourization if parameter is set -if [ "$colourizeLogFile" -eq 0 ]; then - bold="" - cyan="" - err="" - magenta="" - norm="" - ok="" - warn="" - yellow="" -fi ### initial log entries writePlainTextLog "starting '${scriptName}'" "info" "ok" "script" From 3b97d112ad79b1a5ab1f3de6d738c7e5f60ae2e6 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Fri, 24 Jul 2026 02:44:22 -0600 Subject: [PATCH 52/60] feat(log): Add CF-ERR to operation attr Update 'listCFErrors' function so that 'CF-ERR' is appended to the 'operation' field of the error log. --- cfddns.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cfddns.sh b/cfddns.sh index 050d9ad..b93a884 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -85,7 +85,7 @@ listCFErrors() { messages="${messages#*_}" # output to log - writePlainTextLog "${message}" "err" "fail" "$2" "$code" + writePlainTextLog "${message}" "err" "fail" "$2:CF-ERR" "$code" done } From 8d3489ba009017a7253a43992fffbff5bfa023ba Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Fri, 24 Jul 2026 02:48:26 -0600 Subject: [PATCH 53/60] fix(vars): Add missing LOG_PROGRAM_NAME var --- cfddns.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/cfddns.sh b/cfddns.sh index b93a884..c1b57c1 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -241,6 +241,7 @@ textBlockSwitches() { } ### default variable values +LOG_PROGRAM_NAME="CFDDNS" scriptPath="$(CDPATH='' \cd -- "$(dirname -- "$0")" && pwd -P)" scriptName="$(basename "$0")" logFile="$scriptPath/${scriptName%.*}.log" From 51bd6160f37c63e9c49f86b31225f49c1d246793 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Fri, 24 Jul 2026 03:25:29 -0600 Subject: [PATCH 54/60] feat(func): Use centralized log func Create single logging function that can be called and then forwards the request to the appropriate logging backend function for completion. --- cfddns.sh | 75 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/cfddns.sh b/cfddns.sh index c1b57c1..d0f571f 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -35,18 +35,18 @@ badParam() { if [ "$1" = "null" ]; then printf "\n%sERROR: '%s' cannot have a NULL (empty) value.\n" "$err" "$2" printf "%sPlease use '--help' for assistance.%s\n\n" "$cyan" "$norm" - ([ "$logToConsole" -eq 0 ]) && writePlainTextLog \ + ([ "$logToConsole" -eq 0 ]) && writeLog \ "${2} cannot have a NULL (empty) value" "critical" "fail" "preexec" 1 exit 1 elif [ "$1" = "dne" ]; then printf "\n%sERROR: '%s %s'\n" "$err" "$2" "$3" printf "file or directory does not exist or is empty.%s\n\n" "$norm" - ([ "$logToConsole" -eq 0 ]) && writePlainTextLog \ + ([ "$logToConsole" -eq 0 ]) && writeLog \ "${3}: file or directory does not exist or is empty" "critical" "fail" "preexec" 1 exit 1 elif [ "$1" = "errMsg" ]; then printf "\n%sERROR: %s%s\n\n" "$err" "$2" "$norm" - ([ "$logToConsole" -eq 0 ]) && writePlainTextLog \ + ([ "$logToConsole" -eq 0 ]) && writeLog \ "${2%%\Exit*}" "fatal" "fail" "preexec" 1 exit 1 fi @@ -85,7 +85,7 @@ listCFErrors() { messages="${messages#*_}" # output to log - writePlainTextLog "${message}" "err" "fail" "$2:CF-ERR" "$code" + writeLog "${message}" "err" "fail" "$2:CF-ERR" "$code" done } @@ -130,6 +130,17 @@ writePlainTextLog() { >>"$logFile" } +writeLog() { + ([ "$logToNone" ]) && return + + PT_LOG_LEVEL="$(standardizeLogLevels "$2")" + ([ "$logDebug" -eq 0 ] && [ "$PT_LOG_LEVEL" = "DEBUG" ]) && return + + ([ "$logToJournal" ]) && return # TODO: replace this with function when implemented + ([ "$useJsonLogFmt" -eq 1 ]) && return # TODO: replace this with function when implemented + ([ "$useSyslogLogFmt" -eq 1 ]) && writePlainTextLog "$@" +} + scriptExamples() { newline printf "Update Cloudflare DNS host A/AAAA records with current IP address.\n" @@ -247,6 +258,7 @@ scriptName="$(basename "$0")" logFile="$scriptPath/${scriptName%.*}.log" logToJournal=0 logToConsole=0 +logToNone=0 logDebug=0 useJsonLogFmt=1 useSyslogLogFmt=0 @@ -296,6 +308,7 @@ while [ $# -gt 0 ]; do --log-none) # do not log anything logFile="/dev/null" + logToNone=1 ;; --log-debug) # log debugging messages @@ -355,7 +368,7 @@ while [ $# -gt 0 ]; do *) printf "\n%sUnknown option: %s\n" "$err" "$1" printf "%sUse '--help' for valid options.%s\n\n" "$cyan" "$norm" - ([ "$logToConsole" -eq 0 ]) && writePlainTextLog \ + ([ "$logToConsole" -eq 0 ]) && writeLog \ "Unknown script parameter: '${1}'" "critical" "fail" "preexec" 1 exit 1 ;; @@ -366,12 +379,12 @@ done ### pre-flight checks if ! command -v curl >/dev/null; then printf "\n%sThis script requires that 'curl' is installed and accessible. Exiting.%s\n\n" "$err" "$norm" - ([ "$logToConsole" -eq 0 ]) && writePlainTextLog "'curl' must be installed and accessible" "fatal" "fail" "preexec" 2 + ([ "$logToConsole" -eq 0 ]) && writeLog "'curl' must be installed and accessible" "fatal" "fail" "preexec" 2 exit 2 fi if ! command -v jq >/dev/null; then printf "\n%sThis script requires that 'jq' is installed and accessible. Exiting.%s\n\n" "$err" "$norm" - ([ "$logToConsole" -eq 0 ]) && writePlainTextLog "'jq' must be installed and accessible" "fatal" "fail" "preexec" 2 + ([ "$logToConsole" -eq 0 ]) && writeLog "'jq' must be installed and accessible" "fatal" "fail" "preexec" 2 exit 2 fi if [ -z "$dnsRecords" ]; then @@ -387,14 +400,14 @@ if [ "$logToJournal" -eq 1 ] && ! command -v logger >/dev/null 2>&1; then fi ### initial log entries -writePlainTextLog "starting '${scriptName}'" "info" "ok" "script" -writePlainTextLog "script path: ${scriptPath}/${scriptName}" "debug" "ok" "startup" -writePlainTextLog "credentials file: ${accountFile}" "debug" "ok" "startup" +writeLog "starting '${scriptName}'" "info" "ok" "script" +writeLog "script path: ${scriptPath}/${scriptName}" "debug" "ok" "startup" +writeLog "credentials file: ${accountFile}" "debug" "ok" "startup" if [ "$ip4" -eq 1 ]; then - writePlainTextLog "mode: IPv4" "debug" "ok" "startup" + writeLog "mode: IPv4" "debug" "ok" "startup" elif [ "$ip6" -eq 1 ]; then - writePlainTextLog "mode: IPv6" "debug" "ok" "startup" + writeLog "mode: IPv6" "debug" "ok" "startup" fi # detect and report IP address @@ -402,21 +415,21 @@ if [ -z "$ipAddress" ]; then # detect public ip address if [ "$ip4" -eq 1 ]; then if ! ipAddress="$(curl -s $ip4DetectionSvc)"; then - writePlainTextLog \ + writeLog \ "Unable to auto-detect this machine's public IP address; try again later or supply the IP address to be used" "err" "fail" "detectip" 10 exit 10 fi fi if [ "$ip6" -eq 1 ]; then if ! ipAddress="$(curl -s $ip6DetectionSvc)"; then - writePlainTextLog \ + writeLog \ "Unable to auto-detect this machine's public IP address; try again later or supply the IP address to be used" "err" "fail" "detectip" 10 exit 10 fi fi - writePlainTextLog "DDNS IP address (detected): $ipAddress" "info" "ok" "startup" + writeLog "DDNS IP address (detected): $ipAddress" "info" "ok" "startup" else - writePlainTextLog "DDNS IP address (supplied): $ipAddress" "info" "ok" "startup" + writeLog "DDNS IP address (supplied): $ipAddress" "info" "ok" "startup" fi # iterate DNS records to update @@ -428,7 +441,7 @@ while [ -n "$dnsRecordsToUpdate" ] && [ "$dnsRecordsToUpdate" != "$dnsSeparator" if [ -z "$record" ]; then continue fi - writePlainTextLog "DNS host record '${record}' queued for update" "info" "ok" "startup" + writeLog "DNS host record '${record}' queued for update" "info" "ok" "startup" done ### read Cloudflare credentials @@ -445,14 +458,14 @@ case "$accountFile" in ;; esac if [ -z "$cfKey" ]; then - writePlainTextLog "Cloudflare authorized API key (cfKey) is either null or undefined; please check your Cloudflare credentials file" "err" "fail" "creds" 21 + writeLog "Cloudflare authorized API key (cfKey) is either null or undefined; please check your Cloudflare credentials file" "err" "fail" "creds" 21 exit 21 fi if [ -z "$cfZoneId" ]; then - writePlainTextLog "Cloudflare zone id (cfZoneId) is either null or undefined; please check your Cloudflare credentials file" "err" "fail" "creds" 22 + writeLog "Cloudflare zone id (cfZoneId) is either null or undefined; please check your Cloudflare credentials file" "err" "fail" "creds" 22 exit 22 fi -writePlainTextLog "Cloudflare credentials file read successfully" "debug" "ok" "creds" +writeLog "Cloudflare credentials file read successfully" "debug" "ok" "creds" ### connect to Cloudflare and do what needs to be done! dnsRecordsToUpdate="$dnsRecords$dnsSeparator" @@ -477,7 +490,7 @@ while [ -n "$dnsRecordsToUpdate" ] && [ "$dnsRecordsToUpdate" != "$dnsSeparator" -H "Authorization: Bearer ${cfKey}" \ -H "Content-Type: application/json" )"; then - writePlainTextLog "Unable to connect to Cloudflare servers; please try again later." "err" "fail" "cflogin" 3 + writeLog "Unable to connect to Cloudflare servers; please try again later." "err" "fail" "cflogin" 3 exit 3 fi @@ -486,7 +499,7 @@ while [ -n "$dnsRecordsToUpdate" ] && [ "$dnsRecordsToUpdate" != "$dnsSeparator" cfSuccess="$(printf "%s" "$cfLookup" | jq -r '.success')" if [ "$cfSuccess" = "false" ]; then listCFErrors "$cfLookup" "cflogin" - writePlainTextLog "Cloudflare API error; review any previously logged 'CF-ERR:' lines for details." "err" "fail" "cflogin" 25 + writeLog "Cloudflare API error; review any previously logged 'CF-ERR:' lines for details." "err" "fail" "cflogin" 25 exit 25 fi @@ -494,18 +507,18 @@ while [ -n "$dnsRecordsToUpdate" ] && [ "$dnsRecordsToUpdate" != "$dnsSeparator" # skip to the next host if an existing host record cannot be found (this script *updates* only, does not create!) if [ "$resultCount" = "0" ]; then - writePlainTextLog "Cannot find an existing record matching '${record}' to update" "warn" "warn" "ddns" + writeLog "Cannot find an existing record matching '${record}' to update" "warn" "warn" "ddns" invalidDomainCount=$((invalidDomainCount + 1)) continue fi objectId=$(printf "%s" "$cfLookup" | jq -r '.result | .[] | .id') currentIpAddr=$(printf "%s" "$cfLookup" | jq -r '.result | .[] | .content') - writePlainTextLog "The current IP address for '${record}' is ${currentIpAddr}" "debug" "ok" "ddns" + writeLog "The current IP address for '${record}' is ${currentIpAddr}" "debug" "ok" "ddns" # skip to next hostname if record already up to date if [ "$currentIpAddr" = "$ipAddress" ]; then - writePlainTextLog "The IP address for '${record}' is already up to date" "info" "ok" "ddns" + writeLog "The IP address for '${record}' is already up to date" "info" "ok" "ddns" continue fi @@ -519,17 +532,17 @@ while [ -n "$dnsRecordsToUpdate" ] && [ "$dnsRecordsToUpdate" != "$dnsSeparator" -H "Content-Type: application/json" \ --data "${updateJSON}" )"; then - writePlainTextLog "Unable to connect to Cloudflare servers; please try again later." "err" "fail" "ddns" 3 + writeLog "Unable to connect to Cloudflare servers; please try again later." "err" "fail" "ddns" 3 exit 3 fi # note update success or failure cfSuccess="$(printf "%s" "$cfResult" | jq '.success')" if [ "$cfSuccess" = "true" ]; then - writePlainTextLog "The IP address for '${record}' successfully updated" "info" "ok" "ddns" + writeLog "The IP address for '${record}' successfully updated" "info" "ok" "ddns" else listCFErrors "$cfResult" "ddns" - writePlainTextLog "Unable to update the IP address for '${record}'" "err" "fail" "ddns" + writeLog "Unable to update the IP address for '${record}'" "err" "fail" "ddns" # do not exit with error, API error here is probably an update issue specific to this host # increment counter and note it after all processing finished failedHostCount=$((failedHostCount + 1)) @@ -538,14 +551,14 @@ done # exit if [ "$invalidDomainCount" -ne 0 ]; then - writePlainTextLog "${invalidDomainCount} invalid host(s) were supplied for updating" "warn" "warn" "ddns" + writeLog "${invalidDomainCount} invalid host(s) were supplied for updating" "warn" "warn" "ddns" fi if [ "$failedHostCount" -ne 0 ]; then - writePlainTextLog \ + writeLog \ "${failedHostCount} host update(s) failed; review 'CF-ERR:' lines in this log to help determine what may have gone wrong" "err" "fail" "ddns" 26 exit 26 else - writePlainTextLog "${scriptName} completed successfully" "info" "ok" "script" + writeLog "${scriptName} completed successfully" "info" "ok" "script" fi ### exit return codes From 58520b3567a6af9dd4d23317f1651ec643ae2f7b Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Fri, 24 Jul 2026 03:27:44 -0600 Subject: [PATCH 55/60] refactor(log): Remove redundant debug log handling Debug-level logging is handled by the central logging steering function. Remove redundant code in backend plaint-text logging function. --- cfddns.sh | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cfddns.sh b/cfddns.sh index d0f571f..d4cb5a5 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -120,13 +120,9 @@ writePlainTextLog() { return fi - PT_LOG_LEVEL="$(standardizeLogLevels "$2")" - - ([ "$logDebug" -eq 0 ] && [ "$PT_LOG_LEVEL" = "DEBUG" ]) && return - printf "%s %s %s[%s]: [%s] [%s] %s (%s:%s)\n" \ "$(getTimeStamp)" "$(getHostname)" "$LOG_PROGRAM_NAME" "$$" \ - "$PT_LOG_LEVEL" "$(uppercase "$3")" "$1" "${4:-UNSPECIFIED}" "${5:-0}" \ + "$(standardizeLogLevels "$2")" "$(uppercase "$3")" "$1" "${4:-UNSPECIFIED}" "${5:-0}" \ >>"$logFile" } From 539b80e4ec8622882856fff7669ec379e0eda1c7 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Fri, 24 Jul 2026 03:52:30 -0600 Subject: [PATCH 56/60] feat(script): Use HTTPS ip discovery links --- cfddns.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cfddns.sh b/cfddns.sh index d4cb5a5..2dcb284 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -264,8 +264,8 @@ dnsSeparator="," ipAddress="" ip4=1 ip6=0 -ip4DetectionSvc="http://ipv4.icanhazip.com" -ip6DetectionSvc="http://ipv6.icanhazip.com" +ip4DetectionSvc="https://ipv4.icanhazip.com" +ip6DetectionSvc="https://ipv6.icanhazip.com" invalidDomainCount=0 failedHostCount=0 From bff59f5c1f2ee1e7fa1fff7e69479c1a1a2256b2 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Fri, 24 Jul 2026 04:01:35 -0600 Subject: [PATCH 57/60] chore(ide): Save custom dictionary --- Folder.DotSettings | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Folder.DotSettings diff --git a/Folder.DotSettings b/Folder.DotSettings new file mode 100644 index 0000000..5953caf --- /dev/null +++ b/Folder.DotSettings @@ -0,0 +1,4 @@ + + True + True + True \ No newline at end of file From e25bd9fb4d93ac4333a6a43a8aa866beb12a6c4c Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Fri, 24 Jul 2026 04:02:21 -0600 Subject: [PATCH 58/60] feat(func): Add JSON logging func Update central logging steering function to reference this new JSON logging backend function. --- cfddns.sh | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/cfddns.sh b/cfddns.sh index 2dcb284..c43ba68 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -113,6 +113,31 @@ standardizeLogLevels() { esac } +# $1: message, $2: level, $3: status, [$4: operation], [$5: code] +writeJsonLog() { + # not enough information to generate a meaningful log message + if [ -z "$1" ] || [ $# -lt 3 ]; then + return + fi + + JSON_PROPS="$( + jq --null-input \ + --arg timestamp "$(getTimeStamp)" \ + --arg hostname "$(getHostname)" \ + --arg pName "$LOG_PROGRAM_NAME" \ + --arg pid "$$" \ + --arg level "$(standardizeLogLevels "$2")" \ + --arg status "$(uppercase "$3")" \ + --arg message "$1" \ + --arg operation "${4:-UNSPECIFIED}" \ + --arg code "${5:-0}" \ + '{"timestamp":$timestamp, "hostname":$hostname, "programName":$pName, "pid":$pid, "level":$level, "status":$status, "message":$message, "operation":$operation, "code":$code}' \ + )" + + printf "%s\n" "$JSON_PROPS" >>"$logFile" + return +} + # modified syslog fmt: [pid]: [LEVEL:$2] [STATUS:$3] (:) writePlainTextLog() { # not enough information to generate a meaningful log message @@ -127,13 +152,13 @@ writePlainTextLog() { } writeLog() { - ([ "$logToNone" ]) && return + ([ "$logToNone" -eq 1 ]) && return PT_LOG_LEVEL="$(standardizeLogLevels "$2")" ([ "$logDebug" -eq 0 ] && [ "$PT_LOG_LEVEL" = "DEBUG" ]) && return - ([ "$logToJournal" ]) && return # TODO: replace this with function when implemented - ([ "$useJsonLogFmt" -eq 1 ]) && return # TODO: replace this with function when implemented + ([ "$logToJournal" -eq 1 ]) && return # TODO: replace this with function when implemented + ([ "$useJsonLogFmt" -eq 1 ]) && writeJsonLog "$@" ([ "$useSyslogLogFmt" -eq 1 ]) && writePlainTextLog "$@" } From eafba0d67dfe5e791ddf54ff463ecd9f744fa5b1 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Fri, 24 Jul 2026 04:36:26 -0600 Subject: [PATCH 59/60] perf(log): Use compact JSON for logging --- cfddns.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cfddns.sh b/cfddns.sh index c43ba68..0419c4c 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -121,7 +121,7 @@ writeJsonLog() { fi JSON_PROPS="$( - jq --null-input \ + jq --null-input -c \ --arg timestamp "$(getTimeStamp)" \ --arg hostname "$(getHostname)" \ --arg pName "$LOG_PROGRAM_NAME" \ From 08a5c823d5bc4b6b6268315321b31d508fd076e2 Mon Sep 17 00:00:00 2001 From: Asif Bacchus Date: Fri, 24 Jul 2026 04:41:23 -0600 Subject: [PATCH 60/60] refactor(log): Use CLEF for JSON logs Use Compact Log Event Format (CLEF) for JSON logs along with custom properties. --- cfddns.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cfddns.sh b/cfddns.sh index 0419c4c..b849d70 100644 --- a/cfddns.sh +++ b/cfddns.sh @@ -131,7 +131,7 @@ writeJsonLog() { --arg message "$1" \ --arg operation "${4:-UNSPECIFIED}" \ --arg code "${5:-0}" \ - '{"timestamp":$timestamp, "hostname":$hostname, "programName":$pName, "pid":$pid, "level":$level, "status":$status, "message":$message, "operation":$operation, "code":$code}' \ + '{"@t":$timestamp, "@m":$message, "@l":$level, "hostname":$hostname, "programName":$pName, "pid":$pid, "status":$status, "operation":$operation, "code":$code}' \ )" printf "%s\n" "$JSON_PROPS" >>"$logFile"