9 Commits

Author SHA1 Message Date
asif 4c997a6b7d feature [logger] add status logging param 2026-07-22 18:34:14 -06:00
asif 1824d9f139 feat(logger): Framework test file 2026-07-22 17:33:46 -06:00
asif 50b4cf105f chore(ide): Update IDE settings 2026-07-22 17:33:36 -06:00
asif 1e3b545a23 chore(ide): JetBrains IDE configuration files 2026-07-19 23:37:13 -06:00
asif 380509c966 chore(git): Update git control files 2026-07-19 23:36:58 -06:00
asif 71da487aa8 style(fmt): Reformat shell script 2026-07-19 23:33:56 -06:00
asif 3be6eaee0c [preflight] Add check for logger dependency
'logger' is required to facilitate logging to the journaling system.
2026-07-19 18:52:34 -06:00
asif 5c6c8ec4ea [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.
2026-07-19 18:49:34 -06:00
asif c7297a512c [version] Update version, modified date 2026-07-19 15:44:40 -06:00
10 changed files with 668 additions and 406 deletions
+19
View File
@@ -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
@@ -79,3 +96,5 @@
.gitattributes export-ignore
.gitignore export-ignore
.gitkeep export-ignore
.idea export-ignore
.vscode export-ignore
+34
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeveloperToolsToolWindowSettingsV1" lastSelectedContentNodeId="base64-encoder-decoder" pluginVersion="9.0.0">
<developerToolsConfigurations />
</component>
</project>
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxProjectSettings">
<option name="commitMessageIssueKeyValidationOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
<option name="commitMessageValidationEnabledOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="com.itcodebox.notebooks.projectservice.ProjectUIState">
<option name="selectedNotebookId" value="1" />
<option name="selectedChapterId" value="1" />
<option name="selectedNoteId" value="1" />
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RiderProjectSettingsUpdater">
<option name="singleClickDiffPreview" value="1" />
<option name="unhandledExceptionsIgnoreList" value="1" />
<option name="vcsConfiguration" value="3" />
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+34 -12
View File
@@ -3,8 +3,8 @@
#
# update Cloudflare DNS records with current (dynamic) IP address
# Script by Asif Bacchus <asif@bacchus.cloud>
# Last modified: May 10, 2021
# Version 2.2
# Last modified: July 19, 2026
# Version 3.0
#
### text formatting presets using tput
@@ -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"
@@ -364,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=""
@@ -378,6 +391,7 @@ 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"
@@ -415,7 +429,9 @@ fi
record="${dnsRecordsToUpdate%%${dnsSeparator}*}"
dnsRecordsToUpdate="${dnsRecordsToUpdate#*${dnsSeparator}}"
if [ -z "$record" ]; then continue; fi
if [ -z "$record" ]; then
continue
fi
printf "updating record: %s\n" "$record"
done
@@ -426,13 +442,13 @@ fi
writeLog process "Reading Cloudflare credentials"
case "$accountFile" in
/*)
# absolute path, use as-is
# shellcheck source=./cloudflare.credentials
# absolute path, use as-is
# shellcheck source=./cloudflare.credentials
. "$accountFile"
;;
*)
# relative path, rewrite
# shellcheck source=./cloudflare.credentials
# relative path, rewrite
# shellcheck source=./cloudflare.credentials
. "./$accountFile"
;;
esac
@@ -459,13 +475,17 @@ while [ -n "$dnsRecordsToUpdate" ] && [ "$dnsRecordsToUpdate" != "$dnsSeparator"
record="${dnsRecordsToUpdate%%${dnsSeparator}*}"
dnsRecordsToUpdate="${dnsRecordsToUpdate#*${dnsSeparator}}"
if [ -z "$record" ]; then continue; fi
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}" \
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
-H "Content-Type: application/json"
)"; then
writeLog process-error
exitError 3
fi
@@ -506,10 +526,12 @@ while [ -n "$dnsRecordsToUpdate" ] && [ "$dnsRecordsToUpdate" != "$dnsSeparator"
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}" \
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
--data "${updateJSON}"
)"; then
writeLog process-error
exitError 3
fi
+136
View File
@@ -0,0 +1,136 @@
#!/bin/sh
getTimeStamp() {
(date +%F" "%T)
}
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="$(
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" \
'{"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 message "$1" \
'{"level":$level, "programName":$pName, "status":$status, "operation":$operation, "code":$code, "message":$message}' \
)"
fi
echo "$PROPS" | jq "."
return
}
# $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
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 <<end
MESSAGE_ID="$(systemd-id128 new)"
SYSLOG_TIMESTAMP="$(getTimeStamp)"
SYSLOG_FACILITY=3
SYSLOG_IDENTIFIER="${PROGRAM_NAME}"
PRIORITY=4
ERRNO="${DEFAULT_CODE}"
OPERATION="${DEFAULT_OPERATION}"
MESSAGE="${DEFAULT_MESSAGE}"
STATUS="${DEFAULT_STATUS}"
end
else
logger --journald <<end
MESSAGE_ID="$(systemd-id128 new)"
SYSLOG_TIMESTAMP="$(getTimeStamp)"
SYSLOG_FACILITY=3
SYSLOG_IDENTIFIER="${PROGRAM_NAME}"
PRIORITY="${LOG_LEVEL}"
ERRNO="${5:-$DEFAULT_CODE}"
OPERATION="${4:-$DEFAULT_OPERATION}"
MESSAGE="$1"
STATUS="${3:-$DEFAULT_STATUS}"
end
fi
return
}
if [ "$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
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
else
printf "\nPlease specify either 'json' or 'journal' after the script name. Exiting.\n\n"
exit 1
fi
exit 0