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.
- Features
- Requirements
- Installation
- Quick Start
- Shutdown and flush
- Configuration
- HTTP Client
- Manual Tracing
- Context & Attributes
- Domain Filtering
- Header Filtering & Redaction
- Body Capture
- Export Mode
- Auto-Initialization
- Config from File
- OpenTelemetry Integration
- Error Handling
- Examples
- API Reference
- Project Structure
- Automatic HTTP instrumentation — Wrap any
http.Clientor 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.
- Go 1.22+
- A PingOps backend (or compatible OTLP HTTP endpoint) and, if required, an API key.
go get github.com/pingops/pingops-gopackage 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.
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 can be provided in three ways (and combined):
- Programmatically — Pass a
pingops.Configtopingops.Initialize(). - From a file — Call
pingops.InitializeFromFile("config.yaml")orpingops.InitializeFromFile("config.json"). - Environment variables — Set
PINGOPS_*variables; they override file/config when you useMergeWithEnv()or the register package.
| Field | Description |
|---|---|
BaseURL |
PingOps backend base URL (e.g. https://api.pingops.io). |
ServiceName |
Service name used as the service.name resource attribute. |
| 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. |
| 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). |
The SDK provides an instrumented HTTP client so that outgoing requests are automatically traced and sent to PingOps when eligible.
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)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")If you only need an instrumented http.RoundTripper:
rt := pingops.InstrumentedTransport()
// or wrap an existing one:
rt = pingops.WrapTransport(myTransport)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.
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 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
})| 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). |
| 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. |
-
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) })
Inside a trace callback (or any code that has a context from a trace):
traceID := pingops.GetActiveTraceID(ctx)
spanID := pingops.GetActiveSpanID(ctx)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
})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.
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)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"})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).
| 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. |
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"},
},
})- HeadersAllowList — If non-empty, only these header names are kept in spans.
- HeadersDenyList — These header names are always excluded (takes precedence).
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. |
| 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.
pingops.Initialize(pingops.Config{
// ...
HeaderRedaction: &pingops.HeaderRedactionConfig{
Enabled: boolPtr(true),
SensitivePatterns: []string{"authorization", "cookie", "x-api-key"},
Strategy: pingops.RedactionReplace,
RedactionString: "[REDACTED]",
},
})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,
})- ExportModeBatched (default) — Spans are buffered and sent in batches. Configurable via
BatchSizeandBatchTimeout. 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,
})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.
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"]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 viaotel.Tracer()(e.g. fromStartTraceor 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.
InitializeandInitializeFromFilereturn an error if config is invalid or resource/processor setup fails.MustInitialize/MustInitializeFromFilepanic on error (useful inmain).
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) { ... }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.goFull API documentation is available via pkg.go.dev:
Subpackages:
- github.com/pingops/pingops-go/register — Auto-initialization.
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
See the repository’s license file for terms of use.