Repository: favonia/cloudflare-ddns (go)

Issue:
Timestamps in log
I really like this great little project. Especially the very small footprint!:)

Now, I would really appreciate a very small addition: Timestamps in the log output.
The logs are great in general, but without timestamps any diagnosis is quite difficult.

Pull request description:
feat(cron): show the far start time during countdown
Close #634.

Patch:
diff --git a/cmd/ddns/ddns.go b/cmd/ddns/ddns.go
index 4ce73c2..fff6521 100644
--- a/cmd/ddns/ddns.go
+++ b/cmd/ddns/ddns.go
@@ -5,7 +5,6 @@ import (
 	"context"
 	"fmt"
 	"os"
-	"time"
 
 	"github.com/favonia/cloudflare-ddns/internal/config"
 	"github.com/favonia/cloudflare-ddns/internal/cron"
@@ -147,11 +146,10 @@ func realMain() int { //nolint:funlen
 		}
 
 		// Display the remaining time interval
-		interval := time.Until(next)
-		cron.PrintCountdown(ppfmt, "Checking the IP addresses", interval)
+		cron.PrintCountdown(ppfmt, "Checking the IP addresses", next)
 
 		// Wait for the next signal or the alarm, whichever comes first
-		if !sig.Sleep(ppfmt, interval) {
+		if !sig.SleepUntil(ppfmt, next) {
 			stopUpdating(ctx, ppfmt, c, s)
 			monitor.ExitStatusAll(ctx, ppfmt, 0, "Terminated", c.Monitors)
 			ppfmt.Infof(pp.EmojiBye, "Bye!")
diff --git a/internal/cron/countdown.go b/internal/cron/countdown.go
index 75466eb..109cebb 100644
--- a/internal/cron/countdown.go
+++ b/internal/cron/countdown.go
@@ -9,9 +9,12 @@ import (
 const (
 	intervalUnit     time.Duration = time.Second
 	intervalLargeGap time.Duration = time.Second * 5
+	intervalHugeGap  time.Duration = time.Minute * 10
 )
 
-func PrintCountdown(ppfmt pp.PP, activity string, interval time.Duration) {
+func PrintCountdown(ppfmt pp.PP, activity string, target time.Time) {
+	interval := time.Until(target)
+
 	switch {
 	case interval < -intervalLargeGap:
 		ppfmt.Infof(pp.EmojiNow, "%s now (running behind by %v) . . .", activity, -interval.Round(intervalUnit))
@@ -19,7 +22,13 @@ func PrintCountdown(ppfmt pp.PP, activity string, interval time.Duration) {
 		ppfmt.Infof(pp.EmojiNow, "%s now . . .", activity)
 	case interval < intervalLargeGap:
 		ppfmt.Infof(pp.EmojiAlarm, "%s in less than %v . . .", activity, intervalLargeGap)
-	default:
+	case interval < intervalHugeGap:
 		ppfmt.Infof(pp.EmojiAlarm, "%s in about %v . . .", activity, interval.Round(intervalUnit))
+	default:
+		ppfmt.Infof(pp.EmojiAlarm, "%s in about %v (%v) . . .",
+			activity,
+			interval.Round(intervalUnit),
+			target.In(time.Local).Format(time.Kitchen),
+		)
 	}
 }
diff --git a/internal/cron/location.go b/internal/cron/location.go
index 8965581..b60d977 100644
--- a/internal/cron/location.go
+++ b/internal/cron/location.go
@@ -38,5 +38,5 @@ func describeOffset(offset int) string {
 func DescribeLocation(loc *time.Location) string {
 	_, offset := time.Now().In(loc).Zone()
 
-	return fmt.Sprintf("%s (%s now)", loc.String(), describeOffset(offset))
+	return fmt.Sprintf("%s (currently %s)", loc.String(), describeOffset(offset))
 }
diff --git a/internal/signal/signal.go b/internal/signal/signal.go
index 0d22355..22d26db 100644
--- a/internal/signal/signal.go
+++ b/internal/signal/signal.go
@@ -39,15 +39,18 @@ func (h Handle) TearDown() {
 	signal.Stop(h.channel)
 }
 
-// Sleep waits for a period of time. It returns false if it is interrupted by signals in [Signals].
-func (h Handle) Sleep(ppfmt pp.PP, d time.Duration) bool {
-	chanAlarm := time.After(d)
+// SleepUntil waits for a period of time. It returns false if it is interrupted by signals in [Signals].
+func (h Handle) SleepUntil(ppfmt pp.PP, t time.Time) bool {
+	timer := time.NewTimer(time.Until(t))
 	for {
 		select {
 		case sig := <-h.channel:
+			if !timer.Stop() {
+				<-timer.C
+			}
 			ppfmt.Noticef(pp.EmojiSignal, "Caught signal: %v", sig)
 			return false
-		case <-chanAlarm:
+		case <-timer.C:
 			return true
 		}
 	}


Test patch:
diff --git a/internal/config/config_print_test.go b/internal/config/config_print_test.go
index d94c94a..80169c6 100644
--- a/internal/config/config_print_test.go
+++ b/internal/config/config_print_test.go
@@ -37,7 +37,7 @@ func TestPrintDefault(t *testing.T) {
 		printItem(innerMockPP, "IPv6 domains:", "(none)"),
 		printItem(innerMockPP, "IPv6 provider:", "cloudflare.trace"),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Scheduling:"),
-		printItem(innerMockPP, "Timezone:", gomock.AnyOf("UTC (UTC+00 now)", "Local (UTC+00 now)")),
+		printItem(innerMockPP, "Timezone:", gomock.AnyOf("UTC (currently UTC+00)", "Local (currently UTC+00)")),
 		printItem(innerMockPP, "Update frequency:", "@every 5m"),
 		printItem(innerMockPP, "Update on start?", "true"),
 		printItem(innerMockPP, "Delete on stop?", "false"),
@@ -70,7 +70,7 @@ func TestPrintMaps(t *testing.T) {
 		printItem(innerMockPP, "IPv6 domains:", "test6.org, *.test6.org"),
 		printItem(innerMockPP, "IPv6 provider:", "cloudflare.trace"),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Scheduling:"),
-		printItem(innerMockPP, "Timezone:", gomock.AnyOf("UTC (UTC+00 now)", "Local (UTC+00 now)")),
+		printItem(innerMockPP, "Timezone:", gomock.AnyOf("UTC (currently UTC+00)", "Local (currently UTC+00)")),
 		printItem(innerMockPP, "Update frequency:", "@every 5m"),
 		printItem(innerMockPP, "Update on start?", "true"),
 		printItem(innerMockPP, "Delete on stop?", "false"),
@@ -133,7 +133,7 @@ func TestPrintEmpty(t *testing.T) {
 		mockPP.EXPECT().IncIndent().Return(innerMockPP),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Domains and IP providers:"),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Scheduling:"),
-		printItem(innerMockPP, "Timezone:", gomock.AnyOf("UTC (UTC+00 now)", "Local (UTC+00 now)")),
+		printItem(innerMockPP, "Timezone:", gomock.AnyOf("UTC (currently UTC+00)", "Local (currently UTC+00)")),
 		printItem(innerMockPP, "Update frequency:", "@once"),
 		printItem(innerMockPP, "Update on start?", "false"),
 		printItem(innerMockPP, "Delete on stop?", "false"),
diff --git a/internal/cron/countdown_test.go b/internal/cron/countdown_test.go
index e768ae5..f6a0c90 100644
--- a/internal/cron/countdown_test.go
+++ b/internal/cron/countdown_test.go
@@ -18,31 +18,49 @@ func TestPrintCountdown(t *testing.T) {
 
 	for _, tc := range [...]struct {
 		interval []time.Duration
-		output   string
+		output   func(string) string
 	}{
 		{
 			[]time.Duration{-20 * time.Second},
-			string(pp.EmojiNow) + " Secretly dancing now (running behind by 20s) . . .\n",
+			func(_ string) string {
+				return string(pp.EmojiNow) + " Secretly dancing now (running behind by 20s) . . .\n"
+			},
 		},
 		{
 			[]time.Duration{-10 * time.Second},
-			string(pp.EmojiNow) + " Secretly dancing now (running behind by 10s) . . .\n",
+			func(_ string) string {
+				return string(pp.EmojiNow) + " Secretly dancing now (running behind by 10s) . . .\n"
+			},
 		},
 		{
 			[]time.Duration{-time.Second, -time.Nanosecond, 0, time.Nanosecond},
-			string(pp.EmojiNow) + " Secretly dancing now . . .\n",
+			func(_ string) string {
+				return string(pp.EmojiNow) + " Secretly dancing now . . .\n"
+			},
 		},
 		{
-			[]time.Duration{time.Second},
-			string(pp.EmojiAlarm) + " Secretly dancing in less than 5s . . .\n",
+			[]time.Duration{2 * time.Second},
+			func(_ string) string {
+				return string(pp.EmojiAlarm) + " Secretly dancing in less than 5s . . .\n"
+			},
 		},
 		{
 			[]time.Duration{10 * time.Second},
-			string(pp.EmojiAlarm) + " Secretly dancing in about 10s . . .\n",
+			func(_ string) string {
+				return string(pp.EmojiAlarm) + " Secretly dancing in about 10s . . .\n"
+			},
 		},
 		{
 			[]time.Duration{20 * time.Second},
-			string(pp.EmojiAlarm) + " Secretly dancing in about 20s . . .\n",
+			func(_ string) string {
+				return string(pp.EmojiAlarm) + " Secretly dancing in about 20s . . .\n"
+			},
+		},
+		{
+			[]time.Duration{20 * time.Minute},
+			func(t string) string {
+				return string(pp.EmojiAlarm) + " Secretly dancing in about 20m0s (" + t + ") . . .\n"
+			},
 		},
 	} {
 		for _, interval := range tc.interval {
@@ -51,9 +69,9 @@ func TestPrintCountdown(t *testing.T) {
 				var buf strings.Builder
 				pp := pp.New(&buf)
 
-				cron.PrintCountdown(pp, activity, interval)
-
-				require.Equal(t, tc.output, buf.String())
+				target := time.Now().Add(interval)
+				cron.PrintCountdown(pp, activity, target)
+				require.Equal(t, tc.output(target.Format(time.Kitchen)), buf.String())
 			})
 		}
 	}
diff --git a/internal/cron/location_test.go b/internal/cron/location_test.go
index 4bc8c38..f60df15 100644
--- a/internal/cron/location_test.go
+++ b/internal/cron/location_test.go
@@ -24,14 +24,14 @@ func TestDescribeLocation(t *testing.T) {
 		input  *time.Location
 		output string
 	}{
-		{time.UTC, "UTC (UTC+00 now)"},
-		{mustLoadLocation("Asia/Thimphu"), "Asia/Thimphu (UTC+06 now)"},
-		{mustLoadLocation("Asia/Seoul"), "Asia/Seoul (UTC+09 now)"},
-		{mustLoadLocation("Asia/Shanghai"), "Asia/Shanghai (UTC+08 now)"},
-		{mustLoadLocation("Asia/Kolkata"), "Asia/Kolkata (UTC+05:30 now)"},
-		{mustLoadLocation("America/Port_of_Spain"), "America/Port_of_Spain (UTC−04 now)"},
-		{time.FixedZone("Dublin Mean Time", -1521), "Dublin Mean Time (UTC−00:25:21 now)"},
-		{time.FixedZone("Bangkok Mean Time", 24124), "Bangkok Mean Time (UTC+06:42:04 now)"},
+		{time.UTC, "UTC (currently UTC+00)"},
+		{mustLoadLocation("Asia/Thimphu"), "Asia/Thimphu (currently UTC+06)"},
+		{mustLoadLocation("Asia/Seoul"), "Asia/Seoul (currently UTC+09)"},
+		{mustLoadLocation("Asia/Shanghai"), "Asia/Shanghai (currently UTC+08)"},
+		{mustLoadLocation("Asia/Kolkata"), "Asia/Kolkata (currently UTC+05:30)"},
+		{mustLoadLocation("America/Port_of_Spain"), "America/Port_of_Spain (currently UTC−04)"},
+		{time.FixedZone("Dublin Mean Time", -1521), "Dublin Mean Time (currently UTC−00:25:21)"},
+		{time.FixedZone("Bangkok Mean Time", 24124), "Bangkok Mean Time (currently UTC+06:42:04)"},
 	} {
 		t.Run(tc.input.String(), func(t *testing.T) {
 			t.Parallel()
diff --git a/internal/signal/signal_test.go b/internal/signal/signal_test.go
index 2c7bb86..d455a47 100644
--- a/internal/signal/signal_test.go
+++ b/internal/signal/signal_test.go
@@ -57,7 +57,8 @@ func TestSleep(t *testing.T) {
 
 			sig := signal.Setup()
 			go signalSelf()
-			res := sig.Sleep(mockPP, tc.alarmDelay)
+			target := time.Now().Add(tc.alarmDelay)
+			res := sig.SleepUntil(mockPP, target)
 			<-done
 			sig.TearDown()