Documentation menu
gogolanghttpintegrationopentelemetry

Go Integration

Send logs and traces from Go applications to ScryWatch with the official sdk-go client, the sdk-go-http middleware, or plain net/http if you'd rather not add a dependency.

Go Integration

ScryWatch has two official Go packages: github.com/scrywatch/sdk-go, a lightweight log client, and github.com/scrywatch/sdk-go-http, net/http middleware built on top of it that logs every request as an api_call event. For distributed traces, send OTLP via the OpenTelemetry Go SDK. If you’d rather not add a dependency, ScryWatch also accepts logs over plain HTTP.

What you’ll need

Install the client:

go get github.com/scrywatch/sdk-go
import (
    "context"
    "os"

    scrywatch "github.com/scrywatch/sdk-go"
)

client := scrywatch.NewClient(
    "https://api.scrywatch.com",
    os.Getenv("SCRYWATCH_API_KEY"),
    scrywatch.WithService("api"),
    scrywatch.WithEnvironment("production"),
)

client.SetUserID("user-123")
client.Info(context.Background(), "User signed in", map[string]any{"plan": "pro"})
client.Warn(context.Background(), "Slow query", map[string]any{"duration_ms": 1450})
client.Error(context.Background(), "Payment failed", map[string]any{"order_id": "ord_456"})

Functional options:

OptionDefaultDescription
WithService(s)""Service name attached to every event
WithEnvironment(e)""Environment tag (e.g. "production")
WithMaxRetries(n)3Max retry attempts on 5xx / network errors
WithTimeout(d)5sPer-request timeout (ignored when using WithHTTPClient)
WithHTTPClient(c)stdlib defaultProvide your own *http.Client

Retries happen on network errors or 5xx responses, with exponential backoff starting at 100ms and doubling each attempt. 4xx responses return an error immediately — no retry.

HTTP middleware

If you want every inbound HTTP request logged automatically as an api_call event, add sdk-go-http:

go get github.com/scrywatch/sdk-go-http
import (
    scrywatch     "github.com/scrywatch/sdk-go"
    scrywatchhttp "github.com/scrywatch/sdk-go-http"
)

client := scrywatch.NewClient("https://api.scrywatch.com", os.Getenv("SCRYWATCH_API_KEY"), scrywatch.WithService("api"))

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

http.ListenAndServe(":8080", scrywatchhttp.Middleware(client)(mux))

It maps HTTP status to ScryWatch level (< 400info, 400–499warn, ≥ 500error) and logs method, path, status_code, and duration_ms per request.

Option B: Plain net/http (no dependency)

If you’d rather not add a dependency, you can call the ingest endpoint directly.

Log ingest endpoint

POST https://api.scrywatch.com/api/ingest
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

Step 1: Minimal log helper

package scrywatch

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

// LogEvent is a single log entry sent to ScryWatch.
type LogEvent struct {
	Timestamp   int64          `json:"timestamp"`
	Level       string         `json:"level"`
	Type        string         `json:"type"`
	Message     string         `json:"message"`
	Service     string         `json:"service,omitempty"`
	Environment string         `json:"environment,omitempty"`
	UserID      string         `json:"user_id,omitempty"`
	SessionID   string         `json:"session_id,omitempty"`
	Metadata    map[string]any `json:"metadata,omitempty"`
}

// Client sends log events to ScryWatch.
type Client struct {
	endpoint string
	apiKey   string
	http     *http.Client
}

// NewClient creates a new ScryWatch client.
func NewClient(endpoint, apiKey string) *Client {
	return &Client{
		endpoint: endpoint,
		apiKey:   apiKey,
		http:     &http.Client{Timeout: 5 * time.Second},
	}
}

// Log sends a single log event.
func (c *Client) Log(ctx context.Context, level, message string, metadata map[string]any) error {
	return c.LogBatch(ctx, []LogEvent{{
		Timestamp: time.Now().UnixMilli(),
		Level:     level,
		Type:      "custom",
		Message:   message,
		Metadata:  metadata,
	}})
}

// LogBatch sends multiple log events in one request (up to 50 — the ingest
// endpoint's hard cap; larger batches are rejected with a 400).
func (c *Client) LogBatch(ctx context.Context, events []LogEvent) error {
	payload, err := json.Marshal(map[string]any{"events": events})
	if err != nil {
		return err
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint+"/api/ingest", bytes.NewReader(payload))
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bearer "+c.apiKey)
	req.Header.Set("Content-Type", "application/json")

	resp, err := c.http.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	return nil
}

Step 2: Usage

package main

import (
	"context"
	"log"
)

func main() {
	sw := scrywatch.NewClient("https://api.scrywatch.com", "YOUR_API_KEY")

	if err := sw.Log(context.Background(), "info", "Server started", map[string]any{
		"port": 8080,
	}); err != nil {
		log.Printf("scrywatch: %v", err)
	}
}

Batch events

Use LogBatch to send up to 50 events in a single request, reducing HTTP overhead for high-volume services:

events := []scrywatch.LogEvent{
	{Timestamp: time.Now().UnixMilli(), Level: "info", Type: "custom", Message: "Request received", Service: "api"},
	{Timestamp: time.Now().UnixMilli(), Level: "error", Type: "custom", Message: "Upstream timeout", Metadata: map[string]any{"target": "payments"}},
}
if err := sw.LogBatch(context.Background(), events); err != nil {
	log.Printf("scrywatch batch: %v", err)
}

Traces via OpenTelemetry

This applies whether you’re using the official log packages or plain HTTP above — traces are always sent via OTLP, not through sdk-go. ScryWatch accepts OTLP/HTTP traces from the standard OpenTelemetry Go SDK.

⚠️ OTLP support is traces-only today. OTLP log ingestion is not yet supported by ScryWatch — use the HTTP ingest above for logs. Also note: the OpenTelemetry Go Logs Bridge API is still experimental as of early 2026; even when ScryWatch adds support, you should evaluate stability before production use.

go get go.opentelemetry.io/otel \
       go.opentelemetry.io/otel/sdk/trace \
       go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp \
       go.opentelemetry.io/otel/semconv/v1.21.0
import (
	"context"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
)

func initTracer(ctx context.Context) (*trace.TracerProvider, error) {
	exporter, err := otlptracehttp.New(ctx,
		otlptracehttp.WithEndpoint("api.scrywatch.com"),
		otlptracehttp.WithURLPath("/api/traces/otlp"),
		otlptracehttp.WithHeaders(map[string]string{
			"Authorization": "Bearer YOUR_API_KEY",
		}),
	)
	if err != nil {
		return nil, err
	}

	res, _ := resource.New(ctx,
		resource.WithAttributes(semconv.ServiceName("my-go-app")),
	)

	tp := trace.NewTracerProvider(
		trace.WithBatcher(exporter),
		trace.WithResource(res),
	)
	otel.SetTracerProvider(tp)
	return tp, nil
}

// Call at startup
func main() {
	ctx := context.Background()
	tp, err := initTracer(ctx)
	if err != nil {
		log.Fatal(err)
	}
	defer tp.Shutdown(ctx)

	// Now use otel.Tracer("my-service") as normal
}

Production tip: OpenTelemetry Collector

Rather than exporting directly from each service, deploy an OpenTelemetry Collector as a sidecar or DaemonSet and forward to ScryWatch from there. This decouples your services from ScryWatch’s endpoint — if you change backends, only the Collector config changes. See the Kubernetes guide.

Event fields reference

FieldTypeRequiredDescription
timestampnumberUnix milliseconds — use time.Now().UnixMilli()
levelstringinfo | warn | error | debug
typestringcustom | crash | session | navigation | api_call
messagestringLog message
servicestringService name
environmentstringe.g. production, staging
user_idstringUser identifier
metadataobjectAny additional key/value pairs

See also