CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/golang-github-com-go-co-op-gocron-v2

A Golang job scheduling library that lets you run Go functions at pre-determined intervals using cron expressions, fixed durations, daily, weekly, monthly, or one-time schedules with support for distributed deployments.

Overview
Eval results
Files

lifecycle-monitoring.mddocs/guides/observability/

Lifecycle Monitoring

Detailed guide to monitoring job and scheduler lifecycle events.

Overview

The SchedulerMonitor and Monitor interfaces provide comprehensive lifecycle tracking for jobs and scheduler operations.

SchedulerMonitor Interface

Monitor scheduler-level events:

type SchedulerMonitor interface {
    JobScheduled(jobID uuid.UUID, job Job)
    JobUnscheduled(jobID uuid.UUID, job Job)
    JobStarted(jobID uuid.UUID, job Job)
    JobCompleted(jobID uuid.UUID, job Job, err error)
    ConcurrencyLimitReached(limitType string, job Job)
}

Implementation

type mySchedulerMonitor struct{}

func (m *mySchedulerMonitor) JobScheduled(jobID uuid.UUID, job gocron.Job) {
    log.Printf("[SCHEDULED] %s (ID: %s) next run: %v",
        job.Name(), jobID, job.NextRun())
}

func (m *mySchedulerMonitor) JobUnscheduled(jobID uuid.UUID, job gocron.Job) {
    log.Printf("[UNSCHEDULED] %s (ID: %s)", job.Name(), jobID)
}

func (m *mySchedulerMonitor) JobStarted(jobID uuid.UUID, job gocron.Job) {
    log.Printf("[STARTED] %s (ID: %s)", job.Name(), jobID)
}

func (m *mySchedulerMonitor) JobCompleted(jobID uuid.UUID, job gocron.Job, err error) {
    if err != nil {
        log.Printf("[FAILED] %s (ID: %s): %v", job.Name(), jobID, err)
    } else {
        log.Printf("[COMPLETED] %s (ID: %s)", job.Name(), jobID)
    }
}

func (m *mySchedulerMonitor) ConcurrencyLimitReached(limitType string, job gocron.Job) {
    log.Printf("[LIMIT] %s limit reached for %s", limitType, job.Name())
}

s, _ := gocron.NewScheduler(
    gocron.WithSchedulerMonitor(&mySchedulerMonitor{}),
)

Monitor Interface

Monitor job timing metrics:

type Monitor interface {
    RecordJobTiming(
        start time.Time,
        duration time.Duration,
        jobID uuid.UUID,
        jobName string,
        tags []string,
    )
}

Implementation

type myMonitor struct{}

func (m *myMonitor) RecordJobTiming(
    start time.Time,
    duration time.Duration,
    jobID uuid.UUID,
    jobName string,
    tags []string,
) {
    log.Printf("[TIMING] %s took %v (tags: %v)",
        jobName, duration, tags)
}

s, _ := gocron.NewScheduler(
    gocron.WithMonitor(&myMonitor{}),
)

MonitorStatus Interface

Track job status changes:

type MonitorStatus interface {
    UpdateStatus(jobID uuid.UUID, jobName string, status JobStatus)
}

type JobStatus int
const (
    Scheduled JobStatus = iota
    Running
    Completed
    Failed
)

Implementation

type myStatusMonitor struct {
    statuses map[uuid.UUID]gocron.JobStatus
    mu       sync.Mutex
}

func (m *myStatusMonitor) UpdateStatus(
    jobID uuid.UUID,
    jobName string,
    status gocron.JobStatus,
) {
    m.mu.Lock()
    defer m.mu.Unlock()

    m.statuses[jobID] = status
    log.Printf("[STATUS] %s -> %v", jobName, status)
}

s, _ := gocron.NewScheduler(
    gocron.WithMonitorStatus(&myStatusMonitor{
        statuses: make(map[uuid.UUID]gocron.JobStatus),
    }),
)

Event Sequence

Typical event sequence for a job execution:

1. JobScheduled    - Job added to scheduler
2. JobStarted      - Execution begins
3. RecordJobTiming - Execution completes (Monitor)
4. JobCompleted    - Execution finishes (SchedulerMonitor)
5. UpdateStatus    - Status updated (MonitorStatus)

Combining Monitors

Use multiple monitors together:

s, _ := gocron.NewScheduler(
    gocron.WithSchedulerMonitor(&lifecycleMonitor{}),
    gocron.WithMonitor(&metricsMonitor{}),
    gocron.WithMonitorStatus(&statusMonitor{}),
)

Practical Examples

Alerting on Failures

type alertingMonitor struct{}

func (m *alertingMonitor) JobCompleted(
    jobID uuid.UUID,
    job gocron.Job,
    err error,
) {
    if err != nil {
        // Send alert
        sendAlert(fmt.Sprintf("Job %s failed: %v", job.Name(), err))
    }
}

Tracking Execution Times

type timingMonitor struct {
    durations map[string][]time.Duration
    mu        sync.Mutex
}

func (m *timingMonitor) RecordJobTiming(
    start time.Time,
    duration time.Duration,
    jobID uuid.UUID,
    jobName string,
    tags []string,
) {
    m.mu.Lock()
    defer m.mu.Unlock()

    m.durations[jobName] = append(m.durations[jobName], duration)

    // Alert if duration exceeds threshold
    if duration > 5*time.Minute {
        log.Printf("SLOW: %s took %v", jobName, duration)
    }
}

Queue Depth Monitoring

type queueMonitor struct{}

func (m *queueMonitor) ConcurrencyLimitReached(
    limitType string,
    job gocron.Job,
) {
    log.Printf("Queue building up for %s", job.Name())

    // Check queue depth
    if s.JobsWaitingInQueue() > 10 {
        sendAlert("Queue depth exceeded threshold")
    }
}

Integration with Observability Platforms

Prometheus

import "github.com/prometheus/client_golang/prometheus"

var (
    jobExecutions = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "gocron_job_executions_total",
            Help: "Total job executions",
        },
        []string{"job_name", "status"},
    )

    jobDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name: "gocron_job_duration_seconds",
            Help: "Job execution duration",
        },
        []string{"job_name"},
    )
)

type prometheusMonitor struct{}

func (m *prometheusMonitor) JobCompleted(
    jobID uuid.UUID,
    job gocron.Job,
    err error,
) {
    status := "success"
    if err != nil {
        status = "error"
    }
    jobExecutions.WithLabelValues(job.Name(), status).Inc()
}

func (m *prometheusMonitor) RecordJobTiming(
    start time.Time,
    duration time.Duration,
    jobID uuid.UUID,
    jobName string,
    tags []string,
) {
    jobDuration.WithLabelValues(jobName).Observe(duration.Seconds())
}

DataDog

import "github.com/DataDog/datadog-go/statsd"

type datadogMonitor struct {
    client *statsd.Client
}

func (m *datadogMonitor) RecordJobTiming(
    start time.Time,
    duration time.Duration,
    jobID uuid.UUID,
    jobName string,
    tags []string,
) {
    m.client.Timing("gocron.job.duration", duration, tags, 1.0)
    m.client.Incr("gocron.job.executions", tags, 1.0)
}

func (m *datadogMonitor) JobCompleted(
    jobID uuid.UUID,
    job gocron.Job,
    err error,
) {
    tags := []string{fmt.Sprintf("job:%s", job.Name())}
    if err != nil {
        tags = append(tags, "status:error")
        m.client.Incr("gocron.job.errors", tags, 1.0)
    } else {
        tags = append(tags, "status:success")
    }
}

Best Practices

1. Keep Monitors Lightweight

// Good: async processing
func (m *myMonitor) JobCompleted(jobID uuid.UUID, job gocron.Job, err error) {
    go func() {
        // Send to external system
        sendMetric(job.Name(), err)
    }()
}

// Bad: blocking operation
func (m *myMonitor) JobCompleted(jobID uuid.UUID, job gocron.Job, err error) {
    // Blocks job completion
    sendMetricSync(job.Name(), err)
}

2. Handle Monitor Errors

func (m *myMonitor) RecordJobTiming(...) {
    if err := m.client.Send(...); err != nil {
        // Log but don't fail
        log.Printf("Failed to record metric: %v", err)
    }
}

3. Use Appropriate Cardinality

// Good: low cardinality
prometheus.JobDuration.WithLabelValues(job.Name())

// Bad: high cardinality (unique IDs)
prometheus.JobDuration.WithLabelValues(job.ID().String())

See Also

  • Observability Guide - Overview
  • Logging Guide - Logger setup
  • Metrics Guide - Metrics integration
  • API: Monitoring Types - Interface reference

Install with Tessl CLI

npx tessl i tessl/golang-github-com-go-co-op-gocron-v2

docs

index.md

tile.json