Configure logging for the cron scheduler to track operations, errors, and job execution.
The cron package uses a simple logging interface compatible with structured logging systems.
// Logger is the interface used in this package for logging.
// It is a subset of the github.com/go-logr/logr interface.
type Logger interface {
// Info logs routine messages about cron's operation
Info(msg string, keysAndValues ...interface{})
// Error logs an error condition
Error(err error, msg string, keysAndValues ...interface{})
}// DefaultLogger is used by Cron if none is specified.
// Writes to stdout with standard log formatting.
var DefaultLogger LoggerUsage:
// Cron uses DefaultLogger automatically
c := cron.New()
// Or explicitly
c := cron.New(cron.WithLogger(cron.DefaultLogger))Output:
cron: 2024/01/15 10:30:00 start
cron: 2024/01/15 10:30:00 schedule, now=2024-01-15T10:30:00Z, entry=1, next=2024-01-15T11:00:00Z
cron: 2024/01/15 11:00:00 wake, now=2024-01-15T11:00:00Z
cron: 2024/01/15 11:00:00 run, now=2024-01-15T11:00:00Z, entry=1, next=2024-01-15T12:00:00Z// DiscardLogger can be used by callers to discard all log messages.
var DiscardLogger LoggerUsage:
// Disable all logging
c := cron.New(cron.WithLogger(cron.DiscardLogger))Adapters for standard library log.Logger or any Printf-compatible logger.
Logs errors only (Info messages are discarded).
// PrintfLogger wraps a Printf-based logger (such as *log.Logger)
// into an implementation of the Logger interface which logs errors only.
func PrintfLogger(l interface{ Printf(string, ...interface{}) }) LoggerUsage:
import (
"log"
"os"
"github.com/robfig/cron/v3"
)
// Standard library logger
stdLogger := log.New(os.Stdout, "cron: ", log.LstdFlags)
// Wrap it (errors only)
logger := cron.PrintfLogger(stdLogger)
c := cron.New(cron.WithLogger(logger))Logs everything (both Info and Error messages).
// VerbosePrintfLogger wraps a Printf-based logger (such as *log.Logger)
// into an implementation of the Logger interface which logs everything.
func VerbosePrintfLogger(l interface{ Printf(string, ...interface{}) }) LoggerUsage:
import (
"log"
"os"
"github.com/robfig/cron/v3"
)
// Standard library logger
stdLogger := log.New(os.Stdout, "cron: ", log.LstdFlags)
// Wrap it (verbose)
logger := cron.VerbosePrintfLogger(stdLogger)
c := cron.New(cron.WithLogger(logger))Output:
cron: 2024/01/15 10:30:00 start
cron: 2024/01/15 10:30:00 schedule, now=2024-01-15T10:30:00Z, entry=1, next=2024-01-15T11:00:00Z
cron: 2024/01/15 10:30:05 added, now=2024-01-15T10:30:05Z, entry=2, next=2024-01-15T11:00:00Z
cron: 2024/01/15 11:00:00 wake, now=2024-01-15T11:00:00Z
cron: 2024/01/15 11:00:00 run, now=2024-01-15T11:00:00Z, entry=1, next=2024-01-15T12:00:00Z
cron: 2024/01/15 11:00:00 run, now=2024-01-15T11:00:00Z, entry=2, next=2024-01-15T12:00:00Zstart - Scheduler started
cron: startschedule - Job scheduled initially
cron: schedule, now=2024-01-15T10:30:00Z, entry=1, next=2024-01-15T11:00:00Zadded - Job added while running
cron: added, now=2024-01-15T10:30:05Z, entry=2, next=2024-01-15T11:00:00Zwake - Scheduler woke up to check for jobs
cron: wake, now=2024-01-15T11:00:00Zrun - Job executed
cron: run, now=2024-01-15T11:00:00Z, entry=1, next=2024-01-15T12:00:00Zremoved - Job removed from scheduler
cron: removed, entry=1stop - Scheduler stopped
cron: stopdelay - Job execution delayed (from DelayIfStillRunning wrapper)
cron: delay, duration=2m30sskip - Job execution skipped (from SkipIfStillRunning wrapper)
cron: skippanic - Job panicked (from Recover wrapper)
cron: 2024/01/15 11:00:00 panic, error=runtime error: index out of range, stack=...
goroutine 10 [running]:
...Implement the Logger interface for custom logging backends.
type Logger interface {
Info(msg string, keysAndValues ...interface{})
Error(err error, msg string, keysAndValues ...interface{})
}import (
"encoding/json"
"log"
"os"
)
type JSONLogger struct {
logger *log.Logger
}
func NewJSONLogger() *JSONLogger {
return &JSONLogger{
logger: log.New(os.Stdout, "", 0),
}
}
func (l *JSONLogger) Info(msg string, keysAndValues ...interface{}) {
entry := map[string]interface{}{
"level": "info",
"message": msg,
}
// Parse key-value pairs
for i := 0; i < len(keysAndValues); i += 2 {
if i+1 < len(keysAndValues) {
entry[keysAndValues[i].(string)] = keysAndValues[i+1]
}
}
json, _ := json.Marshal(entry)
l.logger.Println(string(json))
}
func (l *JSONLogger) Error(err error, msg string, keysAndValues ...interface{}) {
entry := map[string]interface{}{
"level": "error",
"message": msg,
"error": err.Error(),
}
// Parse key-value pairs
for i := 0; i < len(keysAndValues); i += 2 {
if i+1 < len(keysAndValues) {
entry[keysAndValues[i].(string)] = keysAndValues[i+1]
}
}
json, _ := json.Marshal(entry)
l.logger.Println(string(json))
}
// Usage
logger := NewJSONLogger()
c := cron.New(cron.WithLogger(logger))Output:
{"level":"info","message":"start"}
{"level":"info","message":"schedule","now":"2024-01-15T10:30:00Z","entry":1,"next":"2024-01-15T11:00:00Z"}
{"level":"info","message":"run","now":"2024-01-15T11:00:00Z","entry":1,"next":"2024-01-15T12:00:00Z"}import (
"fmt"
"log/syslog"
)
type SyslogLogger struct {
writer *syslog.Writer
}
func NewSyslogLogger() (*SyslogLogger, error) {
writer, err := syslog.New(syslog.LOG_INFO|syslog.LOG_DAEMON, "cron")
if err != nil {
return nil, err
}
return &SyslogLogger{writer: writer}, nil
}
func (l *SyslogLogger) Info(msg string, keysAndValues ...interface{}) {
l.writer.Info(fmt.Sprintf("%s %v", msg, keysAndValues))
}
func (l *SyslogLogger) Error(err error, msg string, keysAndValues ...interface{}) {
l.writer.Err(fmt.Sprintf("%s: %v %v", msg, err, keysAndValues))
}
// Usage
logger, _ := NewSyslogLogger()
c := cron.New(cron.WithLogger(logger))type MultiLogger struct {
loggers []cron.Logger
}
func NewMultiLogger(loggers ...cron.Logger) *MultiLogger {
return &MultiLogger{loggers: loggers}
}
func (m *MultiLogger) Info(msg string, keysAndValues ...interface{}) {
for _, logger := range m.loggers {
logger.Info(msg, keysAndValues...)
}
}
func (m *MultiLogger) Error(err error, msg string, keysAndValues ...interface{}) {
for _, logger := range m.loggers {
logger.Error(err, msg, keysAndValues...)
}
}
// Usage: log to both console and file
fileLogger := cron.VerbosePrintfLogger(log.New(file, "cron: ", log.LstdFlags))
consoleLogger := cron.VerbosePrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags))
multiLogger := NewMultiLogger(fileLogger, consoleLogger)
c := cron.New(cron.WithLogger(multiLogger))The Logger interface is compatible with github.com/go-logr/logr:
import (
"github.com/go-logr/logr"
"github.com/go-logr/zapr"
"go.uber.org/zap"
"github.com/robfig/cron/v3"
)
// Create zap logger
zapLog, _ := zap.NewProduction()
// Wrap with logr
logger := zapr.NewLogger(zapLog)
// Use with cron (logr.Logger implements cron.Logger interface)
c := cron.New(cron.WithLogger(logger))import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/robfig/cron/v3"
)
type ZerologAdapter struct {
logger zerolog.Logger
}
func (z *ZerologAdapter) Info(msg string, keysAndValues ...interface{}) {
event := z.logger.Info()
for i := 0; i < len(keysAndValues); i += 2 {
if i+1 < len(keysAndValues) {
key := keysAndValues[i].(string)
event = event.Interface(key, keysAndValues[i+1])
}
}
event.Msg(msg)
}
func (z *ZerologAdapter) Error(err error, msg string, keysAndValues ...interface{}) {
event := z.logger.Error().Err(err)
for i := 0; i < len(keysAndValues); i += 2 {
if i+1 < len(keysAndValues) {
key := keysAndValues[i].(string)
event = event.Interface(key, keysAndValues[i+1])
}
}
event.Msg(msg)
}
// Usage
logger := &ZerologAdapter{logger: log.Logger}
c := cron.New(cron.WithLogger(logger))Use verbose logging to see all operations:
logger := cron.VerbosePrintfLogger(
log.New(os.Stdout, "cron: ", log.LstdFlags),
)
c := cron.New(cron.WithLogger(logger))Use quieter logging (errors only) or structured logging:
// Errors only
logger := cron.PrintfLogger(
log.New(os.Stderr, "cron: ", log.LstdFlags),
)
// Or verbose with file output
logFile, _ := os.OpenFile("/var/log/cron.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
logger := cron.VerbosePrintfLogger(
log.New(logFile, "", log.LstdFlags),
)
c := cron.New(cron.WithLogger(logger))Disable logging in tests unless debugging:
func TestCron(t *testing.T) {
c := cron.New(cron.WithLogger(cron.DiscardLogger))
// Test code...
}Always provide a logger to job wrappers:
logger := cron.VerbosePrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags))
c := cron.New(cron.WithChain(
cron.Recover(logger), // Needs logger for panic logs
cron.SkipIfStillRunning(logger), // Needs logger for skip logs
cron.DelayIfStillRunning(logger), // Needs logger for delay logs
))package main
import (
"fmt"
"log"
"os"
"time"
"github.com/robfig/cron/v3"
)
func main() {
// Setup logging to both console and file
logFile, err := os.OpenFile("cron.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatal(err)
}
defer logFile.Close()
// Verbose logger for console (development)
consoleLogger := cron.VerbosePrintfLogger(
log.New(os.Stdout, "cron: ", log.LstdFlags),
)
// Use verbose console logger (or switch to file logger in production)
logger := consoleLogger
// Create cron with logging
c := cron.New(
cron.WithLogger(logger),
cron.WithChain(
cron.Recover(logger),
cron.SkipIfStillRunning(logger),
),
)
// Add jobs
c.AddFunc("@every 10s", func() {
fmt.Println("Job 1 executing")
})
c.AddFunc("@every 15s", func() {
fmt.Println("Job 2 executing (long)")
time.Sleep(20 * time.Second)
})
// Start scheduler
c.Start()
defer c.Stop()
// Run for 1 minute
time.Sleep(1 * time.Minute)
}Output:
cron: 2024/01/15 10:30:00 start
cron: 2024/01/15 10:30:00 schedule, now=2024-01-15T10:30:00Z, entry=1, next=2024-01-15T10:30:10Z
cron: 2024/01/15 10:30:00 schedule, now=2024-01-15T10:30:00Z, entry=2, next=2024-01-15T10:30:15Z
cron: 2024/01/15 10:30:10 wake, now=2024-01-15T10:30:10Z
cron: 2024/01/15 10:30:10 run, now=2024-01-15T10:30:10Z, entry=1, next=2024-01-15T10:30:20Z
Job 1 executing
cron: 2024/01/15 10:30:15 wake, now=2024-01-15T10:30:15Z
cron: 2024/01/15 10:30:15 run, now=2024-01-15T10:30:15Z, entry=2, next=2024-01-15T10:30:30Z
Job 2 executing (long)
cron: 2024/01/15 10:30:20 wake, now=2024-01-15T10:30:20Z
cron: 2024/01/15 10:30:20 run, now=2024-01-15T10:30:20Z, entry=1, next=2024-01-15T10:30:30Z
Job 1 executing
cron: 2024/01/15 10:30:30 wake, now=2024-01-15T10:30:30Z
cron: 2024/01/15 10:30:30 run, now=2024-01-15T10:30:30Z, entry=1, next=2024-01-15T10:30:40Z
cron: 2024/01/15 10:30:30 skip
Job 1 executing
cron: 2024/01/15 10:31:00 stop