Skip to content

Repository files navigation

PingOps Go SDK

A production-grade Go SDK for instrumenting applications with OpenTelemetry and exporting spans to the PingOps backend. Add distributed tracing to your HTTP clients with minimal code, and control what gets traced and how with domain filtering, header redaction, and optional body capture.


Table of Contents


Features

  • Automatic HTTP instrumentation — Wrap any http.Client or create a new one; outgoing requests are traced and sent to PingOps.
  • Manual tracing — Start traces with custom attributes (user ID, session ID, tags, metadata) and run callbacks within that trace context.
  • Deterministic trace IDs — Use a seed (e.g. order ID) so the same business entity always maps to the same trace ID.
  • Domain allow/deny lists — Trace only the hosts you care about; optionally restrict by URL path.
  • Header filtering & redaction — Allow/deny which headers are captured; redact sensitive headers (Authorization, Cookie, etc.) with replace or partial strategies.
  • Optional body capture — Capture request/response bodies (with size limits) globally or per-trace.
  • Multiple config sources — Configure via code, JSON/YAML file, or environment variables; merge file + env for 12-factor apps.
  • OpenTelemetry-native — Uses the official OpenTelemetry Go API and SDK; compatible with other OTEL instrumentations.
  • Batched or immediate export — Choose batched (default) for throughput or immediate for low latency.

Requirements

  • Go 1.22+
  • A PingOps backend (or compatible OTLP HTTP endpoint) and, if required, an API key.

Installation

go get github.com/pingops/pingops-go

Quick Start

package main

import (
	"context"
	"log"

	"github.com/pingops/pingops-go"
)

func main() {
	// 1. Initialize the SDK (required)
	err := pingops.Initialize(pingops.Config{
		BaseURL:     "https://api.pingops.io",
		ServiceName: "my-service",
		APIKey:      "your-api-key",
	})
	if err != nil {
		log.Fatal(err)
	}
	defer pingops.Shutdown(context.Background())

	// 2. Use the instrumented HTTP client
	client := pingops.NewClient()
	resp, err := client.GetWithContext(context.Background(), "https://api.example.com/users")
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	// Request is automatically traced and sent to PingOps
}

After initialization, any request made through pingops.NewClient() (or a client wrapped with pingops.WrapHTTPClient) is traced and, if eligible, exported to PingOps.

Shutdown and flush

Call Shutdown before process exit so the SDK flushes and closes cleanly:

defer pingops.Shutdown(context.Background())

Optionally call ForceFlush to flush buffered spans without shutting down (e.g. before a health check or before exit if you do not call Shutdown immediately):

if err := pingops.ForceFlush(context.Background()); err != nil {
	log.Printf("ForceFlush failed: %v", err)
}

Use IsInitialized to check whether the SDK has been initialized (e.g. after using the register package):

if !pingops.IsInitialized() {
	log.Println("SDK not initialized")
}

Configuration

Configuration can be provided in three ways (and combined):

  1. Programmatically — Pass a pingops.Config to pingops.Initialize().
  2. From a file — Call pingops.InitializeFromFile("config.yaml") or pingops.InitializeFromFile("config.json").
  3. Environment variables — Set PINGOPS_* variables; they override file/config when you use MergeWithEnv() or the register package.

Required fields

Field Description
BaseURL PingOps backend base URL (e.g. https://api.pingops.io).
ServiceName Service name used as the service.name resource attribute.

Optional fields

Field Type Default Description
APIKey string API key for backend authentication.
Debug bool false Enable debug logging.
CaptureRequestBody bool false Capture request bodies (subject to size limits).
CaptureResponseBody bool false Capture response bodies (subject to size limits).
MaxRequestBodySize int 4096 Max request body size to capture (bytes).
MaxResponseBodySize int 4096 Max response body size to capture (bytes).
DomainAllowList []DomainRule If non-empty, only requests matching these rules are traced.
DomainDenyList []DomainRule Requests matching these rules are not traced.
HeadersAllowList []string If set, only these headers are kept in spans.
HeadersDenyList []string These headers are excluded (takes precedence over allow list).
HeaderRedaction *HeaderRedactionConfig How to redact sensitive headers.
BatchSize int 50 Max spans per batch (batched export).
BatchTimeout int 5000 Flush interval in milliseconds (batched export).
ExportMode ExportMode batched pingops.ExportModeBatched or pingops.ExportModeImmediate.

Environment variables

Variable Description
PINGOPS_API_KEY API key for authentication.
PINGOPS_BASE_URL Backend base URL.
PINGOPS_SERVICE_NAME Service name.
PINGOPS_DEBUG Set to "true" to enable debug logging.
PINGOPS_BATCH_SIZE Max spans per batch (default: 50).
PINGOPS_BATCH_TIMEOUT Flush interval in ms (default: 5000).
PINGOPS_EXPORT_MODE "batched" or "immediate".
PINGOPS_CONFIG_FILE Path to config file (used by the register package).

HTTP Client

The SDK provides an instrumented HTTP client so that outgoing requests are automatically traced and sent to PingOps when eligible.

Create a new client

client := pingops.NewClient()

resp, err := client.GetWithContext(ctx, "https://api.example.com/users")
// ...
resp, err = client.PostWithContext(ctx, url, "application/json", body)
// ...
resp, err = client.Put(ctx, url, contentType, body)
resp, err = client.Patch(ctx, url, contentType, body)
resp, err = client.Delete(ctx, url)
resp, err = client.HeadWithContext(ctx, url)

Wrap an existing client

Use your own *http.Client (e.g. with timeouts or custom transport) and wrap it so that requests are still traced:

myClient := &http.Client{Timeout: 10 * time.Second}
wrapped := pingops.WrapHTTPClient(myClient)
resp, err := wrapped.Get("https://api.example.com/data")

Transport-only wrapping

If you only need an instrumented http.RoundTripper:

rt := pingops.InstrumentedTransport()
// or wrap an existing one:
rt = pingops.WrapTransport(myTransport)

Default client and convenience functions

A default instrumented client is available for simple use cases:

resp, err := pingops.Get(ctx, "https://api.example.com/users")
resp, err := pingops.Post(ctx, url, contentType, body)
resp, err := pingops.PostForm(ctx, url, formData)
resp, err := pingops.Head(ctx, url)

Always pass a context.Context when possible so the trace context is propagated.


Manual Tracing

Use manual tracing when you want a logical trace (e.g. “handle checkout”) that includes one or more HTTP calls and carries custom attributes (user ID, session ID, tags, metadata).

StartTrace

StartTrace runs a function inside a new trace. All HTTP calls made with the SDK’s client using the given ctx are part of that trace.

result, err := pingops.StartTrace(ctx, pingops.StartTraceOptions{
	Attributes: &pingops.TraceAttributes{
		UserID:    "user-123",
		SessionID: "session-456",
		Tags:      []string{"checkout", "premium"},
		Metadata:  map[string]string{"cart_value": "99.99"},
	},
}, func(ctx context.Context) (string, error) {
	client := pingops.NewClient()
	resp, err := client.GetWithContext(ctx, "https://api.example.com/checkout")
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	return "success", nil
})

StartTraceOptions

Field Type Description
Attributes *TraceAttributes User ID, session ID, tags, metadata, body capture overrides.
Seed string If set, trace ID is derived from this (deterministic).

TraceAttributes

Field Type Description
TraceID string If set, used as the trace ID (must be valid hex length).
UserID string Propagated to context and span attribute pingops.user_id.
SessionID string Propagated to context and span attribute pingops.session_id.
Tags []string Propagated to context and span attribute pingops.tags.
Metadata map[string]string Propagated to context and span attributes pingops.metadata.*.
CaptureRequestBody *bool Override request body capture for this trace.
CaptureResponseBody *bool Override response body capture for this trace.

Convenience variants

  • StartTraceSimple — No options; only the callback.

    result, err := pingops.StartTraceSimple(ctx, func(ctx context.Context) (string, error) {
        // ...
    })
  • StartTraceWithAttributes — Attributes only (no seed).

    result, err := pingops.StartTraceWithAttributes(ctx, attrs, fn)
  • StartTraceNoReturn — For callbacks that return only an error.

    err := pingops.StartTraceNoReturn(ctx, pingops.StartTraceOptions{
        Attributes: &pingops.TraceAttributes{UserID: "worker-1"},
    }, func(ctx context.Context) error {
        return doWork(ctx)
    })

Getting active trace/span IDs

Inside a trace callback (or any code that has a context from a trace):

traceID := pingops.GetActiveTraceID(ctx)
spanID  := pingops.GetActiveSpanID(ctx)

Deterministic Trace IDs

Use the Seed option so that the same business identifier always produces the same trace ID (e.g. for idempotency or linking backend data).

orderID := "order-abc-123"
_, err := pingops.StartTrace(ctx, pingops.StartTraceOptions{
	Seed: orderID,
	Attributes: &pingops.TraceAttributes{
		Metadata: map[string]string{"order_id": orderID},
	},
}, func(ctx context.Context) (struct{}, error) {
	// Same orderID always yields the same trace ID
	traceID := pingops.GetActiveTraceID(ctx)
	return struct{}{}, nil
})

Context & Attributes

Trace attributes (user ID, session ID, tags, metadata) are stored in context.Context and are available to your code and to the SDK when creating spans.

Reading from context

userID, ok    := pingops.UserIDFromContext(ctx)
sessionID, ok := pingops.SessionIDFromContext(ctx)
tags, ok      := pingops.TagsFromContext(ctx)
metadata, ok  := pingops.MetadataFromContext(ctx)
traceID, ok   := pingops.TraceIDFromContext(ctx)
captureReq, ok := pingops.CaptureRequestBodyFromContext(ctx)
captureResp, ok := pingops.CaptureResponseBodyFromContext(ctx)

Setting on context manually

If you are not using StartTrace but still want to attach attributes to the current context (e.g. before calling the SDK client):

ctx = pingops.WithUserID(ctx, "user-123")
ctx = pingops.WithSessionID(ctx, "session-456")
ctx = pingops.WithTags(ctx, []string{"premium", "checkout"})
ctx = pingops.WithMetadata(ctx, map[string]string{"key": "value"})
ctx = pingops.WithTraceAttributes(ctx, &pingops.TraceAttributes{UserID: "user-1"})

Domain Filtering

Control which HTTP requests are traced using allow and deny lists.

  • DomainAllowList — If non-empty, only requests whose host (and optionally path) match one of the rules are eligible for tracing.
  • DomainDenyList — Requests matching any of these rules are not traced (deny wins over allow).

DomainRule

Field Type Description
Domain string Host to match. If it starts with ., it matches that domain and all subdomains (e.g. .example.com).
Paths []string If set, the request path must start with one of these strings.
HeadersAllowList []string Override global: only these headers for this domain.
HeadersDenyList []string Override global: exclude these headers for this domain.
CaptureRequestBody *bool Override request body capture for this domain.
CaptureResponseBody *bool Override response body capture for this domain.

Example

pingops.Initialize(pingops.Config{
	BaseURL:     "https://api.pingops.io",
	ServiceName: "my-service",
	APIKey:      "key",

	DomainAllowList: []pingops.DomainRule{
		{Domain: "api.example.com"},
		{Domain: ".internal.company.com", Paths: []string{"/api"}},
	},
	DomainDenyList: []pingops.DomainRule{
		{Domain: "blocked.com"},
	},
})

Header Filtering & Redaction

Allow/deny lists

  • HeadersAllowList — If non-empty, only these header names are kept in spans.
  • HeadersDenyList — These header names are always excluded (takes precedence).

Redaction

Sensitive headers (e.g. Authorization, Cookie, X-Api-Key) can be redacted or removed using HeaderRedactionConfig:

Field Type Description
SensitivePatterns []string Case-insensitive header names to redact.
Strategy RedactionStrategy How to redact (see below).
RedactionString string String used for replace/partial strategies (e.g. "[REDACTED]").
VisibleChars int Used by partial strategies (e.g. show first/last N chars).
Enabled *bool Master switch for redaction.

Redaction strategies

Strategy Constant Behavior
Replace pingops.RedactionReplace Replace entire value with RedactionString.
Partial (start) pingops.RedactionPartial Show first VisibleChars, then RedactionString.
Partial (end) pingops.RedactionPartialEnd RedactionString then last VisibleChars.
Remove pingops.RedactionRemove Omit header from span.

Default sensitive patterns (when not overridden): authorization, cookie, x-api-key, x-auth-token. Add more with SensitivePatterns or use pingops.DefaultSensitiveHeaderPatterns() and extend.

Example

pingops.Initialize(pingops.Config{
	// ...
	HeaderRedaction: &pingops.HeaderRedactionConfig{
		Enabled:           boolPtr(true),
		SensitivePatterns: []string{"authorization", "cookie", "x-api-key"},
		Strategy:          pingops.RedactionReplace,
		RedactionString:   "[REDACTED]",
	},
})

Body Capture

You can capture request and response bodies so they appear in spans (useful for debugging; use with care in production).

  • CaptureRequestBody / CaptureResponseBody — Global switch.
  • MaxRequestBodySize / MaxResponseBodySize — Max bytes to capture (default 4096).

Override per-trace via TraceAttributes.CaptureRequestBody and TraceAttributes.CaptureResponseBody, or per-domain via DomainRule.CaptureRequestBody / CaptureResponseBody.

pingops.Initialize(pingops.Config{
	// ...
	CaptureRequestBody:  true,
	CaptureResponseBody: true,
	MaxRequestBodySize:  8192,
	MaxResponseBodySize: 8192,
})

Export Mode

  • ExportModeBatched (default) — Spans are buffered and sent in batches. Configurable via BatchSize and BatchTimeout. Best for throughput.
  • ExportModeImmediate — Each span is exported as soon as it ends. Lower latency, more requests to the backend.
pingops.Initialize(pingops.Config{
	// ...
	ExportMode:   pingops.ExportModeImmediate,
	BatchSize:    100,
	BatchTimeout: 3000,
})

Auto-Initialization

For 12-factor or containerized apps, you can initialize the SDK from environment variables (and optionally a config file) at import time.

Add a blank import of the register package:

import (
	"github.com/pingops/pingops-go"
	_ "github.com/pingops/pingops-go/register"
)

Behavior:

  • If PINGOPS_CONFIG_FILE is set, the SDK loads that file and merges with env.
  • Otherwise, if PINGOPS_BASE_URL and PINGOPS_SERVICE_NAME are set, the SDK initializes from env.
  • If neither condition is met, initialization is skipped (no error). You can still call pingops.Initialize() later.

Always call defer pingops.Shutdown(context.Background()) in main so the SDK flushes on exit.


Config from File

JSON and YAML are supported.

err := pingops.InitializeFromFile("config.yaml")
// or
err := pingops.InitializeFromFile("config.json")

Override with environment variables by loading the struct, then calling cfg.MergeWithEnv() before Initialize (the register package does this when using PINGOPS_CONFIG_FILE).

Example config.yaml (see examples/config-file/config.yaml for a full sample):

baseUrl: https://api.pingops.io
serviceName: my-service
debug: true
captureRequestBody: true
captureResponseBody: true
exportMode: batched
batchSize: 50
batchTimeout: 5000
domainAllowList:
  - domain: api.example.com
  - domain: .internal.company.com
    paths: ["/api"]

OpenTelemetry Integration

The SDK is built on the official OpenTelemetry Go API and SDK:

  • It creates an isolated TracerProvider that uses a PingOps span processor and registers it as the global provider after Initialize. So spans created via otel.Tracer() (e.g. from StartTrace or other instrumentations) can be exported to PingOps.
  • Export is done via OTLP HTTP.
  • It is designed to work alongside other OpenTelemetry instrumentations and exporters.

You can use standard go.opentelemetry.io/otel and go.opentelemetry.io/otel/trace APIs; spans created within a trace context from this SDK will be associated with the same trace.


Error Handling

Initialization

  • Initialize and InitializeFromFile return an error if config is invalid or resource/processor setup fails.
  • MustInitialize / MustInitializeFromFile panic on error (useful in main).

SDK errors

The SDK defines sentinel and typed errors (re-exported from sdkerrors):

Error / Type When
ErrNotInitialized Operations that require initialization.
ErrAlreadyInitialized Redundant init (documented behavior is idempotent no-op).
ErrMissingBaseURL / ErrMissingServiceName Validation.
ErrInvalidConfig Invalid configuration.
ConfigError Config field-level errors; use errors.As.
InitError Initialization stage errors; use errors.As.
ExportError Export failures; use errors.As.
AutoInitError Auto-init missing env vars.

Helpers:

if pingops.IsNotInitialized(err) { ... }
if pingops.IsConfigError(err) { ... }
if pingops.IsInitError(err) { ... }

Examples

The repository includes runnable examples under examples/:

Example Description
basic Initialize, HTTP client, manual traces, deterministic IDs, body capture overrides, default client, wrapping existing client.
advanced Full config, context propagation, concurrent traces, nested traces, error handling, all HTTP methods.
config-file Initialize from YAML/JSON config file.
register Auto-initialization via import _ "github.com/pingops/pingops-go/register".

Run any example (from repo root, with env vars set as needed):

cd examples/basic && go run main.go
cd examples/advanced && go run main.go
cd examples/config-file && go run main.go
cd examples/register && PINGOPS_BASE_URL=https://api.pingops.io PINGOPS_SERVICE_NAME=my-app PINGOPS_API_KEY=key go run main.go

API Reference

Full API documentation is available via pkg.go.dev:

github.com/pingops/pingops-go

Subpackages:


Project Structure

The SDK follows Go best practices with a single public import and internal implementation:

pingops-go/
├── doc.go           # Package documentation
├── pingops.go       # Main entry point (Initialize, Shutdown, StartTrace)
├── config.go        # Configuration types and loading
├── context.go       # Context helpers and trace attributes
├── client.go        # Instrumented HTTP client
├── trace.go         # Trace ID generation and parsing
├── errors.go        # Error types
├── version.go       # SDK version info
├── internal/        # Private implementation (not importable)
│   ├── config/      # Global config state management
│   ├── filter/      # Domain and header filtering
│   ├── processor/   # OpenTelemetry SpanProcessor
│   ├── provider/    # TracerProvider management
│   └── transport/   # HTTP RoundTripper instrumentation
├── register/        # Auto-initialization via blank import
└── examples/        # Usage examples

Users only need a single import: github.com/pingops/pingops-go


License

See the repository’s license file for terms of use.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages