Setting Up OpenTelemetry for Go Applications
Introduction to OpenTelemetry
OpenTelemetry is an open-source observability framework that provides a unified set of APIs, libraries, and instrumentation to capture distributed traces, metrics, and logs from your applications. As a Cloud Native Computing Foundation (CNCF) project, it has become the standard for application observability.
Why OpenTelemetry for Go?
- Vendor-Neutral: Export telemetry to any backend (Jaeger, Zipkin, Prometheus, etc.)
- Native Go Support: Well-designed Go APIs following Go idioms
- Comprehensive: Traces, metrics, and logs in one framework
- Zero Dependencies: Minimal impact on your application
- Production Ready: Used by major companies in production
Prerequisites
Before we begin, ensure you have:
- Go 1.19 or later installed
- Basic understanding of Go programming
- A text editor or IDE (VS Code with Go extension recommended)
- Docker (optional, for running observability backends)
Setting Up OpenTelemetry in a Go Application
Step 1: Create a New Go Project
1
2
3
mkdir opentelemetry-go-demo
cd opentelemetry-go-demo
go mod init github.com/yourusername/opentelemetry-go-demo
Step 2: Install OpenTelemetry Dependencies
Install the core OpenTelemetry packages:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Core OpenTelemetry API and SDK
go get go.opentelemetry.io/otel
go get go.opentelemetry.io/otel/sdk
# OTLP Exporter (for sending to collectors)
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
# HTTP instrumentation
go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
# Stdout exporter (for debugging)
go get go.opentelemetry.io/otel/exporters/stdout/stdouttrace
# Trace and resource packages
go get go.opentelemetry.io/otel/trace
go get go.opentelemetry.io/otel/sdk/resource
go get go.opentelemetry.io/otel/semconv/v1.21.0
Step 3: Initialize OpenTelemetry
Create a file telemetry/tracer.go to set up OpenTelemetry:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package telemetry
import (
"context"
"fmt"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
)
// InitTracer initializes the OpenTelemetry tracer provider
func InitTracer(serviceName, otlpEndpoint string) (func(context.Context) error, error) {
ctx := context.Background()
// Create resource with service information
res, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceName(serviceName),
semconv.ServiceVersion("1.0.0"),
),
)
if err != nil {
return nil, fmt.Errorf("failed to create resource: %w", err)
}
// Create OTLP trace exporter
traceExporter, err := otlptrace.New(ctx,
otlptracegrpc.NewClient(
otlptracegrpc.WithEndpoint(otlpEndpoint),
otlptracegrpc.WithInsecure(),
),
)
if err != nil {
return nil, fmt.Errorf("failed to create trace exporter: %w", err)
}
// Create tracer provider
tracerProvider := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(traceExporter),
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.AlwaysSample()),
)
// Set global tracer provider
otel.SetTracerProvider(tracerProvider)
// Set global propagator for context propagation
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))
// Return cleanup function
return tracerProvider.Shutdown, nil
}
Step 4: Create a Simple HTTP Server
Create main.go with a basic HTTP server:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
package main
import (
"context"
"fmt"
"log"
"math/rand"
"net/http"
"time"
"github.com/yourusername/opentelemetry-go-demo/telemetry"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
)
const (
serviceName = "go-product-service"
otlpEndpoint = "localhost:4317"
serverPort = ":8080"
)
func main() {
// Initialize OpenTelemetry
shutdown, err := telemetry.InitTracer(serviceName, otlpEndpoint)
if err != nil {
log.Fatalf("Failed to initialize tracer: %v", err)
}
defer func() {
if err := shutdown(context.Background()); err != nil {
log.Printf("Error shutting down tracer provider: %v", err)
}
}()
// Create HTTP handlers with instrumentation
mux := http.NewServeMux()
// Wrap handlers with OpenTelemetry instrumentation
mux.Handle("/products", otelhttp.NewHandler(
http.HandlerFunc(handleProducts),
"GetProducts",
))
mux.Handle("/products/", otelhttp.NewHandler(
http.HandlerFunc(handleProductByID),
"GetProductByID",
))
mux.Handle("/orders", otelhttp.NewHandler(
http.HandlerFunc(handleOrders),
"CreateOrder",
))
mux.Handle("/health", http.HandlerFunc(handleHealth))
// Start server
log.Printf("Starting server on %s", serverPort)
if err := http.ListenAndServe(serverPort, mux); err != nil {
log.Fatalf("Server failed: %v", err)
}
}
func handleProducts(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
tracer := otel.Tracer(serviceName)
// Create a custom span
ctx, span := tracer.Start(ctx, "fetchProducts")
defer span.End()
// Add attributes to span
span.SetAttributes(
attribute.String("http.method", r.Method),
attribute.String("http.path", r.URL.Path),
)
// Simulate database query
products, err := fetchProductsFromDB(ctx)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
http.Error(w, "Failed to fetch products", http.StatusInternalServerError)
return
}
span.SetAttributes(attribute.Int("product.count", len(products)))
span.SetStatus(codes.Ok, "Products fetched successfully")
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"products": %v}`, products)
}
func handleProductByID(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
tracer := otel.Tracer(serviceName)
ctx, span := tracer.Start(ctx, "getProductByID")
defer span.End()
// Extract product ID from URL
productID := r.URL.Path[len("/products/"):]
span.SetAttributes(attribute.String("product.id", productID))
// Simulate database query
product, err := fetchProductByID(ctx, productID)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
http.Error(w, "Product not found", http.StatusNotFound)
return
}
span.SetStatus(codes.Ok, "Product found")
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"product": %v}`, product)
}
func handleOrders(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
tracer := otel.Tracer(serviceName)
ctx, span := tracer.Start(ctx, "createOrder")
defer span.End()
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Process order through multiple steps
orderID, err := processOrder(ctx)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
http.Error(w, "Failed to process order", http.StatusInternalServerError)
return
}
span.SetAttributes(attribute.String("order.id", orderID))
span.SetStatus(codes.Ok, "Order created successfully")
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"order_id": "%s", "status": "created"}`, orderID)
}
func handleHealth(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "OK")
}
func fetchProductsFromDB(ctx context.Context) ([]string, error) {
tracer := otel.Tracer(serviceName)
ctx, span := tracer.Start(ctx, "db.query.products")
defer span.End()
// Add database-related attributes
span.SetAttributes(
attribute.String("db.system", "postgresql"),
attribute.String("db.operation", "SELECT"),
attribute.String("db.statement", "SELECT * FROM products"),
)
// Simulate database latency
time.Sleep(time.Duration(50+rand.Intn(50)) * time.Millisecond)
products := []string{"Laptop", "Mouse", "Keyboard", "Monitor"}
span.SetAttributes(attribute.Int("db.rows.returned", len(products)))
return products, nil
}
func fetchProductByID(ctx context.Context, productID string) (string, error) {
tracer := otel.Tracer(serviceName)
ctx, span := tracer.Start(ctx, "db.query.productByID")
defer span.End()
span.SetAttributes(
attribute.String("db.system", "postgresql"),
attribute.String("db.operation", "SELECT"),
attribute.String("product.id", productID),
)
// Simulate database latency
time.Sleep(time.Duration(30+rand.Intn(30)) * time.Millisecond)
return fmt.Sprintf("Product_%s", productID), nil
}
func processOrder(ctx context.Context) (string, error) {
tracer := otel.Tracer(serviceName)
ctx, span := tracer.Start(ctx, "processOrder")
defer span.End()
orderID := fmt.Sprintf("ORDER_%d", time.Now().Unix())
span.SetAttributes(attribute.String("order.id", orderID))
// Step 1: Validate order
if err := validateOrder(ctx, orderID); err != nil {
return "", err
}
// Step 2: Process payment
if err := processPayment(ctx, orderID); err != nil {
return "", err
}
// Step 3: Reserve inventory
if err := reserveInventory(ctx, orderID); err != nil {
return "", err
}
span.AddEvent("Order processed successfully")
return orderID, nil
}
func validateOrder(ctx context.Context, orderID string) error {
tracer := otel.Tracer(serviceName)
_, span := tracer.Start(ctx, "validateOrder")
defer span.End()
span.SetAttributes(attribute.String("order.id", orderID))
time.Sleep(40 * time.Millisecond)
span.AddEvent("Order validated")
return nil
}
func processPayment(ctx context.Context, orderID string) error {
tracer := otel.Tracer(serviceName)
_, span := tracer.Start(ctx, "processPayment")
defer span.End()
span.SetAttributes(
attribute.String("order.id", orderID),
attribute.String("payment.method", "credit_card"),
)
time.Sleep(100 * time.Millisecond)
span.AddEvent("Payment processed")
return nil
}
func reserveInventory(ctx context.Context, orderID string) error {
tracer := otel.Tracer(serviceName)
_, span := tracer.Start(ctx, "reserveInventory")
defer span.End()
span.SetAttributes(attribute.String("order.id", orderID))
time.Sleep(60 * time.Millisecond)
span.AddEvent("Inventory reserved")
return nil
}
Step 5: Alternative - Using Stdout Exporter (for Development)
For local development without a collector, you can use the stdout exporter:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package telemetry
import (
"context"
"fmt"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
)
func InitTracerStdout(serviceName string) (func(context.Context) error, error) {
ctx := context.Background()
// Create stdout exporter
exporter, err := stdouttrace.New(
stdouttrace.WithPrettyPrint(),
)
if err != nil {
return nil, fmt.Errorf("failed to create stdout exporter: %w", err)
}
// Create resource
res, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceName(serviceName),
),
)
if err != nil {
return nil, fmt.Errorf("failed to create resource: %w", err)
}
// Create tracer provider
tracerProvider := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
)
otel.SetTracerProvider(tracerProvider)
otel.SetTextMapPropagator(propagation.TraceContext{})
return tracerProvider.Shutdown, nil
}
Instrumenting HTTP Clients
To trace outbound HTTP requests:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main
import (
"context"
"net/http"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
func makeHTTPRequest(ctx context.Context, url string) error {
// Create HTTP client with OpenTelemetry instrumentation
client := &http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport),
}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return err
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
Instrumenting Database Operations
For database operations, you can use instrumentation libraries or create custom spans:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package database
import (
"context"
"database/sql"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
)
type DB struct {
*sql.DB
serviceName string
}
func (db *DB) QueryWithTracing(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
tracer := otel.Tracer(db.serviceName)
ctx, span := tracer.Start(ctx, "db.query")
defer span.End()
span.SetAttributes(
attribute.String("db.system", "postgresql"),
attribute.String("db.statement", query),
)
rows, err := db.DB.QueryContext(ctx, query, args...)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
}
span.SetStatus(codes.Ok, "Query executed successfully")
return rows, nil
}
Setting Up Jaeger for Visualization
Using Docker
1
2
3
4
5
6
docker run -d --name jaeger \
-e COLLECTOR_OTLP_ENABLED=true \
-p 16686:16686 \
-p 4317:4317 \
-p 4318:4318 \
jaegertracing/all-in-one:latest
Using Docker Compose
Create docker-compose.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
version: '3.8'
services:
jaeger:
image: jaegertracing/all-in-one:latest
container_name: jaeger
environment:
- COLLECTOR_OTLP_ENABLED=true
ports:
- "16686:16686" # Jaeger UI
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
app:
build: .
container_name: go-app
environment:
- OTEL_SERVICE_NAME=go-product-service
- OTEL_EXPORTER_OTLP_ENDPOINT=jaeger:4317
ports:
- "8080:8080"
depends_on:
- jaeger
Dockerfile:
1
2
3
4
5
6
7
8
9
10
11
12
13
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o main .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]
Running and Testing
Step 1: Start Jaeger
1
docker-compose up -d jaeger
Step 2: Run Your Application
1
go run main.go
Step 3: Make API Calls
1
2
3
4
5
6
7
8
# Get all products
curl http://localhost:8080/products
# Get specific product
curl http://localhost:8080/products/123
# Create an order
curl -X POST http://localhost:8080/orders
Step 4: View Traces
Open http://localhost:16686 and explore your traces!
Best Practices
1. Use Context Properly
Always pass context through your call chain:
1
2
3
4
5
6
7
func processRequest(ctx context.Context) error {
ctx, span := tracer.Start(ctx, "processRequest")
defer span.End()
// Pass ctx to child functions
return childFunction(ctx)
}
2. Add Meaningful Attributes
1
2
3
4
5
6
span.SetAttributes(
attribute.String("user.id", userID),
attribute.Int("order.items.count", itemCount),
attribute.Float64("order.total", totalAmount),
attribute.Bool("order.is_priority", isPriority),
)
3. Record Errors Properly
1
2
3
4
5
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
4. Use Span Events for Important Milestones
1
2
3
4
span.AddEvent("Order validated")
span.AddEvent("Payment processed", trace.WithAttributes(
attribute.String("payment.id", paymentID),
))
5. Configure Sampling for Production
1
2
3
4
5
tracerProvider := sdktrace.NewTracerProvider(
sdktrace.WithSampler(sdktrace.TraceIDRatioBased(0.1)), // 10% sampling
sdktrace.WithBatcher(traceExporter),
sdktrace.WithResource(res),
)
Advanced: Distributed Tracing Across Services
When making HTTP requests to other services:
1
2
3
4
5
import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
client := &http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport),
}
The context will automatically be propagated via HTTP headers!
Common Issues and Troubleshooting
Issue 1: Traces Not Appearing
- Verify OTLP endpoint is correct
- Check Jaeger is running:
docker ps - Ensure shutdown is called on application exit
- Check for error logs
Issue 2: Context Not Propagating
- Ensure you’re passing context through function calls
- Verify propagators are set correctly
- Check HTTP client has otelhttp instrumentation
Issue 3: High Memory Usage
- Configure batch span processor correctly
- Implement proper sampling
- Limit span attributes
Additional Resources
- OpenTelemetry Go Documentation
- OpenTelemetry Go GitHub
- OpenTelemetry Contrib GitHub
- OpenTelemetry Semantic Conventions
Conclusion
OpenTelemetry provides excellent support for Go applications with idiomatic APIs that feel natural to Go developers. By following this guide, you now have a solid foundation for instrumenting your Go applications with distributed tracing. Start with automatic instrumentation using the contrib libraries, and add custom spans where you need deeper visibility into your application’s behavior.
Happy tracing!