Repository: favonia/cloudflare-ddns
Pull request #272 (2022-11-16 07:38:43)

feat(monitor): improve Healthchecks integration
1. Explicitly document that any instance of Healthchecks is supported
2. Warn about HTTP connections
3. Use POST method to work with the POST-only mode
4. Send summary for IP updating, record cleaning, and failures

codecov[bot]: # [Codecov](https://codecov.io/gh/favonia/cloudflare-ddns/pull/272?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) Report
> Merging [#272](https://codecov.io/gh/favonia/cloudflare-ddns/pull/272?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) (0987dbe) into [main](https://codecov.io/gh/favonia/cloudflare-ddns/commit/183921d1f3640b57d345938f235a404f41d17f85?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) (183921d) will **decrease** coverage by `0.08%`.
> The diff coverage is `66.66%`.

> :exclamation: Current head 0987dbe differs from pull request most recent head fccfa4b. Consider uploading reports for the commit fccfa4b to get more accurate results

```diff
@@            Coverage Diff             @@
##             main     #272      +/-   ##
==========================================
- Coverage   85.39%   85.31%   -0.09%     
==========================================
  Files          29       29              
  Lines        1951     1981      +30     
==========================================
+ Hits         1666     1690      +24     
- Misses        274      280       +6     
  Partials       11       11              
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `85.31% <66.66%> (-0.09%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/favonia/cloudflare-ddns/pull/272?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) | Coverage Δ | |
|---|---|---|
| [cmd/ddns.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/272/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-Y21kL2RkbnMuZ28=) | `0.00% <0.00%> (ø)` | |
| [internal/monitor/util.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/272/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-aW50ZXJuYWwvbW9uaXRvci91dGlsLmdv) | `80.00% <66.66%> (-20.00%)` | :arrow_down: |
| [internal/monitor/healthchecks.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/272/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-aW50ZXJuYWwvbW9uaXRvci9oZWFsdGhjaGVja3MuZ28=) | `83.16% <80.00%> (-3.79%)` | :arrow_down: |
| [internal/config/config.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/272/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-aW50ZXJuYWwvY29uZmlnL2NvbmZpZy5nbw==) | `98.76% <100.00%> (ø)` | |
| [internal/config/env.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/272/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-aW50ZXJuYWwvY29uZmlnL2Vudi5nbw==) | `98.61% <100.00%> (ø)` | |
| [internal/setter/setter.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/272/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-aW50ZXJuYWwvc2V0dGVyL3NldHRlci5nbw==) | `100.00% <100.00%> (ø)` | |
| [internal/updater/updater.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/272/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-aW50ZXJuYWwvdXBkYXRlci91cGRhdGVyLmdv) | `100.00% <100.00%> (ø)` | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia)


Patch:
diff --git a/README.markdown b/README.markdown
index 0ba90c5..52b87aa 100644
--- a/README.markdown
+++ b/README.markdown
@@ -45,7 +45,7 @@ By default, public IP addresses are obtained using the [Cloudflare debugging pag
 
 - 🛑 The superuser privileges are immediately dropped after the updater starts. This minimizes the impact of undiscovered security bugs in the updater.
 - 🛡️ The updater uses HTTPS (or [DNS over HTTPS](https://en.wikipedia.org/wiki/DNS_over_HTTPS)) to detect public IP addresses, making it harder to tamper with the detection process. _(Due to the nature of address detection, it is impossible to protect the updater from an adversary who can modify the source IP address of the IP packets coming from your machine.)_
-- 🖥️ Optionally, you can [monitor the updater via Healthchecks.io](https://healthchecks.io), which will notify you when the updating fails.
+- 🖥️ Optionally, you can [monitor the updater via Healthchecks](https://healthchecks.io), which will notify you when the updating fails.
 - 📚 The updater uses only established open-source Go libraries.
   <details><summary>🔌 Full list of external Go libraries <em>(click to expand)</em></summary>
 
@@ -433,12 +433,12 @@ In most cases, `CF_ACCOUNT_ID` is not needed.
 <details>
 <summary>👁️ Monitoring the updater</summary>
 
-| Name           | Valid Values                                                                                                                                                         | Meaning                                                                         | Required? | Default Value |
-| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | --------- | ------------- |
-| `QUIET`        | Boolean values, such as `true`, `false`, `0` and `1`. See [strconv.ParseBool](https://pkg.go.dev/strconv#ParseBool)                                                  | Whether the updater should reduce the logging to the standard output            | No        | `false`       |
-| `HEALTHCHECKS` | [Healthchecks.io ping URLs](https://healthchecks.io/docs/), such as `https://hc-ping.com/<uuid>` or `https://hc-ping.com/<project-ping-key>/<name-slug>` (see below) | If set, the updater will ping the URL when it successfully updates IP addresses | No        | (unset)       |
+| Name           | Valid Values                                                                                                                                                      | Meaning                                                                         | Required? | Default Value |
+| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | --------- | ------------- |
+| `QUIET`        | Boolean values, such as `true`, `false`, `0` and `1`. See [strconv.ParseBool](https://pkg.go.dev/strconv#ParseBool)                                               | Whether the updater should reduce the logging to the standard output            | No        | `false`       |
+| `HEALTHCHECKS` | [Healthchecks ping URLs](https://healthchecks.io/docs/), such as `https://hc-ping.com/<uuid>` or `https://hc-ping.com/<project-ping-key>/<name-slug>` (see below) | If set, the updater will ping the URL when it successfully updates IP addresses | No        | (unset)       |
 
-For `HEALTHCHECKS`, the updater accepts any URL that follows the [same notification protocol](https://healthchecks.io/docs/http_api/).
+For `HEALTHCHECKS`, the updater can work with any server following the [same notification protocol](https://healthchecks.io/docs/http_api/), including but not limited to self-hosted instances of [Healthchecks](https://github.com/healthchecks/healthchecks). Both UUID and Slug URLs are supported, and the updater is compatible with POST-only mode.
 
 </details>
 
diff --git a/cmd/ddns.go b/cmd/ddns.go
index f1466cd..6bd77ef 100644
--- a/cmd/ddns.go
+++ b/cmd/ddns.go
@@ -2,6 +2,7 @@ package main
 
 import (
 	"context"
+	"fmt"
 	"os"
 	"os/signal"
 	"syscall"
@@ -33,12 +34,11 @@ func signalWait(signal chan os.Signal, d time.Duration) (os.Signal, bool) {
 
 var Version string //nolint:gochecknoglobals
 
-func welcome(ppfmt pp.PP) {
+func formatName() string {
 	if Version == "" {
-		ppfmt.Noticef(pp.EmojiStar, "Cloudflare DDNS")
-	} else {
-		ppfmt.Noticef(pp.EmojiStar, "Cloudflare DDNS (%s)", Version)
+		return "Cloudflare DDNS"
 	}
+	return fmt.Sprintf("Cloudflare DDNS (%s)", Version)
 }
 
 func initConfig(ctx context.Context, ppfmt pp.PP) (*config.Config, api.Handle, setter.Setter) {
@@ -46,10 +46,10 @@ func initConfig(ctx context.Context, ppfmt pp.PP) (*config.Config, api.Handle, s
 	bye := func() {
 		// Usually, this is called only after initConfig,
 		// but we are exiting early.
-		monitor.StartAll(ctx, ppfmt, c.Monitors)
+		monitor.StartAll(ctx, ppfmt, c.Monitors, formatName())
 
 		ppfmt.Noticef(pp.EmojiBye, "Bye!")
-		monitor.ExitStatusAll(ctx, ppfmt, c.Monitors, 1)
+		monitor.ExitStatusAll(ctx, ppfmt, c.Monitors, 1, "configuration errors")
 		os.Exit(1)
 	}
 
@@ -76,6 +76,17 @@ func initConfig(ctx context.Context, ppfmt pp.PP) (*config.Config, api.Handle, s
 	return c, h, s
 }
 
+func stopUpdating(ctx context.Context, ppfmt pp.PP, c *config.Config, s setter.Setter) {
+	if c.DeleteOnStop {
+		ppfmt.Noticef(pp.EmojiClearRecord, "Deleting all managed records . . .")
+		if ok, msg := updater.ClearIPs(ctx, ppfmt, c, s); ok {
+			monitor.LogAll(ctx, ppfmt, c.Monitors, msg)
+		} else {
+			monitor.FailureAll(ctx, ppfmt, c.Monitors, msg)
+		}
+	}
+}
+
 func main() { //nolint:funlen
 	ppfmt := pp.New(os.Stdout)
 	if !config.ReadQuiet("QUIET", &ppfmt) {
@@ -86,7 +97,7 @@ func main() { //nolint:funlen
 		ppfmt.Noticef(pp.EmojiMute, "Quiet mode enabled")
 	}
 
-	welcome(ppfmt)
+	ppfmt.Noticef(pp.EmojiStar, formatName())
 
 	// Drop the superuser privilege
 	dropPriviledges(ppfmt)
@@ -105,7 +116,7 @@ func main() { //nolint:funlen
 	c, h, s := initConfig(ctx, ppfmt)
 
 	// Start the tool now
-	monitor.StartAll(ctx, ppfmt, c.Monitors)
+	monitor.StartAll(ctx, ppfmt, c.Monitors, formatName())
 
 	first := true
 mainLoop:
@@ -116,30 +127,22 @@ mainLoop:
 
 		// Update the IP
 		if !first || c.UpdateOnStart {
-			if updater.UpdateIPs(ctx, ppfmt, c, s) {
-				monitor.SuccessAll(ctx, ppfmt, c.Monitors)
+			if ok, msg := updater.UpdateIPs(ctx, ppfmt, c, s); ok {
+				monitor.SuccessAll(ctx, ppfmt, c.Monitors, msg)
 			} else {
-				monitor.FailureAll(ctx, ppfmt, c.Monitors)
+				monitor.FailureAll(ctx, ppfmt, c.Monitors, msg)
 			}
 		} else {
-			monitor.SuccessAll(ctx, ppfmt, c.Monitors)
+			monitor.SuccessAll(ctx, ppfmt, c.Monitors, "")
 		}
 		first = false
 
 		// Maybe there's nothing scheduled in near future?
 		if next.IsZero() {
-			if c.DeleteOnStop {
-				ppfmt.Errorf(pp.EmojiUserError, "No scheduled updates in near future. Deleting all managed records . . .")
-				if !updater.ClearIPs(ctx, ppfmt, c, s) {
-					monitor.FailureAll(ctx, ppfmt, c.Monitors)
-				}
-				ppfmt.Noticef(pp.EmojiBye, "Done now. Bye!")
-			} else {
-				ppfmt.Errorf(pp.EmojiUserError, "No scheduled updates in near future")
-				ppfmt.Noticef(pp.EmojiBye, "Bye!")
-			}
-
-			monitor.ExitStatusAll(ctx, ppfmt, c.Monitors, 0)
+			ppfmt.Errorf(pp.EmojiUserError, "No scheduled updates in near future")
+			stopUpdating(ctx, ppfmt, c, s)
+			ppfmt.Noticef(pp.EmojiBye, "Bye!")
+			monitor.ExitStatusAll(ctx, ppfmt, c.Monitors, 0, "Not scheduled updates")
 			break mainLoop
 		}
 
@@ -170,21 +173,14 @@ mainLoop:
 
 			ppfmt.Noticef(pp.EmojiRepeatOnce, "Restarting . . .")
 			c, h, s = initConfig(ctx, ppfmt)
+			monitor.LogAll(ctx, ppfmt, c.Monitors, "Restarted")
 			continue mainLoop
 
 		case syscall.SIGINT, syscall.SIGTERM:
-			if c.DeleteOnStop {
-				ppfmt.Noticef(pp.EmojiSignal, "Caught signal: %v. Deleting all managed records . . .", sig)
-				if !updater.ClearIPs(ctx, ppfmt, c, s) {
-					monitor.FailureAll(ctx, ppfmt, c.Monitors)
-				}
-				ppfmt.Noticef(pp.EmojiBye, "Done now. Bye!")
-			} else {
-				ppfmt.Noticef(pp.EmojiSignal, "Caught signal: %v", sig)
-				ppfmt.Noticef(pp.EmojiBye, "Bye!")
-			}
-
-			monitor.ExitStatusAll(ctx, ppfmt, c.Monitors, 0)
+			ppfmt.Noticef(pp.EmojiSignal, "Caught signal: %v", sig)
+			stopUpdating(ctx, ppfmt, c, s)
+			ppfmt.Noticef(pp.EmojiBye, "Bye!")
+			monitor.ExitStatusAll(ctx, ppfmt, c.Monitors, 0, fmt.Sprintf("Signal: %v", sig))
 			break mainLoop
 
 		default:
diff --git a/internal/README.markdown b/internal/README.markdown
index 664b0a6..b3020ae 100644
--- a/internal/README.markdown
+++ b/internal/README.markdown
@@ -4,10 +4,10 @@
 - `config`: read configuration settings from environment variables
 - `cron`: parse Cron expressions
 - `domain`: handle domain names and split them into possible subdomains and zones
-- `domainexp`: handle domain lists and parse boolean expressions on domains (for `PROXIED`)
-- `file`: virtualize file system (to enable testing)
+- `domainexp`: parse domain lists and parse boolean expressions on domains (for `PROXIED`)
+- `file`: virtualize file systems (to enable testing)
 - `ipnet`: define a type for labelling IPv4 and IPv6
-- `monitor`: ping the monitoring API, currently only supporting Healthchecks.io
+- `monitor`: ping the monitoring API, currently only supporting Healthchecks
 - `pp`: pretty print messages with emojis
 - `provider`: find out the public IP
 - `setter`: set the IP of one domain using a DNS service API
diff --git a/internal/config/config.go b/internal/config/config.go
index 25107a1..71f1269 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -264,7 +264,7 @@ func (c *Config) ReadEnv(ppfmt pp.PP) bool {
 		!ReadString(ppfmt, "PROXIED", &c.ProxiedTemplate) ||
 		!ReadNonnegDuration(ppfmt, "DETECTION_TIMEOUT", &c.DetectionTimeout) ||
 		!ReadNonnegDuration(ppfmt, "UPDATE_TIMEOUT", &c.UpdateTimeout) ||
-		!ReadHealthChecksURL(ppfmt, "HEALTHCHECKS", &c.Monitors) {
+		!ReadHealthchecksURL(ppfmt, "HEALTHCHECKS", &c.Monitors) {
 		return false
 	}
 
diff --git a/internal/config/env.go b/internal/config/env.go
index 0e87d43..f62844d 100644
--- a/internal/config/env.go
+++ b/internal/config/env.go
@@ -282,15 +282,15 @@ func ReadCron(ppfmt pp.PP, key string, field *cron.Schedule) bool {
 	return true
 }
 
-// ReadHealthChecksURL reads the base URL of the healthcheck.io endpoint.
-func ReadHealthChecksURL(ppfmt pp.PP, key string, field *[]monitor.Monitor) bool {
+// ReadHealthchecksURL reads the base URL of the healthcheck.io endpoint.
+func ReadHealthchecksURL(ppfmt pp.PP, key string, field *[]monitor.Monitor) bool {
 	val := Getenv(key)
 
 	if val == "" {
 		return true
 	}
 
-	h, ok := monitor.NewHealthChecks(ppfmt, val)
+	h, ok := monitor.NewHealthchecks(ppfmt, val)
 	if !ok {
 		return false
 	}
diff --git a/internal/mocks/mock_monitor.go b/internal/mocks/mock_monitor.go
index 7cd1b41..07cb726 100644
--- a/internal/mocks/mock_monitor.go
+++ b/internal/mocks/mock_monitor.go
@@ -50,57 +50,71 @@ func (mr *MockMonitorMockRecorder) DescribeService() *gomock.Call {
 }
 
 // ExitStatus mocks base method.
-func (m *MockMonitor) ExitStatus(arg0 context.Context, arg1 pp.PP, arg2 int) bool {
+func (m *MockMonitor) ExitStatus(arg0 context.Context, arg1 pp.PP, arg2 int, arg3 string) bool {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "ExitStatus", arg0, arg1, arg2)
+	ret := m.ctrl.Call(m, "ExitStatus", arg0, arg1, arg2, arg3)
 	ret0, _ := ret[0].(bool)
 	return ret0
 }
 
 // ExitStatus indicates an expected call of ExitStatus.
-func (mr *MockMonitorMockRecorder) ExitStatus(arg0, arg1, arg2 interface{}) *gomock.Call {
+func (mr *MockMonitorMockRecorder) ExitStatus(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExitStatus", reflect.TypeOf((*MockMonitor)(nil).ExitStatus), arg0, arg1, arg2)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExitStatus", reflect.TypeOf((*MockMonitor)(nil).ExitStatus), arg0, arg1, arg2, arg3)
 }
 
 // Failure mocks base method.
-func (m *MockMonitor) Failure(arg0 context.Context, arg1 pp.PP) bool {
+func (m *MockMonitor) Failure(arg0 context.Context, arg1 pp.PP, arg2 string) bool {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Failure", arg0, arg1)
+	ret := m.ctrl.Call(m, "Failure", arg0, arg1, arg2)
 	ret0, _ := ret[0].(bool)
 	return ret0
 }
 
 // Failure indicates an expected call of Failure.
-func (mr *MockMonitorMockRecorder) Failure(arg0, arg1 interface{}) *gomock.Call {
+func (mr *MockMonitorMockRecorder) Failure(arg0, arg1, arg2 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Failure", reflect.TypeOf((*MockMonitor)(nil).Failure), arg0, arg1)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Failure", reflect.TypeOf((*MockMonitor)(nil).Failure), arg0, arg1, arg2)
+}
+
+// Log mocks base method.
+func (m *MockMonitor) Log(arg0 context.Context, arg1 pp.PP, arg2 string) bool {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Log", arg0, arg1, arg2)
+	ret0, _ := ret[0].(bool)
+	return ret0
+}
+
+// Log indicates an expected call of Log.
+func (mr *MockMonitorMockRecorder) Log(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Log", reflect.TypeOf((*MockMonitor)(nil).Log), arg0, arg1, arg2)
 }
 
 // Start mocks base method.
-func (m *MockMonitor) Start(arg0 context.Context, arg1 pp.PP) bool {
+func (m *MockMonitor) Start(arg0 context.Context, arg1 pp.PP, arg2 string) bool {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Start", arg0, arg1)
+	ret := m.ctrl.Call(m, "Start", arg0, arg1, arg2)
 	ret0, _ := ret[0].(bool)
 	return ret0
 }
 
 // Start indicates an expected call of Start.
-func (mr *MockMonitorMockRecorder) Start(arg0, arg1 interface{}) *gomock.Call {
+func (mr *MockMonitorMockRecorder) Start(arg0, arg1, arg2 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockMonitor)(nil).Start), arg0, arg1)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockMonitor)(nil).Start), arg0, arg1, arg2)
 }
 
 // Success mocks base method.
-func (m *MockMonitor) Success(arg0 context.Context, arg1 pp.PP) bool {
+func (m *MockMonitor) Success(arg0 context.Context, arg1 pp.PP, arg2 string) bool {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Success", arg0, arg1)
+	ret := m.ctrl.Call(m, "Success", arg0, arg1, arg2)
 	ret0, _ := ret[0].(bool)
 	return ret0
 }
 
 // Success indicates an expected call of Success.
-func (mr *MockMonitorMockRecorder) Success(arg0, arg1 interface{}) *gomock.Call {
+func (mr *MockMonitorMockRecorder) Success(arg0, arg1, arg2 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Success", reflect.TypeOf((*MockMonitor)(nil).Success), arg0, arg1)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Success", reflect.TypeOf((*MockMonitor)(nil).Success), arg0, arg1, arg2)
 }
diff --git a/internal/mocks/mock_setter.go b/internal/mocks/mock_setter.go
index cdf5e53..67c271f 100644
--- a/internal/mocks/mock_setter.go
+++ b/internal/mocks/mock_setter.go
@@ -40,11 +40,12 @@ func (m *MockSetter) EXPECT() *MockSetterMockRecorder {
 }
 
 // Set mocks base method.
-func (m *MockSetter) Set(arg0 context.Context, arg1 pp.PP, arg2 domain.Domain, arg3 ipnet.Type, arg4 netip.Addr, arg5 api.TTL, arg6 bool) bool {
+func (m *MockSetter) Set(arg0 context.Context, arg1 pp.PP, arg2 domain.Domain, arg3 ipnet.Type, arg4 netip.Addr, arg5 api.TTL, arg6 bool) (bool, string) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Set", arg0, arg1, arg2, arg3, arg4, arg5, arg6)
 	ret0, _ := ret[0].(bool)
-	return ret0
+	ret1, _ := ret[1].(string)
+	return ret0, ret1
 }
 
 // Set indicates an expected call of Set.
diff --git a/internal/monitor/base.go b/internal/monitor/base.go
index 6a8dc3b..33b8a95 100644
--- a/internal/monitor/base.go
+++ b/internal/monitor/base.go
@@ -10,8 +10,9 @@ import (
 
 type Monitor interface {
 	DescribeService() string
-	Success(context.Context, pp.PP) bool
-	Start(context.Context, pp.PP) bool
-	Failure(context.Context, pp.PP) bool
-	ExitStatus(context.Context, pp.PP, int) bool
+	Success(context.Context, pp.PP, string) bool
+	Start(context.Context, pp.PP, string) bool
+	Failure(context.Context, pp.PP, string) bool
+	Log(context.Context, pp.PP, string) bool
+	ExitStatus(context.Context, pp.PP, int, string) bool
 }
diff --git a/internal/monitor/healthchecks.go b/internal/monitor/healthchecks.go
index 6940e54..cfc5d8f 100644
--- a/internal/monitor/healthchecks.go
+++ b/internal/monitor/healthchecks.go
@@ -13,45 +13,58 @@ import (
 	"github.com/favonia/cloudflare-ddns/internal/pp"
 )
 
-type HealthChecks struct {
+type Healthchecks struct {
 	BaseURL    *url.URL
 	Timeout    time.Duration
 	MaxRetries int
 }
 
 const (
-	HealthChecksDefaultTimeout    = 10 * time.Second
-	HealthChecksDefaultMaxRetries = 5
+	HealthchecksDefaultTimeout    = 10 * time.Second
+	HealthchecksDefaultMaxRetries = 5
 )
 
-type HealthChecksOption func(*HealthChecks)
+type HealthchecksOption func(*Healthchecks)
 
-func SetHealthChecksMaxRetries(maxRetries int) HealthChecksOption {
+func SetHealthchecksMaxRetries(maxRetries int) HealthchecksOption {
 	if maxRetries <= 0 {
 		panic("maxRetries <= 0")
 	}
-	return func(h *HealthChecks) {
+	return func(h *Healthchecks) {
 		h.MaxRetries = maxRetries
 	}
 }
 
-func NewHealthChecks(ppfmt pp.PP, rawURL string, os ...HealthChecksOption) (Monitor, bool) {
+func NewHealthchecks(ppfmt pp.PP, rawURL string, os ...HealthchecksOption) (Monitor, bool) {
 	url, err := url.Parse(rawURL)
 	if err != nil {
-		ppfmt.Errorf(pp.EmojiUserError, "Failed to parse the Healthchecks.io URL (redacted)")
+		ppfmt.Errorf(pp.EmojiUserError, "Failed to parse the Healthchecks URL (redacted)")
 		return nil, false
 	}
 
 	if !(url.IsAbs() && url.Opaque == "" && url.Host != "") {
-		ppfmt.Errorf(pp.EmojiUserError, `The Healthchecks.io URL (redacted) does not look like a valid URL.`)
+		ppfmt.Errorf(pp.EmojiUserError, `The Healthchecks URL (redacted) does not look like a valid URL.`)
 		ppfmt.Errorf(pp.EmojiUserError, `A valid example is "https://hc-ping.com/01234567-0123-0123-0123-0123456789abc".`)
 		return nil, false
 	}
 
-	h := &HealthChecks{
+	switch url.Scheme {
+	case "http":
+		ppfmt.Warningf(pp.EmojiUserWarning, "The Healthchecks URL (redacted) uses HTTP; please consider using HTTPS")
+
+	case "https":
+		// HTTPS is good!
+
+	default:
+		ppfmt.Errorf(pp.EmojiUserError, `The Healthchecks URL (redacted) does not look like a valid URL.`)
+		ppfmt.Errorf(pp.EmojiUserError, `A valid example is "https://hc-ping.com/01234567-0123-0123-0123-0123456789abc".`)
+		return nil, false
+	}
+
+	h := &Healthchecks{
 		BaseURL:    url,
-		Timeout:    HealthChecksDefaultTimeout,
-		MaxRetries: HealthChecksDefaultMaxRetries,
+		Timeout:    HealthchecksDefaultTimeout,
+		MaxRetries: HealthchecksDefaultMaxRetries,
 	}
 
 	for _, o := range os {
@@ -61,12 +74,12 @@ func NewHealthChecks(ppfmt pp.PP, rawURL string, os ...HealthChecksOption) (Moni
 	return h, true
 }
 
-func (h *HealthChecks) DescribeService() string {
-	return "Healthchecks.io"
+func (h *Healthchecks) DescribeService() string {
+	return "Healthchecks"
 }
 
 //nolint:funlen
-func (h *HealthChecks) ping(ctx context.Context, ppfmt pp.PP, endpoint string) bool {
+func (h *Healthchecks) ping(ctx context.Context, ppfmt pp.PP, endpoint string, message string) bool {
 	url := h.BaseURL.JoinPath(endpoint)
 
 	endpointDescription := "default (root)"
@@ -82,10 +95,10 @@ func (h *HealthChecks) ping(ctx context.Context, ppfmt pp.PP, endpoint string) b
 		ctx, cancel := context.WithTimeout(ctx, h.Timeout)
 		defer cancel()
 
-		req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
+		req, err := http.NewRequestWithContext(ctx, http.MethodPost, url.String(), strings.NewReader(message))
 		if err != nil {
 			ppfmt.Warningf(pp.EmojiImpossible,
-				"Failed to prepare HTTP(S) request to the %s endpoint of Healthchecks.io: %v",
+				"Failed to prepare HTTP(S) request to the %s endpoint of Healthchecks: %v",
 				endpointDescription, err)
 			return false
 		}
@@ -93,7 +106,7 @@ func (h *HealthChecks) ping(ctx context.Context, ppfmt pp.PP, endpoint string) b
 		resp, err := http.DefaultClient.Do(req)
 		if err != nil {
 			ppfmt.Warningf(pp.EmojiError,
-				"Failed to send HTTP(S) request to the %s endpoint of Healthchecks.io: %v",
+				"Failed to send HTTP(S) request to the %s endpoint of Healthchecks: %v",
 				endpointDescription, err)
 			ppfmt.Infof(pp.EmojiRepeatOnce, "Trying again . . .")
 			continue
@@ -102,7 +115,7 @@ func (h *HealthChecks) ping(ctx context.Context, ppfmt pp.PP, endpoint string) b
 		body, err := io.ReadAll(resp.Body)
 		if err != nil {
 			ppfmt.Warningf(pp.EmojiError,
-				"Failed to read HTTP(S) response from the %s endpoint of Healthchecks.io: %v",
+				"Failed to read HTTP(S) response from the %s endpoint of Healthchecks: %v",
 				endpointDescription, err)
 			ppfmt.Infof(pp.EmojiRepeatOnce, "Trying again . . .")
 			continue
@@ -128,40 +141,44 @@ func (h *HealthChecks) ping(ctx context.Context, ppfmt pp.PP, endpoint string) b
 		bodyAsString := strings.TrimSpace(string(body))
 		if bodyAsString != "OK" {
 			ppfmt.Warningf(pp.EmojiError,
-				"Failed to ping the %s endpoint of Healthchecks.io; got response code: %d %s",
+				"Failed to ping the %s endpoint of Healthchecks; got response code: %d %s",
 				endpointDescription, resp.StatusCode, bodyAsString,
 			)
 			return false
 		}
 
-		ppfmt.Infof(pp.EmojiNotification, "Successfully pinged the %s endpoint of Healthchecks.io", endpointDescription)
+		ppfmt.Infof(pp.EmojiNotification, "Successfully pinged the %s endpoint of Healthchecks", endpointDescription)
 		return true
 	}
 
 	ppfmt.Warningf(
 		pp.EmojiError,
-		"Failed to send HTTP(S) request to the %s endpoint of Healthchecks.io in %d time(s)",
+		"Failed to send HTTP(S) request to the %s endpoint of Healthchecks in %d time(s)",
 		endpointDescription, h.MaxRetries)
 	return false
 }
 
-func (h *HealthChecks) Success(ctx context.Context, ppfmt pp.PP) bool {
-	return h.ping(ctx, ppfmt, "")
+func (h *Healthchecks) Success(ctx context.Context, ppfmt pp.PP, message string) bool {
+	return h.ping(ctx, ppfmt, "", message)
+}
+
+func (h *Healthchecks) Start(ctx context.Context, ppfmt pp.PP, message string) bool {
+	return h.ping(ctx, ppfmt, "/start", message)
 }
 
-func (h *HealthChecks) Start(ctx context.Context, ppfmt pp.PP) bool {
-	return h.ping(ctx, ppfmt, "/start")
+func (h *Healthchecks) Failure(ctx context.Context, ppfmt pp.PP, message string) bool {
+	return h.ping(ctx, ppfmt, "/fail", message)
 }
 
-func (h *HealthChecks) Failure(ctx context.Context, ppfmt pp.PP) bool {
-	return h.ping(ctx, ppfmt, "/fail")
+func (h *Healthchecks) Log(ctx context.Context, ppfmt pp.PP, message string) bool {
+	return h.ping(ctx, ppfmt, "/log", message)
 }
 
-func (h *HealthChecks) ExitStatus(ctx context.Context, ppfmt pp.PP, code int) bool {
+func (h *Healthchecks) ExitStatus(ctx context.Context, ppfmt pp.PP, code int, message string) bool {
 	if code < 0 || code > 255 {
 		ppfmt.Errorf(pp.EmojiImpossible, "Exit code (%d) not within the range 0-255", code)
 		return false
 	}
 
-	return h.ping(ctx, ppfmt, fmt.Sprintf("/%d", code))
+	return h.ping(ctx, ppfmt, fmt.Sprintf("/%d", code), message)
 }
diff --git a/internal/monitor/util.go b/internal/monitor/util.go
index 33338c1..2bf1eda 100644
--- a/internal/monitor/util.go
+++ b/internal/monitor/util.go
@@ -6,40 +6,50 @@ import (
 	"github.com/favonia/cloudflare-ddns/internal/pp"
 )
 
-func SuccessAll(ctx context.Context, ppfmt pp.PP, ms []Monitor) bool {
+func SuccessAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, message string) bool {
 	ok := true
 	for _, m := range ms {
-		if !m.Success(ctx, ppfmt) {
+		if !m.Success(ctx, ppfmt, message) {
 			ok = false
 		}
 	}
 	return ok
 }
 
-func StartAll(ctx context.Context, ppfmt pp.PP, ms []Monitor) bool {
+func StartAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, message string) bool {
 	ok := true
 	for _, m := range ms {
-		if !m.Start(ctx, ppfmt) {
+		if !m.Start(ctx, ppfmt, message) {
 			ok = false
 		}
 	}
 	return ok
 }
 
-func FailureAll(ctx context.Context, ppfmt pp.PP, ms []Monitor) bool {
+func FailureAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, message string) bool {
 	ok := true
 	for _, m := range ms {
-		if !m.Failure(ctx, ppfmt) {
+		if !m.Failure(ctx, ppfmt, message) {
 			ok = false
 		}
 	}
 	return ok
 }
 
-func ExitStatusAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, code int) bool {
+func LogAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, message string) bool {
 	ok := true
 	for _, m := range ms {
-		if !m.ExitStatus(ctx, ppfmt, code) {
+		if !m.Log(ctx, ppfmt, message) {
+			ok = false
+		}
+	}
+	return ok
+}
+
+func ExitStatusAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, code int, message string) bool {
+	ok := true
+	for _, m := range ms {
+		if !m.ExitStatus(ctx, ppfmt, code, message) {
 			ok = false
 		}
 	}
diff --git a/internal/pp/emoji.go b/internal/pp/emoji.go
index 281f2e1..cd457f6 100644
--- a/internal/pp/emoji.go
+++ b/internal/pp/emoji.go
@@ -13,9 +13,10 @@ const (
 	EmojiMute         Emoji = "🔇" // quiet mode
 	EmojiExperimental Emoji = "🧪" // experimental features
 
-	EmojiAddRecord    Emoji = "🐣" // adding new DNS records
-	EmojiDelRecord    Emoji = "💀" // deleting DNS records
+	EmojiCreateRecord Emoji = "🐣" // adding new DNS records
+	EmojiDeleteRecord Emoji = "💀" // deleting DNS records
 	EmojiUpdateRecord Emoji = "📡" // updating DNS records
+	EmojiClearRecord  Emoji = "🧹" // clearing DNS records
 
 	EmojiNotification Emoji = "🔔" // sending out notifications, pinging, health checks
 	EmojiRepeatOnce   Emoji = "🔂" // repeating things once
diff --git a/internal/setter/base.go b/internal/setter/base.go
index 48ffa0d..34c8e14 100644
--- a/internal/setter/base.go
+++ b/internal/setter/base.go
@@ -21,5 +21,5 @@ type Setter interface {
 		IP netip.Addr,
 		ttl api.TTL,
 		proxied bool,
-	) bool
+	) (bool, string)
 }
diff --git a/internal/setter/setter.go b/internal/setter/setter.go
index faaf047..1d3b149 100644
--- a/internal/setter/setter.go
+++ b/internal/setter/setter.go
@@ -2,6 +2,7 @@ package setter
 
 import (
 	"context"
+	"fmt"
 	"net/netip"
 	"sort"
 
@@ -51,14 +52,16 @@ func New(_ppfmt pp.PP, handle api.Handle) (Setter, bool) {
 // Set calls the DNS service API to update the API of one domain.
 //
 //nolint:funlen
-func (s *setter) Set(ctx context.Context, ppfmt pp.PP, domain domain.Domain, ipnet ipnet.Type, ip netip.Addr, ttl api.TTL, proxied bool) bool { //nolint:lll
+func (s *setter) Set(ctx context.Context, ppfmt pp.PP,
+	domain domain.Domain, ipnet ipnet.Type, ip netip.Addr, ttl api.TTL, proxied bool,
+) (bool, string) {
 	recordType := ipnet.RecordType()
 	domainDescription := domain.Describe()
 
 	rs, ok := s.Handle.ListRecords(ctx, ppfmt, domain, ipnet)
 	if !ok {
 		ppfmt.Errorf(pp.EmojiError, "Failed to retrieve the current %s records of %q", recordType, domainDescription)
-		return false
+		return false, fmt.Sprintf("%s %s possibly inconsistent", recordType, domainDescription)
 	}
 
 	// The intention of these two lists is to find or create a good record and then delete everything else.
@@ -90,11 +93,14 @@ func (s *setter) Set(ctx context.Context, ppfmt pp.PP, domain domain.Domain, ipn
 	// If it's up to date and there are no other records, we are done!
 	if uptodate && len(duplicateMatchedIDs) == 0 && len(unmatchedIDsToUpdate) == 0 {
 		ppfmt.Infof(pp.EmojiAlreadyDone, "The %s records of %q are already up to date", recordType, domainDescription)
-		return true
+		return true, ""
 	}
 
 	// This counts the stale records that have not being deleted yet.
 	//
+	// Message for monitor (e.g. Healthchecks)
+	monitorMessage := ""
+
 	// We need a different counter (instead of using len(unmatchedIDsToUpdate) all the times)
 	// because when we fail to delete a record, we will give up and remove that record from
 	// unmatchedIDsToUpdate, but the stale record that could not be deleted should still be
@@ -117,6 +123,7 @@ func (s *setter) Set(ctx context.Context, ppfmt pp.PP, domain domain.Domain, ipn
 				// If the updating succeeds, we can move on to the next stage!
 				ppfmt.Noticef(pp.EmojiUpdateRecord,
 					"Updated a stale %s record of %q (ID: %s)", recordType, domainDescription, id)
+				monitorMessage = fmt.Sprintf("%s %s set to %s", recordType, domainDescription, ip.String())
 
 				// Now it's up to date! Note that matchedIDs must be empty; otherwise uptodate would have been true.
 				uptodate = true
@@ -127,7 +134,7 @@ func (s *setter) Set(ctx context.Context, ppfmt pp.PP, domain domain.Domain, ipn
 			} else {
 				// If the updating fails, we will delete it.
 				if s.Handle.DeleteRecord(ctx, ppfmt, domain, ipnet, id) {
-					ppfmt.Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)",
+					ppfmt.Noticef(pp.EmojiDeleteRecord, "Deleted a stale %s record of %q (ID: %s)",
 						recordType, domainDescription, id)
 
 					// Only when the deletion succeeds, we decrease the counter of remaining stale records.
@@ -147,7 +154,8 @@ func (s *setter) Set(ctx context.Context, ppfmt pp.PP, domain domain.Domain, ipn
 	if !uptodate {
 		if id, ok := s.Handle.CreateRecord(ctx, ppfmt,
 			domain, ipnet, ip, ttl, proxied); ok {
-			ppfmt.Noticef(pp.EmojiAddRecord, "Added a new %s record of %q (ID: %s)", recordType, domainDescription, id)
+			ppfmt.Noticef(pp.EmojiCreateRecord, "Added a new %s record of %q (ID: %s)", recordType, domainDescription, id)
+			monitorMessage = fmt.Sprintf("%s %s set to %s", recordType, domainDescription, ip.String())
 
 			// Now it's up to date! matchedIDs and unmatchedIDsToUpdate must both be empty at this point
 			uptodate = true
@@ -157,16 +165,21 @@ func (s *setter) Set(ctx context.Context, ppfmt pp.PP, domain domain.Domain, ipn
 	// Now, we should try to delete all remaining stale records.
 	for _, id := range unmatchedIDsToUpdate {
 		if s.Handle.DeleteRecord(ctx, ppfmt, domain, ipnet, id) {
-			ppfmt.Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)", recordType, domainDescription, id)
+			ppfmt.Noticef(pp.EmojiDeleteRecord, "Deleted a stale %s record of %q (ID: %s)", recordType, domainDescription, id)
 			numUndeletedUnmatched--
 		}
 	}
 
+	// We meant to clear all records, and we did it!
+	if numUndeletedUnmatched == 0 && !ip.IsValid() {
+		monitorMessage = fmt.Sprintf("%s %s cleared", recordType, domainDescription)
+	}
+
 	// We should also delete all duplicate records even if they are up to date.
 	// This has lower priority than deleting the stale records.
 	for _, id := range duplicateMatchedIDs {
 		if s.Handle.DeleteRecord(ctx, ppfmt, domain, ipnet, id) {
-			ppfmt.Noticef(pp.EmojiDelRecord, "Deleted a duplicate %s record of %q (ID: %s)",
+			ppfmt.Noticef(pp.EmojiDeleteRecord, "Deleted a duplicate %s record of %q (ID: %s)",
 				recordType, domainDescription, id)
 		}
 	}
@@ -176,8 +189,8 @@ func (s *setter) Set(ctx context.Context, ppfmt pp.PP, domain domain.Domain, ipn
 		ppfmt.Errorf(pp.EmojiError,
 			"Failed to complete updating of %s records of %q; records might be inconsistent",
 			recordType, domainDescription)
-		return false
+		return false, fmt.Sprintf("%s %s possibly inconsistent", recordType, domainDescription)
 	}
 
-	return true
+	return true, monitorMessage
 }
diff --git a/internal/updater/updater.go b/internal/updater/updater.go
index 73dcd67..b73f45a 100644
--- a/internal/updater/updater.go
+++ b/internal/updater/updater.go
@@ -3,6 +3,7 @@ package updater
 import (
 	"context"
 	"net/netip"
+	"strings"
 
 	"github.com/favonia/cloudflare-ddns/internal/config"
 	"github.com/favonia/cloudflare-ddns/internal/domain"
@@ -17,26 +18,30 @@ func getProxied(ppfmt pp.PP, c *config.Config, domain domain.Domain) bool {
 	}
 
 	ppfmt.Warningf(pp.EmojiImpossible,
-		"Proxied[%s] not initialized; please report the bug at https://github.com/favonia/cloudflare-ddns/issues/new",
+		"Proxied[%s] not initialized; this should not happen; please report the bug at https://github.com/favonia/cloudflare-ddns/issues/new", //nolint:lll
 		domain.Describe(),
 	)
 	return false
 }
 
-func setIP(ctx context.Context, ppfmt pp.PP, c *config.Config, s setter.Setter, ipNet ipnet.Type, ip netip.Addr) bool {
+func setIP(ctx context.Context, ppfmt pp.PP,
+	c *config.Config, s setter.Setter, ipNet ipnet.Type, ip netip.Addr,
+) (bool, string) {
 	ok := true
+	var msgs []string
 
 	for _, domain := range c.Domains[ipNet] {
 		ctx, cancel := context.WithTimeout(ctx, c.UpdateTimeout)
 		defer cancel()
 
-		if !s.Set(ctx, ppfmt, domain, ipNet, ip, c.TTL,
-			getProxied(ppfmt, c, domain)) {
-			ok = false
+		setOk, msg := s.Set(ctx, ppfmt, domain, ipNet, ip, c.TTL, getProxied(ppfmt, c, domain))
+		ok = ok && setOk
+		if msg != "" {
+			msgs = append(msgs, msg)
 		}
 	}
 
-	return ok
+	return ok, strings.Join(msgs, "\n")
 }
 
 var MessageShouldDisplay = map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true} //nolint:gochecknoglobals
@@ -67,36 +72,43 @@ func detectIP(ctx context.Context, ppfmt pp.PP, c *config.Config, ipNet ipnet.Ty
 	return ip
 }
 
-func UpdateIPs(ctx context.Context, ppfmt pp.PP, c *config.Config, s setter.Setter) bool {
+func UpdateIPs(ctx context.Context, ppfmt pp.PP, c *config.Config, s setter.Setter) (bool, string) {
 	ok := true
+	var msgs []string
 
 	for _, ipNet := range [...]ipnet.Type{ipnet.IP4, ipnet.IP6} {
 		if c.Provider[ipNet] != nil {
 			ip := detectIP(ctx, ppfmt, c, ipNet)
 			if !ip.IsValid() {
+				// We can't detect the new IP address. It's probably better to leave existing IP addresses alone.
 				ok = false
 				continue
 			}
 
-			if !setIP(ctx, ppfmt, c, s, ipNet, ip) {
-				ok = false
+			setOk, msg := setIP(ctx, ppfmt, c, s, ipNet, ip)
+			ok = ok && setOk
+			if msg != "" {
+				msgs = append(msgs, msg)
 			}
 		}
 	}
 
-	return ok
+	return ok, strings.Join(msgs, "\n")
 }
 
-func ClearIPs(ctx context.Context, ppfmt pp.PP, c *config.Config, s setter.Setter) bool {
+func ClearIPs(ctx context.Context, ppfmt pp.PP, c *config.Config, s setter.Setter) (bool, string) {
 	ok := true
+	var msgs []string
 
 	for _, ipNet := range [...]ipnet.Type{ipnet.IP4, ipnet.IP6} {
 		if c.Provider[ipNet] != nil {
-			if !setIP(ctx, ppfmt, c, s, ipNet, netip.Addr{}) {
-				ok = false
+			setOk, msg := setIP(ctx, ppfmt, c, s, ipNet, netip.Addr{})
+			ok = ok && setOk
+			if msg != "" {
+				msgs = append(msgs, msg)
 			}
 		}
 	}
 
-	return ok
+	return ok, strings.Join(msgs, "\n")
 }


Test patch:
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index e488e00..e8fe0c0 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -402,7 +402,7 @@ func TestPrintMaps(t *testing.T) {
 		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "%-*s %s", 24, "IP detection:", "5s"),
 		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "%-*s %s", 24, "Record updating:", "30s"),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Monitors:"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "%-*s %s", 24, "Healthchecks.io:", "(URL redacted)"),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "%-*s %s", 24, "Healthchecks:", "(URL redacted)"),
 	)
 
 	c := config.Default()
@@ -418,7 +418,7 @@ func TestPrintMaps(t *testing.T) {
 	c.Proxied[domain.FQDN("c")] = false
 	c.Proxied[domain.FQDN("d")] = false
 
-	m, ok := monitor.NewHealthChecks(mockPP, "http://user:pass@host/path")
+	m, ok := monitor.NewHealthchecks(mockPP, "https://user:pass@host/path")
 	require.True(t, ok)
 	c.Monitors = []monitor.Monitor{m}
 
diff --git a/internal/config/env_test.go b/internal/config/env_test.go
index 7764d71..cd1ed40 100644
--- a/internal/config/env_test.go
+++ b/internal/config/env_test.go
@@ -720,7 +720,7 @@ func urlMustParse(t *testing.T, u string) *url.URL {
 }
 
 //nolint:paralleltest,funlen // paralleltest should not be used because environment vars are global
-func TestReadHealthChecksURL(t *testing.T) {
+func TestReadHealthchecksURL(t *testing.T) {
 	key := keyPrefix + "HEALTHCHECKS"
 
 	type mon = monitor.Monitor
@@ -742,10 +742,10 @@ func TestReadHealthChecksURL(t *testing.T) {
 		"example": {
 			true, "https://hi.org/1234",
 			[]mon{},
-			[]mon{&monitor.HealthChecks{
+			[]mon{&monitor.Healthchecks{
 				BaseURL:    urlMustParse(t, "https://hi.org/1234"),
-				Timeout:    monitor.HealthChecksDefaultTimeout,
-				MaxRetries: monitor.HealthChecksDefaultMaxRetries,
+				Timeout:    monitor.HealthchecksDefaultTimeout,
+				MaxRetries: monitor.HealthchecksDefaultMaxRetries,
 			}},
 			true,
 			nil,
@@ -753,10 +753,10 @@ func TestReadHealthChecksURL(t *testing.T) {
 		"password": {
 			true, "https://me:pass@hi.org/1234",
 			[]mon{},
-			[]mon{&monitor.HealthChecks{
+			[]mon{&monitor.Healthchecks{
 				BaseURL:    urlMustParse(t, "https://me:pass@hi.org/1234"),
-				Timeout:    monitor.HealthChecksDefaultTimeout,
-				MaxRetries: monitor.HealthChecksDefaultMaxRetries,
+				Timeout:    monitor.HealthchecksDefaultTimeout,
+				MaxRetries: monitor.HealthchecksDefaultMaxRetries,
 			}},
 			true,
 			nil,
@@ -764,10 +764,10 @@ func TestReadHealthChecksURL(t *testing.T) {
 		"fragment": {
 			true, "https://hi.org/1234#fragment",
 			[]mon{},
-			[]mon{&monitor.HealthChecks{
+			[]mon{&monitor.Healthchecks{
 				BaseURL:    urlMustParse(t, "https://hi.org/1234#fragment"),
-				Timeout:    monitor.HealthChecksDefaultTimeout,
-				MaxRetries: monitor.HealthChecksDefaultMaxRetries,
+				Timeout:    monitor.HealthchecksDefaultTimeout,
+				MaxRetries: monitor.HealthchecksDefaultMaxRetries,
 			}},
 			true,
 			nil,
@@ -775,10 +775,10 @@ func TestReadHealthChecksURL(t *testing.T) {
 		"query": {
 			true, "https://hi.org/1234?hello=123",
 			[]mon{},
-			[]mon{&monitor.HealthChecks{
+			[]mon{&monitor.Healthchecks{
 				BaseURL:    urlMustParse(t, "https://hi.org/1234?hello=123"),
-				Timeout:    monitor.HealthChecksDefaultTimeout,
-				MaxRetries: monitor.HealthChecksDefaultMaxRetries,
+				Timeout:    monitor.HealthchecksDefaultTimeout,
+				MaxRetries: monitor.HealthchecksDefaultMaxRetries,
 			}},
 			true,
 			nil,
@@ -793,7 +793,7 @@ func TestReadHealthChecksURL(t *testing.T) {
 			if tc.prepareMockPP != nil {
 				tc.prepareMockPP(mockPP)
 			}
-			ok := config.ReadHealthChecksURL(mockPP, key, &field)
+			ok := config.ReadHealthchecksURL(mockPP, key, &field)
 			require.Equal(t, tc.ok, ok)
 			require.Equal(t, tc.newField, field)
 		})
diff --git a/internal/monitor/healthchecks_test.go b/internal/monitor/healthchecks_test.go
index a5e5cdd..7ca8fdd 100644
--- a/internal/monitor/healthchecks_test.go
+++ b/internal/monitor/healthchecks_test.go
@@ -16,32 +16,32 @@ import (
 	"github.com/favonia/cloudflare-ddns/internal/pp"
 )
 
-func TestSetHealthChecksMaxRetries(t *testing.T) {
+func TestSetHealthchecksMaxRetries(t *testing.T) {
 	t.Parallel()
 
-	m := &monitor.HealthChecks{} //nolint:exhaustruct
+	m := &monitor.Healthchecks{} //nolint:exhaustruct
 
-	monitor.SetHealthChecksMaxRetries(42)(m)
+	monitor.SetHealthchecksMaxRetries(42)(m)
 
-	require.Equal(t, &monitor.HealthChecks{MaxRetries: 42}, m) //nolint:exhaustruct
+	require.Equal(t, &monitor.Healthchecks{MaxRetries: 42}, m) //nolint:exhaustruct
 }
 
-func TestSetHealthChecksMaxRetriesPanic(t *testing.T) {
+func TestSetHealthchecksMaxRetriesPanic(t *testing.T) {
 	t.Parallel()
 
 	require.Panics(t,
 		func() {
-			monitor.SetHealthChecksMaxRetries(0)
+			monitor.SetHealthchecksMaxRetries(0)
 		},
 	)
 	require.Panics(t,
 		func() {
-			monitor.SetHealthChecksMaxRetries(-1)
+			monitor.SetHealthchecksMaxRetries(-1)
 		},
 	)
 }
 
-func TestNewHealthChecks(t *testing.T) {
+func TestNewHealthchecks(t *testing.T) {
 	t.Parallel()
 
 	rawURL := "https://user:pass@host/path"
@@ -50,35 +50,48 @@ func TestNewHealthChecks(t *testing.T) {
 
 	mockCtrl := gomock.NewController(t)
 	mockPP := mocks.NewMockPP(mockCtrl)
-	m, ok := monitor.NewHealthChecks(mockPP, rawURL, monitor.SetHealthChecksMaxRetries(100))
-	require.Equal(t, &monitor.HealthChecks{
+	m, ok := monitor.NewHealthchecks(mockPP, rawURL, monitor.SetHealthchecksMaxRetries(100))
+	require.Equal(t, &monitor.Healthchecks{
 		BaseURL:    parsedURL,
-		Timeout:    monitor.HealthChecksDefaultTimeout,
+		Timeout:    monitor.HealthchecksDefaultTimeout,
 		MaxRetries: 100,
 	}, m)
 	require.True(t, ok)
 }
 
-func TestNewHealthChecksFail1(t *testing.T) {
+func TestNewHealthchecksFail1(t *testing.T) {
 	t.Parallel()
 
 	mockCtrl := gomock.NewController(t)
 	mockPP := mocks.NewMockPP(mockCtrl)
 	gomock.InOrder(
-		mockPP.EXPECT().Errorf(pp.EmojiUserError, `The Healthchecks.io URL (redacted) does not look like a valid URL.`),
+		mockPP.EXPECT().Errorf(pp.EmojiUserError, `The Healthchecks URL (redacted) does not look like a valid URL.`),
 		mockPP.EXPECT().Errorf(pp.EmojiUserError, `A valid example is "https://hc-ping.com/01234567-0123-0123-0123-0123456789abc".`), //nolint:lll
 	)
-	_, ok := monitor.NewHealthChecks(mockPP, "this is not a valid URL")
+	_, ok := monitor.NewHealthchecks(mockPP, "this is not a valid URL")
 	require.False(t, ok)
 }
 
-func TestNewHealthChecksFail2(t *testing.T) {
+func TestNewHealthchecksFail2(t *testing.T) {
 	t.Parallel()
 
 	mockCtrl := gomock.NewController(t)
 	mockPP := mocks.NewMockPP(mockCtrl)
-	mockPP.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse the Healthchecks.io URL (redacted)")
-	_, ok := monitor.NewHealthChecks(mockPP, "://#?")
+	gomock.InOrder(
+		mockPP.EXPECT().Errorf(pp.EmojiUserError, `The Healthchecks URL (redacted) does not look like a valid URL.`),
+		mockPP.EXPECT().Errorf(pp.EmojiUserError, `A valid example is "https://hc-ping.com/01234567-0123-0123-0123-0123456789abc".`), //nolint:lll
+	)
+	_, ok := monitor.NewHealthchecks(mockPP, "ftp://example.org")
+	require.False(t, ok)
+}
+
+func TestNewHealthchecksFail3(t *testing.T) {
+	t.Parallel()
+
+	mockCtrl := gomock.NewController(t)
+	mockPP := mocks.NewMockPP(mockCtrl)
+	mockPP.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse the Healthchecks URL (redacted)")
+	_, ok := monitor.NewHealthchecks(mockPP, "://#?")
 	require.False(t, ok)
 }
 
@@ -87,9 +100,9 @@ func TestDescripbeService(t *testing.T) {
 
 	mockCtrl := gomock.NewController(t)
 	mockPP := mocks.NewMockPP(mockCtrl)
-	m, ok := monitor.NewHealthChecks(mockPP, "https://user:pass@host/path")
+	m, ok := monitor.NewHealthchecks(mockPP, "https://user:pass@host/path")
 	require.True(t, ok)
-	require.Equal(t, "Healthchecks.io", m.DescribeService())
+	require.Equal(t, "Healthchecks", m.DescribeService())
 }
 
 //nolint:funlen
@@ -107,6 +120,7 @@ func TestEndPoints(t *testing.T) {
 	for name, tc := range map[string]struct {
 		endpoint      func(pp.PP, monitor.Monitor) bool
 		url           string
+		message       string
 		actions       []action
 		pinged        bool
 		ok            bool
@@ -114,134 +128,162 @@ func TestEndPoints(t *testing.T) {
 	}{
 		"success": {
 			func(ppfmt pp.PP, m monitor.Monitor) bool {
-				return m.Success(context.Background(), ppfmt)
+				return m.Success(context.Background(), ppfmt, "hello")
 			},
-			"/",
+			"/", "hello",
 			[]action{ActionAbort, ActionAbort, ActionOk},
 			true, true,
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks.io: %v", `default (root)`, gomock.Any()), //nolint:lll
-					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),                                                                                     //nolint:lll
-					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks.io: %v", `default (root)`, gomock.Any()), //nolint:lll
-					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),                                                                                     //nolint:lll
-					m.EXPECT().Infof(pp.EmojiNotification, "Successfully pinged the %s endpoint of Healthchecks.io", `default (root)`),                             //nolint:lll
+					m.EXPECT().Warningf(pp.EmojiUserWarning, "The Healthchecks URL (redacted) uses HTTP; please consider using HTTPS"),
+					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks: %v", `default (root)`, gomock.Any()), //nolint:lll
+					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),
+					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks: %v", `default (root)`, gomock.Any()), //nolint:lll
+					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),
+					m.EXPECT().Infof(pp.EmojiNotification, "Successfully pinged the %s endpoint of Healthchecks", `default (root)`), //nolint:lll
 				)
 			},
 		},
 		"success/notok": {
 			func(ppfmt pp.PP, m monitor.Monitor) bool {
-				return m.Success(context.Background(), ppfmt)
+				return m.Success(context.Background(), ppfmt, "aloha")
 			},
-			"/",
+			"/", "aloha",
 			[]action{ActionAbort, ActionAbort, ActionNotOk},
 			false, false,
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks.io: %v", `default (root)`, gomock.Any()),                 //nolint:lll
-					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),                                                                                                     //nolint:lll
-					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks.io: %v", `default (root)`, gomock.Any()),                 //nolint:lll
-					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),                                                                                                     //nolint:lll
-					m.EXPECT().Warningf(pp.EmojiError, "Failed to ping the %s endpoint of Healthchecks.io; got response code: %d %s", `default (root)`, 400, "invalid url format"), //nolint:lll
+					m.EXPECT().Warningf(pp.EmojiUserWarning, "The Healthchecks URL (redacted) uses HTTP; please consider using HTTPS"),
+					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks: %v", `default (root)`, gomock.Any()), //nolint:lll
+					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),
+					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks: %v", `default (root)`, gomock.Any()), //nolint:lll
+					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),
+					m.EXPECT().Warningf(pp.EmojiError, "Failed to ping the %s endpoint of Healthchecks; got response code: %d %s", `default (root)`, 400, "invalid url format"), //nolint:lll
 				)
 			},
 		},
 		"success/abort/all": {
 			func(ppfmt pp.PP, m monitor.Monitor) bool {
-				return m.Success(context.Background(), ppfmt)
+				return m.Success(context.Background(), ppfmt, "stop now")
 			},
-			"/",
+			"/", "stop now",
 			[]action{ActionAbort, ActionAbort, ActionAbort},
 			false, false,
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks.io: %v", `default (root)`, gomock.Any()), //nolint:lll
-					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),                                                                                     //nolint:lll
-					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks.io: %v", `default (root)`, gomock.Any()), //nolint:lll
-					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),                                                                                     //nolint:lll
-					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks.io: %v", `default (root)`, gomock.Any()), //nolint:lll
-					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),                                                                                     //nolint:lll
-					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks.io in %d time(s)", `default (root)`, 3),  //nolint:lll
+					m.EXPECT().Warningf(pp.EmojiUserWarning, "The Healthchecks URL (redacted) uses HTTP; please consider using HTTPS"),
+					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks: %v", `default (root)`, gomock.Any()), //nolint:lll
+					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),
+					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks: %v", `default (root)`, gomock.Any()), //nolint:lll
+					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),
+					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks: %v", `default (root)`, gomock.Any()), //nolint:lll
+					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),
+					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks in %d time(s)", `default (root)`, 3), //nolint:lll
 				)
 			},
 		},
 		"start": {
 			func(ppfmt pp.PP, m monitor.Monitor) bool {
-				return m.Start(context.Background(), ppfmt)
+				return m.Start(context.Background(), ppfmt, "starting now!")
 			},
-			"/start",
+			"/start", "starting now!",
 			[]action{ActionAbort, ActionAbort, ActionOk},
 			true, true,
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks.io: %v", `"/start"`, gomock.Any()), //nolint:lll
-					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),                                                                               //nolint:lll
-					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks.io: %v", `"/start"`, gomock.Any()), //nolint:lll
-					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),                                                                               //nolint:lll
-					m.EXPECT().Infof(pp.EmojiNotification, "Successfully pinged the %s endpoint of Healthchecks.io", `"/start"`),                             //nolint:lll
+					m.EXPECT().Warningf(pp.EmojiUserWarning, "The Healthchecks URL (redacted) uses HTTP; please consider using HTTPS"),
+					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks: %v", `"/start"`, gomock.Any()), //nolint:lll
+					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),
+					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks: %v", `"/start"`, gomock.Any()), //nolint:lll
+					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),
+					m.EXPECT().Infof(pp.EmojiNotification, "Successfully pinged the %s endpoint of Healthchecks", `"/start"`), //nolint:lll
 				)
 			},
 		},
 		"failure": {
 			func(ppfmt pp.PP, m monitor.Monitor) bool {
-				return m.Failure(context.Background(), ppfmt)
+				return m.Failure(context.Background(), ppfmt, "something's wrong")
+			},
+			"/fail", "something's wrong",
+			[]action{ActionAbort, ActionAbort, ActionOk},
+			true, true,
+			func(m *mocks.MockPP) {
+				gomock.InOrder(
+					m.EXPECT().Warningf(pp.EmojiUserWarning, "The Healthchecks URL (redacted) uses HTTP; please consider using HTTPS"),
+					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks: %v", `"/fail"`, gomock.Any()), //nolint:lll
+					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),
+					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks: %v", `"/fail"`, gomock.Any()), //nolint:lll
+					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),
+					m.EXPECT().Infof(pp.EmojiNotification, "Successfully pinged the %s endpoint of Healthchecks", `"/fail"`), //nolint:lll
+				)
+			},
+		},
+		"log": {
+			func(ppfmt pp.PP, m monitor.Monitor) bool {
+				return m.Log(context.Background(), ppfmt, "message")
 			},
-			"/fail",
+			"/log", "message",
 			[]action{ActionAbort, ActionAbort, ActionOk},
 			true, true,
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks.io: %v", `"/fail"`, gomock.Any()), //nolint:lll
-					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),                                                                              //nolint:lll
-					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks.io: %v", `"/fail"`, gomock.Any()), //nolint:lll
-					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),                                                                              //nolint:lll
-					m.EXPECT().Infof(pp.EmojiNotification, "Successfully pinged the %s endpoint of Healthchecks.io", `"/fail"`),                             //nolint:lll
+					m.EXPECT().Warningf(pp.EmojiUserWarning, "The Healthchecks URL (redacted) uses HTTP; please consider using HTTPS"),
+					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks: %v", `"/log"`, gomock.Any()), //nolint:lll
+					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),
+					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks: %v", `"/log"`, gomock.Any()), //nolint:lll
+					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),
+					m.EXPECT().Infof(pp.EmojiNotification, "Successfully pinged the %s endpoint of Healthchecks", `"/log"`), //nolint:lll
 				)
 			},
 		},
 		"exitstatus/0": {
 			func(ppfmt pp.PP, m monitor.Monitor) bool {
-				return m.ExitStatus(context.Background(), ppfmt, 0)
+				return m.ExitStatus(context.Background(), ppfmt, 0, "bye!")
 			},
-			"/0",
+			"/0", "bye!",
 			[]action{ActionAbort, ActionAbort, ActionOk},
 			true, true,
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks.io: %v", `"/0"`, gomock.Any()), //nolint:lll
-					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),                                                                           //nolint:lll
-					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks.io: %v", `"/0"`, gomock.Any()), //nolint:lll
-					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),                                                                           //nolint:lll
-					m.EXPECT().Infof(pp.EmojiNotification, "Successfully pinged the %s endpoint of Healthchecks.io", `"/0"`),                             //nolint:lll
+					m.EXPECT().Warningf(pp.EmojiUserWarning, "The Healthchecks URL (redacted) uses HTTP; please consider using HTTPS"),
+					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks: %v", `"/0"`, gomock.Any()), //nolint:lll
+					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),
+					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks: %v", `"/0"`, gomock.Any()), //nolint:lll
+					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),
+					m.EXPECT().Infof(pp.EmojiNotification, "Successfully pinged the %s endpoint of Healthchecks", `"/0"`),
 				)
 			},
 		},
 		"exitstatus/1": {
 			func(ppfmt pp.PP, m monitor.Monitor) bool {
-				return m.ExitStatus(context.Background(), ppfmt, 1)
+				return m.ExitStatus(context.Background(), ppfmt, 1, "did exit(1)")
 			},
-			"/1",
+			"/1", "did exit(1)",
 			[]action{ActionAbort, ActionAbort, ActionOk},
 			true, true,
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks.io: %v", `"/1"`, gomock.Any()), //nolint:lll
-					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),                                                                           //nolint:lll
-					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks.io: %v", `"/1"`, gomock.Any()), //nolint:lll
-					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),                                                                           //nolint:lll
-					m.EXPECT().Infof(pp.EmojiNotification, "Successfully pinged the %s endpoint of Healthchecks.io", `"/1"`),                             //nolint:lll
+					m.EXPECT().Warningf(pp.EmojiUserWarning, "The Healthchecks URL (redacted) uses HTTP; please consider using HTTPS"),
+					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks: %v", `"/1"`, gomock.Any()), //nolint:lll
+					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),
+					m.EXPECT().Warningf(pp.EmojiError, "Failed to send HTTP(S) request to the %s endpoint of Healthchecks: %v", `"/1"`, gomock.Any()), //nolint:lll
+					m.EXPECT().Infof(pp.EmojiRepeatOnce, "Trying again . . ."),
+					m.EXPECT().Infof(pp.EmojiNotification, "Successfully pinged the %s endpoint of Healthchecks", `"/1"`),
 				)
 			},
 		},
 		"exitstatus/-1": {
 			func(ppfmt pp.PP, m monitor.Monitor) bool {
-				return m.ExitStatus(context.Background(), ppfmt, -1)
+				return m.ExitStatus(context.Background(), ppfmt, -1, "feeling negative")
 			},
-			"",
+			"", "feeling negative",
 			nil,
 			false, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiImpossible, "Exit code (%d) not within the range 0-255", -1)
+				gomock.InOrder(
+					m.EXPECT().Warningf(pp.EmojiUserWarning, "The Healthchecks URL (redacted) uses HTTP; please consider using HTTPS"),
+					m.EXPECT().Errorf(pp.EmojiImpossible, "Exit code (%d) not within the range 0-255", -1),
+				)
 			},
 		},
 	} {
@@ -257,9 +299,13 @@ func TestEndPoints(t *testing.T) {
 			visited := 0
 			pinged := false
 			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-				require.Equal(t, http.MethodGet, r.Method)
+				require.Equal(t, http.MethodPost, r.Method)
 				require.Equal(t, tc.url, r.URL.EscapedPath())
 
+				reqBody, err := io.ReadAll(r.Body)
+				require.Nil(t, err)
+				require.Equal(t, tc.message, string(reqBody))
+
 				visited++
 				require.LessOrEqual(t, visited, len(tc.actions))
 				switch tc.actions[visited-1] {
@@ -280,7 +326,7 @@ func TestEndPoints(t *testing.T) {
 				}
 			}))
 
-			m, ok := monitor.NewHealthChecks(mockPP, server.URL, monitor.SetHealthChecksMaxRetries(3))
+			m, ok := monitor.NewHealthchecks(mockPP, server.URL, monitor.SetHealthchecksMaxRetries(3))
 			require.True(t, ok)
 			ok = tc.endpoint(mockPP, m)
 			require.Equal(t, tc.ok, ok)
diff --git a/internal/monitor/util_test.go b/internal/monitor/util_test.go
index 99e0fff..411e62a 100644
--- a/internal/monitor/util_test.go
+++ b/internal/monitor/util_test.go
@@ -18,13 +18,15 @@ func TestSuccessAll(t *testing.T) {
 	mockCtrl := gomock.NewController(t)
 	mockPP := mocks.NewMockPP(mockCtrl)
 
+	message := "aloha"
+
 	for i := 0; i < 5; i++ {
 		m := mocks.NewMockMonitor(mockCtrl)
-		m.EXPECT().Success(context.Background(), mockPP)
+		m.EXPECT().Success(context.Background(), mockPP, message)
 		ms = append(ms, m)
 	}
 
-	monitor.SuccessAll(context.Background(), mockPP, ms)
+	monitor.SuccessAll(context.Background(), mockPP, ms, message)
 }
 
 func TestStartAll(t *testing.T) {
@@ -35,13 +37,15 @@ func TestStartAll(t *testing.T) {
 	mockCtrl := gomock.NewController(t)
 	mockPP := mocks.NewMockPP(mockCtrl)
 
+	message := "你好"
+
 	for i := 0; i < 5; i++ {
 		m := mocks.NewMockMonitor(mockCtrl)
-		m.EXPECT().Start(context.Background(), mockPP)
+		m.EXPECT().Start(context.Background(), mockPP, message)
 		ms = append(ms, m)
 	}
 
-	monitor.StartAll(context.Background(), mockPP, ms)
+	monitor.StartAll(context.Background(), mockPP, ms, message)
 }
 
 func TestFailureAll(t *testing.T) {
@@ -52,13 +56,34 @@ func TestFailureAll(t *testing.T) {
 	mockCtrl := gomock.NewController(t)
 	mockPP := mocks.NewMockPP(mockCtrl)
 
+	message := "secret"
+
 	for i := 0; i < 5; i++ {
 		m := mocks.NewMockMonitor(mockCtrl)
-		m.EXPECT().Failure(context.Background(), mockPP)
+		m.EXPECT().Failure(context.Background(), mockPP, message)
 		ms = append(ms, m)
 	}
 
-	monitor.FailureAll(context.Background(), mockPP, ms)
+	monitor.FailureAll(context.Background(), mockPP, ms, message)
+}
+
+func TestLogAll(t *testing.T) {
+	t.Parallel()
+
+	var ms []monitor.Monitor
+
+	mockCtrl := gomock.NewController(t)
+	mockPP := mocks.NewMockPP(mockCtrl)
+
+	message := "forest"
+
+	for i := 0; i < 5; i++ {
+		m := mocks.NewMockMonitor(mockCtrl)
+		m.EXPECT().Log(context.Background(), mockPP, message)
+		ms = append(ms, m)
+	}
+
+	monitor.LogAll(context.Background(), mockPP, ms, message)
 }
 
 func TestExitStatus(t *testing.T) {
@@ -69,11 +94,13 @@ func TestExitStatus(t *testing.T) {
 	mockCtrl := gomock.NewController(t)
 	mockPP := mocks.NewMockPP(mockCtrl)
 
+	message := "bye!"
+
 	for i := 0; i < 5; i++ {
 		m := mocks.NewMockMonitor(mockCtrl)
-		m.EXPECT().ExitStatus(context.Background(), mockPP, 42)
+		m.EXPECT().ExitStatus(context.Background(), mockPP, 42, message)
 		ms = append(ms, m)
 	}
 
-	monitor.ExitStatusAll(context.Background(), mockPP, ms, 42)
+	monitor.ExitStatusAll(context.Background(), mockPP, ms, 42, message)
 }
diff --git a/internal/setter/setter_test.go b/internal/setter/setter_test.go
index b1ba70e..e24b116 100644
--- a/internal/setter/setter_test.go
+++ b/internal/setter/setter_test.go
@@ -36,6 +36,7 @@ func TestSet(t *testing.T) {
 	for name, tc := range map[string]struct {
 		ip                netip.Addr
 		ok                bool
+		msg               string
 		ttl               api.TTL
 		proxied           bool
 		prepareMockPP     func(m *mocks.MockPP)
@@ -44,6 +45,7 @@ func TestSet(t *testing.T) {
 		"0-nil/1-true": {
 			invalidIP,
 			true,
+			"",
 			1,
 			true,
 			func(m *mocks.MockPP) {
@@ -56,10 +58,11 @@ func TestSet(t *testing.T) {
 		"0/1-false": {
 			ip1,
 			true,
+			`AAAA sub.test.org set to ::1`,
 			1,
 			false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Noticef(pp.EmojiAddRecord, "Added a new %s record of %q (ID: %s)", "AAAA", "sub.test.org", record1)
+				m.EXPECT().Noticef(pp.EmojiCreateRecord, "Added a new %s record of %q (ID: %s)", "AAAA", "sub.test.org", record1)
 			},
 			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) {
 				gomock.InOrder(
@@ -71,6 +74,7 @@ func TestSet(t *testing.T) {
 		"1unmatched/300-false": {
 			ip1,
 			true,
+			`AAAA sub.test.org set to ::1`,
 			300,
 			false,
 			func(m *mocks.MockPP) {
@@ -91,12 +95,13 @@ func TestSet(t *testing.T) {
 		"1unmatched-updatefail/300-false": {
 			ip1,
 			true,
+			`AAAA sub.test.org set to ::1`,
 			300,
 			false,
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record1),
-					m.EXPECT().Noticef(pp.EmojiAddRecord, "Added a new %s record of %q (ID: %s)", "AAAA", "sub.test.org", record2),
+					m.EXPECT().Noticef(pp.EmojiDeleteRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record1), //nolint:lll
+					m.EXPECT().Noticef(pp.EmojiCreateRecord, "Added a new %s record of %q (ID: %s)", "AAAA", "sub.test.org", record2),
 				)
 			},
 			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) {
@@ -111,10 +116,11 @@ func TestSet(t *testing.T) {
 		"1unmatched-nil/300-false": {
 			invalidIP,
 			true,
+			`AAAA sub.test.org cleared`,
 			300,
 			false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record1)
+				m.EXPECT().Noticef(pp.EmojiDeleteRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record1) //nolint:lll
 			},
 			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) {
 				gomock.InOrder(
@@ -126,6 +132,7 @@ func TestSet(t *testing.T) {
 		"1matched/300-false": {
 			ip1,
 			true,
+			``,
 			300,
 			false,
 			func(m *mocks.MockPP) {
@@ -138,11 +145,12 @@ func TestSet(t *testing.T) {
 		"2matched/300-false": {
 			ip1,
 			true,
+			``,
 			300,
 			false,
 			func(m *mocks.MockPP) {
 				m.EXPECT().Noticef(
-					pp.EmojiDelRecord,
+					pp.EmojiDeleteRecord,
 					"Deleted a duplicate %s record of %q (ID: %s)",
 					"AAAA",
 					"sub.test.org",
@@ -159,6 +167,7 @@ func TestSet(t *testing.T) {
 		"2matched-deletefail/300-false": {
 			ip1,
 			true,
+			``,
 			300,
 			false,
 			nil,
@@ -172,6 +181,7 @@ func TestSet(t *testing.T) {
 		"2unmatched/300-false": {
 			ip1,
 			true,
+			`AAAA sub.test.org set to ::1`,
 			300,
 			false,
 			func(m *mocks.MockPP) {
@@ -183,7 +193,7 @@ func TestSet(t *testing.T) {
 						"sub.test.org",
 						record1,
 					),
-					m.EXPECT().Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record2),
+					m.EXPECT().Noticef(pp.EmojiDeleteRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record2), //nolint:lll
 				)
 			},
 			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) {
@@ -197,11 +207,12 @@ func TestSet(t *testing.T) {
 		"2unmatched-updatefail/300-false": {
 			ip1,
 			true,
+			`AAAA sub.test.org set to ::1`,
 			300,
 			false,
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record1),
+					m.EXPECT().Noticef(pp.EmojiDeleteRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record1), //nolint:lll
 					m.EXPECT().Noticef(
 						pp.EmojiUpdateRecord,
 						"Updated a stale %s record of %q (ID: %s)",
@@ -223,13 +234,14 @@ func TestSet(t *testing.T) {
 		"2unmatched-updatefailtwice/300-false": {
 			ip1,
 			true,
+			`AAAA sub.test.org set to ::1`,
 			300,
 			false,
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record1),
-					m.EXPECT().Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record2),
-					m.EXPECT().Noticef(pp.EmojiAddRecord, "Added a new %s record of %q (ID: %s)", "AAAA", "sub.test.org", record3),
+					m.EXPECT().Noticef(pp.EmojiDeleteRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record1), //nolint:lll
+					m.EXPECT().Noticef(pp.EmojiDeleteRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record2), //nolint:lll
+					m.EXPECT().Noticef(pp.EmojiCreateRecord, "Added a new %s record of %q (ID: %s)", "AAAA", "sub.test.org", record3),
 				)
 			},
 			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) {
@@ -247,12 +259,13 @@ func TestSet(t *testing.T) {
 		"2unmatched-updatefail-deletefail-updatefail/300-false": {
 			ip1,
 			false,
+			`AAAA sub.test.org possibly inconsistent`,
 			300,
 			false,
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record2),
-					m.EXPECT().Noticef(pp.EmojiAddRecord, "Added a new %s record of %q (ID: %s)", "AAAA", "sub.test.org", record3),
+					m.EXPECT().Noticef(pp.EmojiDeleteRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record2),                      //nolint:lll
+					m.EXPECT().Noticef(pp.EmojiCreateRecord, "Added a new %s record of %q (ID: %s)", "AAAA", "sub.test.org", record3),                          //nolint:lll
 					m.EXPECT().Errorf(pp.EmojiError, "Failed to complete updating of %s records of %q; records might be inconsistent", "AAAA", "sub.test.org"), //nolint:lll
 				)
 			},
@@ -271,12 +284,13 @@ func TestSet(t *testing.T) {
 		"2unmatched-updatefailtwice-createfail/300-false": {
 			ip1,
 			false,
+			`AAAA sub.test.org possibly inconsistent`,
 			300,
 			false,
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record1),
-					m.EXPECT().Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record2),
+					m.EXPECT().Noticef(pp.EmojiDeleteRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record1),                      //nolint:lll
+					m.EXPECT().Noticef(pp.EmojiDeleteRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record2),                      //nolint:lll
 					m.EXPECT().Errorf(pp.EmojiError, "Failed to complete updating of %s records of %q; records might be inconsistent", "AAAA", "sub.test.org"), //nolint:lll
 				)
 			},
@@ -294,6 +308,7 @@ func TestSet(t *testing.T) {
 		"listfail/300-false": {
 			ip1,
 			false,
+			`AAAA sub.test.org possibly inconsistent`,
 			300,
 			false,
 			func(m *mocks.MockPP) {
@@ -306,12 +321,13 @@ func TestSet(t *testing.T) {
 		"impossible-invalidIP-records/300-false": {
 			invalidIP,
 			true,
+			`AAAA sub.test.org cleared`,
 			300,
 			false,
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record1),
-					m.EXPECT().Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record2),
+					m.EXPECT().Noticef(pp.EmojiDeleteRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record1), //nolint:lll
+					m.EXPECT().Noticef(pp.EmojiDeleteRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record2), //nolint:lll
 				)
 			},
 			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) {
@@ -343,8 +359,9 @@ func TestSet(t *testing.T) {
 			s, ok := setter.New(mockPP, mockHandle)
 			require.True(t, ok)
 
-			ok = s.Set(ctx, mockPP, domain, ipNetwork, tc.ip, tc.ttl, tc.proxied)
+			ok, msg := s.Set(ctx, mockPP, domain, ipNetwork, tc.ip, tc.ttl, tc.proxied)
 			require.Equal(t, tc.ok, ok)
+			require.Equal(t, tc.msg, msg)
 		})
 	}
 }
diff --git a/internal/updater/updater_test.go b/internal/updater/updater_test.go
index 649976f..31234f9 100644
--- a/internal/updater/updater_test.go
+++ b/internal/updater/updater_test.go
@@ -50,69 +50,75 @@ func TestUpdateIPs(t *testing.T) {
 		ttl                  api.TTL
 		proxied              mockproxied
 		ok                   bool
+		msg                  string
 		MessageShouldDisplay map[ipnet.Type]bool
 		prepareMockPP        func(m *mocks.MockPP)
 		prepareMockProvider  mockproviders
 		prepareMockSetter    func(ppfmt pp.PP, m *mocks.MockSetter)
 	}{
 		"none": {
-			api.TTLAuto, proxiedBoth, true, map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true}, nil, mockproviders{}, nil,
+			api.TTLAuto, proxiedBoth, true, ``, map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true}, nil, mockproviders{}, nil,
 		},
 		"ip4only": {
 			api.TTLAuto,
 			proxiedNone,
 			true,
+			"",
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			pp4only,
 			mockproviders{ipnet.IP4: provider4},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTL(1), false).Return(true)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTL(1), false).Return(true, "")
 			},
 		},
 		"ip4only/setfail": {
 			api.TTLAuto,
 			proxiedBoth,
 			false,
+			"error",
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			pp4only,
 			mockproviders{ipnet.IP4: provider4},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTL(1), true).Return(false)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTL(1), true).Return(false, "error") //nolint:lll
 			},
 		},
 		"ip6only": {
 			api.TTLAuto,
 			proxiedNone,
 			true,
+			"ok",
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			pp6only,
 			mockproviders{ipnet.IP6: provider6},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTL(1), false).Return(true)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTL(1), false).Return(true, "ok")
 			},
 		},
 		"ip6only/setfail": {
 			api.TTLAuto,
 			proxiedBoth,
 			false,
+			"bad",
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			pp6only,
 			mockproviders{ipnet.IP6: provider6},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTL(1), true).Return(false)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTL(1), true).Return(false, "bad")
 			},
 		},
 		"both": {
 			api.TTLAuto,
 			proxiedNone,
 			true,
+			"",
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			ppBoth,
 			mockproviders{ipnet.IP4: provider4, ipnet.IP6: provider6},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
 				gomock.InOrder(
-					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTL(1), false).Return(true),
-					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTL(1), false).Return(true),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTL(1), false).Return(true, ""),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTL(1), false).Return(true, ""),
 				)
 			},
 		},
@@ -120,13 +126,14 @@ func TestUpdateIPs(t *testing.T) {
 			api.TTLAuto,
 			proxiedBoth,
 			false,
+			"hey!",
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			ppBoth,
 			mockproviders{ipnet.IP4: provider4, ipnet.IP6: provider6},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
 				gomock.InOrder(
-					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTL(1), true).Return(false),
-					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTL(1), true).Return(true),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTL(1), true).Return(false, "hey!"), //nolint:lll
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTL(1), true).Return(true, ""),
 				)
 			},
 		},
@@ -134,13 +141,14 @@ func TestUpdateIPs(t *testing.T) {
 			api.TTLAuto,
 			proxiedNone,
 			false,
+			"wrong",
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			ppBoth,
 			mockproviders{ipnet.IP4: provider4, ipnet.IP6: provider6},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
 				gomock.InOrder(
-					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTL(1), false).Return(true),
-					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTL(1), false).Return(false),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTL(1), false).Return(true, ""),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTL(1), false).Return(false, "wrong"), //nolint:lll
 				)
 			},
 		},
@@ -148,6 +156,7 @@ func TestUpdateIPs(t *testing.T) {
 			api.TTLAuto,
 			proxiedBoth,
 			false,
+			"looking good",
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
@@ -163,13 +172,14 @@ func TestUpdateIPs(t *testing.T) {
 				ipnet.IP6: provider6,
 			},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTL(1), true).Return(true)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTL(1), true).Return(true, "looking good") //nolint:lll
 			},
 		},
 		"ip6fails": {
 			api.TTLAuto,
 			proxiedNone,
 			false,
+			"good",
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
@@ -187,13 +197,14 @@ func TestUpdateIPs(t *testing.T) {
 				},
 			},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTL(1), false).Return(true)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTL(1), false).Return(true, "good") //nolint:lll
 			},
 		},
 		"ip6fails/again": {
 			api.TTLAuto,
 			proxiedBoth,
 			false,
+			"",
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: false},
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
@@ -208,13 +219,14 @@ func TestUpdateIPs(t *testing.T) {
 				},
 			},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTL(1), true).Return(true)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTL(1), true).Return(true, "")
 			},
 		},
 		"bothfail": {
 			api.TTLAuto,
 			proxiedNone,
 			false,
+			"",
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
@@ -240,19 +252,20 @@ func TestUpdateIPs(t *testing.T) {
 			api.TTLAuto,
 			mockproxied{},
 			true,
+			"response",
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
 					m.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv4", ip4),
 					m.EXPECT().Warningf(pp.EmojiImpossible,
-						"Proxied[%s] not initialized; please report the bug at https://github.com/favonia/cloudflare-ddns/issues/new",
+						"Proxied[%s] not initialized; this should not happen; please report the bug at https://github.com/favonia/cloudflare-ddns/issues/new", //nolint:lll
 						"ip4.hello",
 					),
 				)
 			},
 			mockproviders{ipnet.IP4: provider4},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTL(1), false).Return(true)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTL(1), false).Return(true, "response") //nolint:lll
 			},
 		},
 	} {
@@ -282,8 +295,9 @@ func TestUpdateIPs(t *testing.T) {
 			if tc.prepareMockSetter != nil {
 				tc.prepareMockSetter(mockPP, mockSetter)
 			}
-			ok := updater.UpdateIPs(ctx, mockPP, conf, mockSetter)
+			ok, msg := updater.UpdateIPs(ctx, mockPP, conf, mockSetter)
 			require.Equal(t, tc.ok, ok)
+			require.Equal(t, tc.msg, msg)
 		})
 	}
 }
@@ -306,6 +320,7 @@ func TestClearIPs(t *testing.T) {
 		ttl                  api.TTL
 		proxied              mockproxied
 		ok                   bool
+		msg                  string
 		MessageShouldDisplay map[ipnet.Type]bool
 		prepareMockPP        func(m *mocks.MockPP)
 		prepareMockProvider  mockproviders
@@ -315,6 +330,7 @@ func TestClearIPs(t *testing.T) {
 			api.TTLAuto,
 			proxiedNone,
 			true,
+			``,
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			nil,
 			mockproviders{},
@@ -324,57 +340,62 @@ func TestClearIPs(t *testing.T) {
 			api.TTLAuto,
 			proxiedNone,
 			true,
+			"hello",
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			nil,
 			mockproviders{ipnet.IP4: true},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, api.TTL(1), false).Return(true)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, api.TTL(1), false).Return(true, "hello")
 			},
 		},
 		"ip4only/setfail": {
 			api.TTLAuto,
 			proxiedNone,
 			false,
+			"err",
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			nil,
 			mockproviders{ipnet.IP4: true},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, api.TTL(1), false).Return(false)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, api.TTL(1), false).Return(false, "err")
 			},
 		},
 		"ip6only": {
 			api.TTLAuto,
 			proxiedNone,
 			true,
+			"",
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			nil,
 			mockproviders{ipnet.IP6: true},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, api.TTL(1), false).Return(true)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, api.TTL(1), false).Return(true, "")
 			},
 		},
 		"ip6only/setfail": {
 			api.TTLAuto,
 			proxiedNone,
 			false,
+			"test",
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			nil,
 			mockproviders{ipnet.IP6: true},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, api.TTL(1), false).Return(false)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, api.TTL(1), false).Return(false, "test")
 			},
 		},
 		"both": {
 			api.TTLAuto,
 			proxiedNone,
 			true,
+			"both\nneither",
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			nil,
 			mockproviders{ipnet.IP4: true, ipnet.IP6: true},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
 				gomock.InOrder(
-					m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, api.TTL(1), false).Return(true),
-					m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, api.TTL(1), false).Return(true),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, api.TTL(1), false).Return(true, "both"),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, api.TTL(1), false).Return(true, "neither"),
 				)
 			},
 		},
@@ -382,13 +403,14 @@ func TestClearIPs(t *testing.T) {
 			api.TTLAuto,
 			proxiedNone,
 			false,
+			"999",
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			nil,
 			mockproviders{ipnet.IP4: true, ipnet.IP6: true},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
 				gomock.InOrder(
-					m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, api.TTL(1), false).Return(false),
-					m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, api.TTL(1), false).Return(true),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, api.TTL(1), false).Return(false, ""),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, api.TTL(1), false).Return(true, "999"),
 				)
 			},
 		},
@@ -396,13 +418,14 @@ func TestClearIPs(t *testing.T) {
 			api.TTLAuto,
 			proxiedNone,
 			false,
+			"1\n2",
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			nil,
 			mockproviders{ipnet.IP4: true, ipnet.IP6: true},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
 				gomock.InOrder(
-					m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, api.TTL(1), false).Return(true),
-					m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, api.TTL(1), false).Return(false),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, api.TTL(1), false).Return(true, "1"),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, api.TTL(1), false).Return(false, "2"),
 				)
 			},
 		},
@@ -432,8 +455,9 @@ func TestClearIPs(t *testing.T) {
 			if tc.prepareMockSetter != nil {
 				tc.prepareMockSetter(mockPP, mockSetter)
 			}
-			ok := updater.ClearIPs(ctx, mockPP, conf, mockSetter)
+			ok, msg := updater.ClearIPs(ctx, mockPP, conf, mockSetter)
 			require.Equal(t, tc.ok, ok)
+			require.Equal(t, tc.msg, msg)
 		})
 	}
 }