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.
WithMonitordoes not recover from panics. Wrap the inner function withdefer 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
WithMonitorsends the failure ping. Do not silently swallowctx.Err(). - Goroutine fan-out. If you spawn goroutines inside the wrapped function, do not return from
WithMonitoruntil they finish (errgroup.Wait()orsync.WaitGroup). Otherwise the success ping fires before the work is done.
Production patterns
- Time-bound pings. The package-level
clientenforces 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
WithMonitorper message with a stablerunID(e.g. the message ID). Drumbeats correlatesstart/successbyrun_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.
Related guides
- 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.