or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

configuration-options.mdcore-scheduler.mdindex.mdjob-management.mdjob-wrappers.mdlogging.mdschedule-parsing.mdschedule-types.md
tile.json

logging.mddocs/

Logging

Configure logging for the cron scheduler to track operations, errors, and job execution.

Logger Interface

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{})
}

Built-in Loggers

DefaultLogger

// DefaultLogger is used by Cron if none is specified.
// Writes to stdout with standard log formatting.
var DefaultLogger Logger

Usage:

// 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

// DiscardLogger can be used by callers to discard all log messages.
var DiscardLogger Logger

Usage:

// Disable all logging
c := cron.New(cron.WithLogger(cron.DiscardLogger))

Printf-Based Loggers

Adapters for standard library log.Logger or any Printf-compatible logger.

PrintfLogger

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{}) }) Logger

Usage:

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))

VerbosePrintfLogger

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{}) }) Logger

Usage:

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:00Z

Log Messages

Info Level Messages

start - Scheduler started

cron: start

schedule - Job scheduled initially

cron: schedule, now=2024-01-15T10:30:00Z, entry=1, next=2024-01-15T11:00:00Z

added - Job added while running

cron: added, now=2024-01-15T10:30:05Z, entry=2, next=2024-01-15T11:00:00Z

wake - Scheduler woke up to check for jobs

cron: wake, now=2024-01-15T11:00:00Z

run - Job executed

cron: run, now=2024-01-15T11:00:00Z, entry=1, next=2024-01-15T12:00:00Z

removed - Job removed from scheduler

cron: removed, entry=1

stop - Scheduler stopped

cron: stop

delay - Job execution delayed (from DelayIfStillRunning wrapper)

cron: delay, duration=2m30s

skip - Job execution skipped (from SkipIfStillRunning wrapper)

cron: skip

Error Level Messages

panic - Job panicked (from Recover wrapper)

cron: 2024/01/15 11:00:00 panic, error=runtime error: index out of range, stack=...
goroutine 10 [running]:
...

Custom Logger Implementation

Implement the Logger interface for custom logging backends.

type Logger interface {
    Info(msg string, keysAndValues ...interface{})
    Error(err error, msg string, keysAndValues ...interface{})
}

Example: JSON Logger

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"}

Example: Syslog Logger

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))

Example: Multi-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))

Integration with Structured Logging

logr Integration

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))

Zerolog Integration

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))

Logging Best Practices

Development

Use verbose logging to see all operations:

logger := cron.VerbosePrintfLogger(
    log.New(os.Stdout, "cron: ", log.LstdFlags),
)
c := cron.New(cron.WithLogger(logger))

Production

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))

Testing

Disable logging in tests unless debugging:

func TestCron(t *testing.T) {
    c := cron.New(cron.WithLogger(cron.DiscardLogger))
    // Test code...
}

Job Wrappers

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
))

Complete Example

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