Go Instrumentation
Instrument a Go application with OpenTelemetry and send traces, logs, and metrics to Maple.
This guide covers instrumenting a Go application to send traces and logs to Maple using the OpenTelemetry SDK.
Run this with Claude Code:
maple-onboardwalks every service in the repo, installs OpenTelemetry, and verifies the bootstrap end-to-end. See the maple-onboard skill.
Prerequisites
- Go 1.21+
- A Maple project with an API key (or use the
MAPLE_TESTplaceholder while pairing — it’s accepted by the ingest gateway and discarded, so the bootstrap can run before you’ve created your first key)
Install Dependencies
go get go.opentelemetry.io/otel \
go.opentelemetry.io/otel/sdk \
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
Configure the SDK
Set up the tracer provider in your application startup:
package main
import (
"context"
"log"
"os"
"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.24.0"
)
func initTracer(ctx context.Context) (*trace.TracerProvider, error) {
exporter, err := otlptracehttp.New(ctx,
otlptracehttp.WithEndpointURL("https://ingest.maple.dev/v1/traces"),
otlptracehttp.WithHeaders(map[string]string{
"Authorization": "Bearer YOUR_API_KEY",
}),
)
if err != nil {
return nil, err
}
res, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceName("my-go-app"),
semconv.DeploymentEnvironment(os.Getenv("DEPLOYMENT_ENV")),
),
resource.WithAttributes(
semconv.ServiceVersion("1.0.0"),
),
)
if err != nil {
return nil, err
}
tp := trace.NewTracerProvider(
trace.WithBatcher(exporter),
trace.WithResource(res),
)
otel.SetTracerProvider(tp)
return tp, nil
}
func main() {
ctx := context.Background()
tp, err := initTracer(ctx)
if err != nil {
log.Fatal(err)
}
defer tp.Shutdown(ctx)
// Your application code here
}
Instrumentation Libraries
Go does not have auto-discovery of instrumentation. Instead, add instrumentation packages for the libraries you use.
HTTP Server
Wrap your HTTP handler with otelhttp:
go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
mux := http.NewServeMux()
mux.HandleFunc("/api/orders", handleOrders)
// Wrap the entire handler
handler := otelhttp.NewHandler(mux, "server")
http.ListenAndServe(":8080", handler)
HTTP Client
Wrap outgoing HTTP transports to trace client requests:
client := &http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport),
}
resp, err := client.Get("https://api.example.com/data")
gRPC
go get go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc
import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
// Server
server := grpc.NewServer(
grpc.StatsHandler(otelgrpc.NewServerHandler()),
)
// Client
conn, err := grpc.Dial(addr,
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
)
Database
go get github.com/XSAM/otelsql
import "github.com/XSAM/otelsql"
db, err := otelsql.Open("postgres", dsn)
Custom Spans
Create custom spans to trace specific operations:
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
)
var tracer = otel.Tracer("my-app")
func processOrder(ctx context.Context, orderID string) error {
ctx, span := tracer.Start(ctx, "process-order")
defer span.End()
span.SetAttributes(
attribute.String("order.id", orderID),
// Set peer.service when calling another service
attribute.String("peer.service", "payment-api"),
)
if err := chargePayment(ctx, orderID); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
return nil
}
Setting peer.service on outgoing calls makes them visible on Maple’s service map.
Always propagate ctx through function calls so child spans are linked to parent spans.
Log Correlation
slog Bridge
Use the OTel slog bridge to send structured logs with trace correlation:
go get go.opentelemetry.io/contrib/bridges/otelslog
import "go.opentelemetry.io/contrib/bridges/otelslog"
logger := otelslog.NewLogger("my-app")
logger.InfoContext(ctx, "Order processed", "order_id", orderID)
Manual Correlation
Alternatively, extract trace context and include it in your log fields:
import "go.opentelemetry.io/otel/trace"
span := trace.SpanFromContext(ctx)
sc := span.SpanContext()
logger.Info("Order processed",
"trace_id", sc.TraceID().String(),
"span_id", sc.SpanID().String(),
"order_id", orderID,
)
Environment Variables
As an alternative to programmatic configuration, set standard OTel environment variables:
export OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.maple.dev"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer YOUR_API_KEY"
export OTEL_SERVICE_NAME="my-go-app"
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment.name=production,vcs.repository.url.full=https://github.com/acme/my-go-app"
These variables are read by the OTel SDK automatically when creating exporters with default options.
Verify
- Start your application
- Generate some traffic (send a request, trigger an operation)
- Open the Maple dashboard and check that traces appear in the traces view
If traces aren’t appearing, verify:
- The ingest endpoint URL is correct
- Your API key is valid
- Your application can reach
ingest.maple.dev(or your self-hosted URL)