Go integration

Wire Drumbeats into a Go service. Quick example, centralized setup using net/http, error handling, and production patterns.

This guide shows how to wire Drumbeats into a Go codebase. By the end you have a single drumbeats package that wraps any job with start / success / failure pings using the standard net/http client — no third-party dependencies.

Quick example

go
package main

import (
    "fmt"
    "net/http"
)

const monitor = "https://api.drumbeats.io/v1/ping/<monitor-id>"

func main() {
    http.Get(monitor + "/start")
    if err := runJob(); err != nil {
        http.Get(monitor + "/failure")
        fmt.Println("job failed:", err)
        return
    }
    http.Get(monitor + "/success")
}
package main

import (
    "fmt"
    "net/http"
)

const monitor = "https://api.drumbeats.io/v1/ping/<monitor-id>"

func main() {
    http.Get(monitor + "/start")
    if err := runJob(); err != nil {
        http.Get(monitor + "/failure")
        fmt.Println("job failed:", err)
        return
    }
    http.Get(monitor + "/success")
}

This minimal shape works but blocks on every ping. The centralized setup below adds a timeout and a defer-driven success/failure split.

Centralized setup

internal/monitoring/drumbeats.go
package monitoring

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "net/url"
    "os"
    "time"
)

var baseURL = func() string {
    if v := os.Getenv("DRUMBEATS_BASE_URL"); v != "" {
        return v
    }
    return "https://api.drumbeats.io/v1"
}()

var Monitors = map[string]string{
    "daily_backup":    "11111111-2222-3333-4444-555555555555",
    "hourly_sync":     "66666666-7777-8888-9999-aaaaaaaaaaaa",
    "newsletter_send": "bbbbbbbb-cccc-dddd-eeee-ffffffffffff",
}

var client = &http.Client{Timeout: 3 * time.Second}

func ping(monitorID, event, runID string, payload string) {
    u, _ := url.Parse(fmt.Sprintf("%s/ping/%s/%s", baseURL, monitorID, event))
    q := u.Query()
    if runID != "" {
        q.Set("run_id", runID)
    }
    u.RawQuery = q.Encode()

    if payload == "" {
        _, _ = client.Get(u.String())
        return
    }
    body, _ := json.Marshal(map[string]string{"payload": payload})
    _, _ = client.Post(u.String(), "application/json", bytes.NewReader(body))
}

// WithMonitor runs fn under the given monitor key and reports the result.
// Errors are propagated; ping failures are swallowed.
func WithMonitor(ctx context.Context, key, runID string, fn func() error) error {
    id, ok := Monitors[key]
    if !ok {
        return fmt.Errorf("unknown monitor key %q", key)
    }
    if runID == "" {
        runID = fmt.Sprintf("%s-%d", key, time.Now().UnixNano())
    }
    ping(id, "start", runID, "")
    if err := fn(); err != nil {
        ping(id, "failure", runID, err.Error())
        return err
    }
    ping(id, "success", runID, "")
    return nil
}
package monitoring

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "net/url"
    "os"
    "time"
)

var baseURL = func() string {
    if v := os.Getenv("DRUMBEATS_BASE_URL"); v != "" {
        return v
    }
    return "https://api.drumbeats.io/v1"
}()

var Monitors = map[string]string{
    "daily_backup":    "11111111-2222-3333-4444-555555555555",
    "hourly_sync":     "66666666-7777-8888-9999-aaaaaaaaaaaa",
    "newsletter_send": "bbbbbbbb-cccc-dddd-eeee-ffffffffffff",
}

var client = &http.Client{Timeout: 3 * time.Second}

func ping(monitorID, event, runID string, payload string) {
    u, _ := url.Parse(fmt.Sprintf("%s/ping/%s/%s", baseURL, monitorID, event))
    q := u.Query()
    if runID != "" {
        q.Set("run_id", runID)
    }
    u.RawQuery = q.Encode()

    if payload == "" {
        _, _ = client.Get(u.String())
        return
    }
    body, _ := json.Marshal(map[string]string{"payload": payload})
    _, _ = client.Post(u.String(), "application/json", bytes.NewReader(body))
}

// WithMonitor runs fn under the given monitor key and reports the result.
// Errors are propagated; ping failures are swallowed.
func WithMonitor(ctx context.Context, key, runID string, fn func() error) error {
    id, ok := Monitors[key]
    if !ok {
        return fmt.Errorf("unknown monitor key %q", key)
    }
    if runID == "" {
        runID = fmt.Sprintf("%s-%d", key, time.Now().UnixNano())
    }
    ping(id, "start", runID, "")
    if err := fn(); err != nil {
        ping(id, "failure", runID, err.Error())
        return err
    }
    ping(id, "success", runID, "")
    return nil
}

Apply it at any call site:

go
err := monitoring.WithMonitor(ctx, "daily_backup", "", func() error {
    if err := dumpDatabase(); err != nil {
        return fmt.Errorf("dump: %w", err)
    }
    return uploadToS3()
})
err := monitoring.WithMonitor(ctx, "daily_backup", "", func() error {
    if err := dumpDatabase(); err != nil {
        return fmt.Errorf("dump: %w", err)
    }
    return uploadToS3()
})

Error handling

  • Panics. WithMonitor does not recover from panics. Wrap the inner function with defer recover() if your workload can panic and you want the failure to surface as a Drumbeats incident instead of crashing the process.
  • Context cancellation. If the caller's context is canceled, the inner function should return an error so WithMonitor sends the failure ping. Do not silently swallow ctx.Err().
  • Goroutine fan-out. If you spawn goroutines inside the wrapped function, do not return from WithMonitor until they finish (errgroup.Wait() or sync.WaitGroup). Otherwise the success ping fires before the work is done.

Production patterns

  • Time-bound pings. The package-level client enforces a 3-second timeout. A slow Drumbeats request never blocks the workload.
  • Cap payload size. Truncate long error strings before passing them to ping; the cap is 25KB per Beats and usage.
  • Worker pool. For high-throughput queue workers, call WithMonitor per message with a stable runID (e.g. the message ID). Drumbeats correlates start/success by run_id, so 100 concurrent messages produce 100 distinct runs.
  • Slug-based for multi-environment. Use /v1/s-ping/<project>/<slug>/<event> if the same binary runs in staging and prod — see Ping API: scheduled pings.
  • Monitor types — pick Cron, Heartbeat, or Event-driven first.
  • Ping API — endpoint and parameter reference.
  • Production hardening — retries, timeouts, observability for any language.
  • Alternatives — how Drumbeats compares to Cronitor and Healthchecks.io for Go services.