Skip to content

Commit 43863da

Browse files
author
Rob Figueiredo
committed
cron/logger: update Logger to comply with logr
this makes it possible to use cron with key/value logging systems. BREAKING: WithVerboseLogger was changed to WithLogger. Callers should update by invoking: WithLogger(VerbosePrintfLogger(logger)) WithVerboseLogger was introduced in the most recent commit, so it's unlikely to affect many users. Fixes #202
1 parent 0275a3e commit 43863da

File tree

6 files changed

+107
-37
lines changed

6 files changed

+107
-37
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ It is currently IN DEVELOPMENT and will be considered released once a 3.0
1616
version is tagged. It is backwards INCOMPATIBLE with both the v1 and v2
1717
branches.
1818

19+
New features:
20+
21+
- Extensible, key/value logging via an interface that complies with
22+
the github.com/go-logr/logr project.
23+
1924
Updates required:
2025

2126
- The v1 branch accepted an optional seconds field at the beginning of the cron

cron.go

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cron
22

33
import (
4+
"fmt"
45
"runtime"
56
"sort"
67
"time"
@@ -17,7 +18,6 @@ type Cron struct {
1718
snapshot chan chan []Entry
1819
running bool
1920
logger Logger
20-
vlogger Logger
2121
location *time.Location
2222
parser Parser
2323
nextID EntryID
@@ -106,7 +106,6 @@ func New(opts ...Option) *Cron {
106106
remove: make(chan EntryID),
107107
running: false,
108108
logger: DefaultLogger,
109-
vlogger: nil,
110109
location: time.Local,
111110
parser: standardParser,
112111
}
@@ -213,7 +212,12 @@ func (c *Cron) runWithRecovery(j Job) {
213212
const size = 64 << 10
214213
buf := make([]byte, size)
215214
buf = buf[:runtime.Stack(buf, false)]
216-
c.logger.Printf("panic running job: %v\n%s", r, buf)
215+
var err error
216+
err, ok := r.(error)
217+
if !ok {
218+
err = fmt.Errorf("%v", r)
219+
}
220+
c.logger.Error(err, "panic running job", "stack", "...\n"+string(buf))
217221
}
218222
}()
219223
j.Run()
@@ -222,13 +226,13 @@ func (c *Cron) runWithRecovery(j Job) {
222226
// run the scheduler.. this is private just due to the need to synchronize
223227
// access to the 'running' state variable.
224228
func (c *Cron) run() {
225-
c.logVerbosef("cron is starting")
229+
c.logger.Info("start")
226230

227231
// Figure out the next activation times for each entry.
228232
now := c.now()
229233
for _, entry := range c.entries {
230234
entry.Next = entry.Schedule.Next(now)
231-
c.logVerbosef("(%s) scheduled entry %d for %s", now, entry.ID, entry.Next)
235+
c.logger.Info("schedule", "now", now, "entry", entry.ID, "next", entry.Next)
232236
}
233237

234238
for {
@@ -248,7 +252,7 @@ func (c *Cron) run() {
248252
select {
249253
case now = <-timer.C:
250254
now = now.In(c.location)
251-
c.logVerbosef("(%s) woke up", now)
255+
c.logger.Info("wake", "now", now)
252256

253257
// Run every entry whose next time was less than now
254258
for _, e := range c.entries {
@@ -258,52 +262,37 @@ func (c *Cron) run() {
258262
go c.runWithRecovery(e.Job)
259263
e.Prev = e.Next
260264
e.Next = e.Schedule.Next(now)
261-
c.logVerbosef("(%s) started entry %d, next scheduled for %s", now, e.ID, e.Next)
265+
c.logger.Info("run", "now", now, "entry", e.ID, "next", e.Next)
262266
}
263267

264268
case newEntry := <-c.add:
265269
timer.Stop()
266270
now = c.now()
267271
newEntry.Next = newEntry.Schedule.Next(now)
268272
c.entries = append(c.entries, newEntry)
269-
c.logVerbosef("(%s) added new entry %d, scheduled for", now, newEntry.ID, newEntry.Next)
273+
c.logger.Info("added", "now", now, "entry", newEntry.ID, "next", newEntry.Next)
270274

271275
case replyChan := <-c.snapshot:
272276
replyChan <- c.entrySnapshot()
273277
continue
274278

275279
case <-c.stop:
276280
timer.Stop()
277-
c.logVerbosef("cron is stopping")
281+
c.logger.Info("stop")
278282
return
279283

280284
case id := <-c.remove:
281285
timer.Stop()
282286
now = c.now()
283287
c.removeEntry(id)
284-
c.logVerbosef("removed entry %d", id)
288+
c.logger.Info("removed", "entry", id)
285289
}
286290

287291
break
288292
}
289293
}
290294
}
291295

292-
// logVerbosef logs a verbose message, if such a logger is configured.
293-
func (c *Cron) logVerbosef(format string, args ...interface{}) {
294-
if c.vlogger != nil {
295-
// Format any times provided as RFC3339, easier to read than default.
296-
var formattedArgs []interface{}
297-
for _, arg := range args {
298-
if t, ok := arg.(time.Time); ok {
299-
arg = t.Format(time.RFC3339)
300-
}
301-
formattedArgs = append(formattedArgs, arg)
302-
}
303-
c.vlogger.Printf(format, formattedArgs...)
304-
}
305-
}
306-
307296
// now returns current time in c location
308297
func (c *Cron) now() time.Time {
309298
return time.Now().In(c.location)

doc.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,14 @@ care must be taken to ensure proper synchronization.
158158
All cron methods are designed to be correctly synchronized as long as the caller
159159
ensures that invocations have a clear happens-before ordering between them.
160160
161+
Logging
162+
163+
Cron defines a Logger interface that is a subset of the one defined in
164+
github.com/go-logr/logr. It has two logging levels (Info and Error), and
165+
parameters are key/value pairs. This makes it possible for cron logging to plug
166+
into structured logging systems. An adapter, [Verbose]PrintfLogger, is provided
167+
to wrap the standard library *log.Logger.
168+
161169
Implementation
162170
163171
Cron entries are stored in an array, sorted by their next activation time. Cron

logger.go

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,80 @@ package cron
33
import (
44
"log"
55
"os"
6+
"strings"
7+
"time"
68
)
79

8-
var DefaultLogger = log.New(os.Stderr, "cron: ", log.LstdFlags)
10+
// DefaultLogger is used by Cron if none is specified.
11+
var DefaultLogger Logger = PrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags))
912

1013
// Logger is the interface used in this package for logging, so that any backend
11-
// can be easily plugged in. It's implemented directly by "log" and logrus.
14+
// can be plugged in. It is a subset of the github.com/go-logr/logr interface.
1215
type Logger interface {
13-
Printf(string, ...interface{})
16+
// Info logs routine messages about cron's operation.
17+
Info(msg string, keysAndValues ...interface{})
18+
// Error logs an error condition.
19+
Error(err error, msg string, keysAndValues ...interface{})
20+
}
21+
22+
// PrintfLogger wraps a Printf-based logger (such as the standard library "log")
23+
// into an implementation of the Logger interface which logs errors only.
24+
func PrintfLogger(l interface{ Printf(string, ...interface{}) }) Logger {
25+
return printfLogger{l, false}
26+
}
27+
28+
// VerbosePrintfLogger wraps a Printf-based logger (such as the standard library
29+
// "log") into an implementation of the Logger interface which logs everything.
30+
func VerbosePrintfLogger(l interface{ Printf(string, ...interface{}) }) Logger {
31+
return printfLogger{l, true}
32+
}
33+
34+
type printfLogger struct {
35+
logger interface{ Printf(string, ...interface{}) }
36+
logInfo bool
37+
}
38+
39+
func (pl printfLogger) Info(msg string, keysAndValues ...interface{}) {
40+
if pl.logInfo {
41+
keysAndValues = formatTimes(keysAndValues)
42+
pl.logger.Printf(
43+
formatString(len(keysAndValues)),
44+
append([]interface{}{msg}, keysAndValues...)...)
45+
}
46+
}
47+
48+
func (pl printfLogger) Error(err error, msg string, keysAndValues ...interface{}) {
49+
keysAndValues = formatTimes(keysAndValues)
50+
pl.logger.Printf(
51+
formatString(len(keysAndValues)+2),
52+
append([]interface{}{msg, "error", err}, keysAndValues...)...)
53+
}
54+
55+
// formatString returns a logfmt-like format string for the number of
56+
// key/values.
57+
func formatString(numKeysAndValues int) string {
58+
var sb strings.Builder
59+
sb.WriteString("%s")
60+
if numKeysAndValues > 0 {
61+
sb.WriteString(", ")
62+
}
63+
for i := 0; i < numKeysAndValues/2; i++ {
64+
if i > 0 {
65+
sb.WriteString(", ")
66+
}
67+
sb.WriteString("%v=%v")
68+
}
69+
return sb.String()
70+
}
71+
72+
// formatTimes formats any time.Time values as RFC3339.
73+
func formatTimes(keysAndValues []interface{}) []interface{} {
74+
var formattedArgs []interface{}
75+
for _, arg := range keysAndValues {
76+
if t, ok := arg.(time.Time); ok {
77+
arg = t.Format(time.RFC3339)
78+
}
79+
formattedArgs = append(formattedArgs, arg)
80+
}
81+
return formattedArgs
1482
}

option.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,13 @@ func WithParser(p Parser) Option {
3333
// WithPanicLogger overrides the logger used for logging job panics.
3434
func WithPanicLogger(l *log.Logger) Option {
3535
return func(c *Cron) {
36-
c.logger = l
36+
c.logger = PrintfLogger(l)
3737
}
3838
}
3939

40-
// WithVerboseLogger enables verbose logging of events that occur in cron.
41-
func WithVerboseLogger(logger Logger) Option {
40+
// WithLogger uses the provided logger.
41+
func WithLogger(logger Logger) Option {
4242
return func(c *Cron) {
43-
c.vlogger = logger
43+
c.logger = logger
4444
}
4545
}

option_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,16 @@ func TestWithPanicLogger(t *testing.T) {
2727
var b bytes.Buffer
2828
var logger = log.New(&b, "", log.LstdFlags)
2929
c := New(WithPanicLogger(logger))
30-
if c.logger != logger {
30+
if c.logger.(printfLogger).logger != logger {
3131
t.Error("expected provided logger")
3232
}
3333
}
3434

3535
func TestWithVerboseLogger(t *testing.T) {
3636
var buf syncWriter
3737
var logger = log.New(&buf, "", log.LstdFlags)
38-
c := New(WithVerboseLogger(logger))
39-
if c.vlogger != logger {
38+
c := New(WithLogger(VerbosePrintfLogger(logger)))
39+
if c.logger.(printfLogger).logger != logger {
4040
t.Error("expected provided logger")
4141
}
4242

@@ -45,8 +45,8 @@ func TestWithVerboseLogger(t *testing.T) {
4545
time.Sleep(OneSecond)
4646
c.Stop()
4747
out := buf.String()
48-
if !strings.Contains(out, "scheduled entry") ||
49-
!strings.Contains(out, "started entry") {
48+
if !strings.Contains(out, "schedule,") ||
49+
!strings.Contains(out, "run,") {
5050
t.Error("expected to see some actions, got:", out)
5151
}
5252
}

0 commit comments

Comments
 (0)