diff --git a/cmd/cmd_renew.go b/cmd/cmd_renew.go index 1487d50e8..c36e4be6a 100644 --- a/cmd/cmd_renew.go +++ b/cmd/cmd_renew.go @@ -141,7 +141,7 @@ func renewForDomains(ctx context.Context, cmd *cli.Command, lazyClient lzSetUp, // This is just meant to be informal for the user. log.Info("acme: Trying renewal.", log.CertNameAttr(certID), - slog.Any("time-remaining", FormattableDuration(cert.NotAfter.Sub(time.Now().UTC()))), + slog.Any("time-remaining", log.FormattableDuration(cert.NotAfter.Sub(time.Now().UTC()))), ) err = hookManager.Pre(ctx, certID, renewalDomains) @@ -230,7 +230,7 @@ func renewForCSR(ctx context.Context, cmd *cli.Command, lazyClient lzSetUp, cert // This is just meant to be informal for the user. log.Info("acme: Trying renewal.", log.CertNameAttr(certID), - slog.Any("time-remaining", FormattableDuration(cert.NotAfter.Sub(time.Now().UTC()))), + slog.Any("time-remaining", log.FormattableDuration(cert.NotAfter.Sub(time.Now().UTC()))), ) err = hookManager.Pre(ctx, certID, certcrypto.ExtractDomainsCSR(csr)) @@ -286,7 +286,7 @@ func isInRenewalPeriod(cert *x509.Certificate, certID string, days int, now time log.Infof( log.LazySprintf("Skip renewal: The certificate expires at %s, the renewal can be performed in %s.", cert.NotAfter.Format(time.RFC3339), - FormattableDuration(dueDate.Sub(now)), + log.FormattableDuration(dueDate.Sub(now)), ), log.CertNameAttr(certID), ) @@ -434,43 +434,3 @@ func sameDomains(a, b []string) bool { return slices.Equal(aClone, bClone) } - -type FormattableDuration time.Duration - -func (f FormattableDuration) String() string { - d := time.Duration(f) - - days := int(math.Trunc(d.Hours() / 24)) - hours := int(d.Hours()) % 24 - minutes := int(d.Minutes()) % 60 - seconds := int(d.Seconds()) % 60 - ns := int(d.Nanoseconds()) % int(time.Second) - - s := new(strings.Builder) - - if days > 0 { - _, _ = fmt.Fprintf(s, "%dd", days) - } - - if hours > 0 { - _, _ = fmt.Fprintf(s, "%dh", hours) - } - - if minutes > 0 { - _, _ = fmt.Fprintf(s, "%dm", minutes) - } - - if seconds > 0 { - _, _ = fmt.Fprintf(s, "%ds", seconds) - } - - if ns > 0 { - _, _ = fmt.Fprintf(s, "%dns", ns) - } - - return s.String() -} - -func (f FormattableDuration) LogValue() slog.Value { - return slog.StringValue(f.String()) -} diff --git a/cmd/cmd_renew_test.go b/cmd/cmd_renew_test.go index edaf89d2f..78216cc0b 100644 --- a/cmd/cmd_renew_test.go +++ b/cmd/cmd_renew_test.go @@ -169,56 +169,3 @@ func Test_isInRenewalPeriod_dynamic(t *testing.T) { }) } } - -func TestFormattableDuration(t *testing.T) { - testCases := []struct { - desc string - date time.Time - duration time.Duration - expected string - }{ - { - desc: "all", - duration: 47*time.Hour + 3*time.Minute + 8*time.Second + 1234567890*time.Nanosecond, - expected: "1d23h3m9s234567890ns", - }, - { - desc: "without nanoseconds", - duration: 47*time.Hour + 3*time.Minute + 8*time.Second, - expected: "1d23h3m8s", - }, - { - desc: "without seconds", - duration: 47*time.Hour + 3*time.Minute + 2*time.Nanosecond, - expected: "1d23h3m2ns", - }, - { - desc: "without minutes", - duration: 47*time.Hour + 8*time.Second + 2*time.Nanosecond, - expected: "1d23h8s2ns", - }, - { - desc: "without hours", - duration: 3*time.Minute + 8*time.Second + 2*time.Nanosecond, - expected: "3m8s2ns", - }, - { - desc: "only hours", - duration: 23 * time.Hour, - expected: "23h", - }, - { - desc: "only days", - duration: 48 * time.Hour, - expected: "2d", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - assert.Equal(t, test.expected, FormattableDuration(test.duration).String()) - }) - } -} diff --git a/log/duration.go b/log/duration.go new file mode 100644 index 000000000..52b408840 --- /dev/null +++ b/log/duration.go @@ -0,0 +1,49 @@ +package log + +import ( + "fmt" + "log/slog" + "math" + "strings" + "time" +) + +type FormattableDuration time.Duration + +func (f FormattableDuration) String() string { + d := time.Duration(f) + + days := int(math.Trunc(d.Hours() / 24)) + hours := int(d.Hours()) % 24 + minutes := int(d.Minutes()) % 60 + seconds := int(d.Seconds()) % 60 + ns := int(d.Nanoseconds()) % int(time.Second) + + s := new(strings.Builder) + + if days > 0 { + _, _ = fmt.Fprintf(s, "%dd", days) + } + + if hours > 0 { + _, _ = fmt.Fprintf(s, "%dh", hours) + } + + if minutes > 0 { + _, _ = fmt.Fprintf(s, "%dm", minutes) + } + + if seconds > 0 { + _, _ = fmt.Fprintf(s, "%ds", seconds) + } + + if ns > 0 { + _, _ = fmt.Fprintf(s, "%dns", ns) + } + + return s.String() +} + +func (f FormattableDuration) LogValue() slog.Value { + return slog.StringValue(f.String()) +} diff --git a/log/duration_test.go b/log/duration_test.go new file mode 100644 index 000000000..3e25b5b1b --- /dev/null +++ b/log/duration_test.go @@ -0,0 +1,61 @@ +package log + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestFormattableDuration(t *testing.T) { + testCases := []struct { + desc string + date time.Time + duration time.Duration + expected string + }{ + { + desc: "all", + duration: 47*time.Hour + 3*time.Minute + 8*time.Second + 1234567890*time.Nanosecond, + expected: "1d23h3m9s234567890ns", + }, + { + desc: "without nanoseconds", + duration: 47*time.Hour + 3*time.Minute + 8*time.Second, + expected: "1d23h3m8s", + }, + { + desc: "without seconds", + duration: 47*time.Hour + 3*time.Minute + 2*time.Nanosecond, + expected: "1d23h3m2ns", + }, + { + desc: "without minutes", + duration: 47*time.Hour + 8*time.Second + 2*time.Nanosecond, + expected: "1d23h8s2ns", + }, + { + desc: "without hours", + duration: 3*time.Minute + 8*time.Second + 2*time.Nanosecond, + expected: "3m8s2ns", + }, + { + desc: "only hours", + duration: 23 * time.Hour, + expected: "23h", + }, + { + desc: "only days", + duration: 48 * time.Hour, + expected: "2d", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + assert.Equal(t, test.expected, FormattableDuration(test.duration).String()) + }) + } +}