LogTide

Official Go SDK for LogTide with automatic batching, retry logic, circuit breaker pattern, Hub/Scope context isolation, and native OpenTelemetry integration.

Installation

go get github.com/logtide-dev/logtide-sdk-go

Requires Go 1.23 or later.

Quick Start

Global singleton (recommended)

package main

import (
    "context"
    logtide "github.com/logtide-dev/logtide-sdk-go"
)

func main() {
    flush := logtide.Init(logtide.ClientOptions{
        DSN:         "https://[email protected]",
        Service:     "my-service",
        Environment: "production",
        Release:     "v1.2.3",
    })
    defer flush()

    ctx := context.Background()
    logtide.Info(ctx, "Server started", map[string]any{"port": 8080})
    logtide.Error(ctx, "Connection failed", map[string]any{"host": "db.example.com"})
}

Explicit client

opts := logtide.NewClientOptions()
opts.DSN     = "https://[email protected]"
opts.Service = "my-service"

client, err := logtide.NewClient(opts)
if err != nil {
    log.Fatal(err)
}
defer client.Close()

client.Info(context.Background(), "Hello LogTide!", nil)

Configuration

opts := logtide.NewClientOptions()

// Identity
opts.DSN         = "https://[email protected]" // required
opts.Service     = "my-service"                               // required
opts.Release     = "v1.2.3"
opts.Environment = "production"
opts.Tags        = map[string]string{"region": "eu-west-1"}

// Batching
opts.BatchSize     = 100               // entries per HTTP batch (default: 100)
opts.FlushInterval = 5 * time.Second   // (default: 5s)
opts.FlushTimeout  = 10 * time.Second  // (default: 10s)

// Reliability
opts.MaxRetries             = 3
opts.RetryMinBackoff        = 1 * time.Second
opts.RetryMaxBackoff        = 60 * time.Second
opts.CircuitBreakerThreshold = 5               // failures before open (default: 5)
opts.CircuitBreakerTimeout  = 30 * time.Second

// Stack traces
opts.AttachStacktrace = logtide.Bool(true) // default: true

Logging Methods

All methods return an EventID (empty string if the entry was dropped).

ctx := context.Background()

client.Debug(ctx, "cache miss", map[string]any{"key": "user:42"})
client.Info(ctx, "request handled", map[string]any{"status": 200, "ms": 12})
client.Warn(ctx, "rate limit approaching", nil)
client.Error(ctx, "db query failed", map[string]any{"query": "SELECT ..."})
client.Critical(ctx, "out of memory", nil)

Hub & Scope

The Hub/Scope model lets you attach tags, breadcrumbs, and user info to all log entries within a logical unit of work — without passing extra parameters everywhere.

Global scope

logtide.ConfigureScope(func(s *logtide.Scope) {
    s.SetTag("region", "eu-west-1")
    s.SetUser(logtide.User{ID: "u123", Email: "[email protected]"})
})

Per-request isolation

logtide.PushScope()
defer logtide.PopScope()

logtide.ConfigureScope(func(s *logtide.Scope) {
    s.SetTag("request_id", requestID)
    s.AddBreadcrumb(&logtide.Breadcrumb{
        Category:  "auth",
        Message:   "user authenticated",
        Level:     logtide.LevelInfo,
        Timestamp: time.Now(),
    }, nil)
})

// All log calls here include request_id tag + breadcrumb
logtide.Info(ctx, "processing order", nil)

Per-request with explicit Hub

hub := logtide.CurrentHub().Clone()
hub.ConfigureScope(func(s *logtide.Scope) {
    s.SetTag("request_id", r.Header.Get("X-Request-ID"))
})

// Inject into context for use downstream
ctx := logtide.SetHubOnContext(r.Context(), hub)
hub.Info(ctx, "request received", nil)

Error Capture

result, err := db.Query(ctx, "SELECT ...")
if err != nil {
    // Captures the error with full stack trace automatically
    client.CaptureError(ctx, err, map[string]any{
        "query": "SELECT ...",
        "user":  userID,
    })
}

slog Handler

The logtideslog integration provides a log/slog handler, so existing slog-based logging flows to LogTide without code changes. Records honour the client's full pipeline — scope merge, processors, BeforeSend, sampling, and batching.

import (
    "log/slog"

    logtide "github.com/logtide-dev/logtide-sdk-go"
    "github.com/logtide-dev/logtide-sdk-go/integrations/logtideslog"
)

client, err := logtide.NewClient(logtide.ClientOptions{
    DSN:     "https://[email protected]",
    Service: "my-service",
})
if err != nil {
    log.Fatal(err)
}
defer client.Close()

// opts may be nil (defaults to slog.LevelInfo).
handler := logtideslog.New(client, &logtideslog.Options{Level: slog.LevelDebug})
slog.SetDefault(slog.New(handler))

slog.Info("user signed in", "user_id", 42, "plan", "pro")
slog.Error("checkout failed", "err", err, "order_id", 1234)

Attributes become entry metadata, slog groups become nested metadata objects, and attribute values implementing error are promoted to structured exceptions so server-side error grouping works.

OpenTelemetry Integration

Automatic trace context

tracer := otel.Tracer("my-service")

func handleRequest(ctx context.Context) {
    ctx, span := tracer.Start(ctx, "handle-request")
    defer span.End()

    // trace_id and span_id are included automatically
    client.Info(ctx, "Processing request", map[string]any{"user_id": 123})
}

Span exporter

import "github.com/logtide-dev/logtide-sdk-go/integrations/otelexport"

integration := otelexport.New()

flush := logtide.Init(logtide.ClientOptions{
    DSN:     "https://[email protected]",
    Service: "my-service",
    Integrations: func(defaults []logtide.Integration) []logtide.Integration {
        return append(defaults, integration)
    },
})
defer flush()

tp := sdktrace.NewTracerProvider(
    sdktrace.WithBatcher(integration.Exporter()),
)

Metric exporter

Export OpenTelemetry metrics (counters, gauges, histograms) to LogTide. Each data point becomes a log entry carrying the same service name, environment, tags, and resource attributes as your logs and traces. It is opt-in — register it only if you need metrics.

import (
    "go.opentelemetry.io/otel"
    sdkmetric "go.opentelemetry.io/otel/sdk/metric"

    "github.com/logtide-dev/logtide-sdk-go/integrations/otelmetric"
)

integration := otelmetric.New()

flush := logtide.Init(logtide.ClientOptions{
    DSN:     "https://[email protected]",
    Service: "my-service",
    Integrations: func(defaults []logtide.Integration) []logtide.Integration {
        return append(defaults, integration)
    },
})
defer flush()

// A PeriodicReader collects metrics on an interval and hands them to the exporter.
mp := sdkmetric.NewMeterProvider(
    sdkmetric.WithReader(sdkmetric.NewPeriodicReader(integration.Exporter())),
)
otel.SetMeterProvider(mp)

When exemplars are enabled and a sampled span is active during measurement, each linked data point's entry inherits the exemplar's trace_id/span_id, and the full exemplar list is recorded under metadata.metric.exemplars — correlating metrics with the traces that produced them.

Outbound trace propagation

Wrap an http.Client transport with WrapTransport to inject the W3C traceparent header on outbound requests, propagating the current trace context to downstream services. The trace context is sourced from the active OTel span, falling back to the scope carried by the request context. Requests with no active trace, or with a traceparent already set, pass through untouched.

// A nil base uses http.DefaultTransport.
client := &http.Client{Transport: logtide.WrapTransport(nil)}

// Pass the request-scoped context so the trace context propagates.
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.upstream.dev/users", nil)
resp, err := client.Do(req)

HTTP Middleware

The built-in middleware automatically isolates scope per request, sets HTTP tags, parses Traceparent headers, and adds request/response breadcrumbs.

import lnethttp "github.com/logtide-dev/logtide-sdk-go/integrations/nethttp"

mux := http.NewServeMux()
mux.HandleFunc("/users", usersHandler)

// Wrap with LogTide middleware
http.ListenAndServe(":8080", lnethttp.Middleware(mux))

Best Practices

1. Always defer flush/Close
Use defer flush() (global) or defer client.Close() (explicit client) immediately after initialisation to guarantee buffered entries are delivered on shutdown.
2. Clone the Hub per request
Never share a Hub across goroutines with different lifecycles. Call hub.Clone() at the start of each request or goroutine to get an isolated scope.
3. Use CaptureError for Go errors
CaptureError attaches a full stack trace automatically. Use it instead of Error whenever you have a Go error value.
4. Use the net/http middleware
nethttp.Middleware handles scope isolation, HTTP tagging, and breadcrumbs automatically — no boilerplate needed.
Esc

Type to search across all documentation pages