Go Distributed Tracing mit OpenTelemetry im Jahr 2026: Vollständige Observability für Microservices
Go Distributed Tracing mit OpenTelemetry im Jahr 2026: Vollständige Observability für Microservices
Wenn Sie Microservice-Probleme immer noch mit "Logs hinzufügen → neu starten → Logs lesen" debuggen, steckt Ihre Betriebseffizienz im Jahr 2018 fest. Wenn eine Anfrage 5 Dienste, 3 Datenbanken und 2 Nachrichtenwarteschlangen durchläuft, können Sie ohne verteiltes Tracing einfach nicht den Latenz-Engpass identifizieren. Verteiltes Tracing ist keine Option – es ist eine der drei Säulen der Microservice-Observability (Metriken, Logs, Traces).
Im Jahr 2026 ist OpenTelemetry zum De-facto-Standard geworden, wobei Jaeger und Grafana Tempo das OTLP-Protokoll vollständig unterstützen. Dieser Artikel beginnt mit der OpenTelemetry-Architektur, bietet vollständigen Go-Implementierungscode und behandelt Auto-Instrumentierung, manuelle Instrumentierung, Kontextweitergabe und Backend-Integration.
Warum Verteiltes Tracing für Microservices Unverzichtbar Ist
| Observability-Säule | Gelöstes Problem | Typische Tools | Konsequenz ohne sie |
|---|---|---|---|
| Metriken | "Was ist falsch?" | Prometheus | Problemumfang nicht quantifizierbar |
| Logs | "Wo ist der Fehler?" | Loki/ELK | Spezifische Fehler nicht eingrenzbar |
| Traces | "Warum ist es langsam? Wo ist der Engpass?" | Jaeger/Tempo | Latenz-Engpässe nicht lokalisierbar |
| Alle kombiniert | "Vollständiges Problembild" | Grafana | Nur Problemfragmente sichtbar |
Wichtige Erkenntnis: Bei einer langsamen Anfrage über 5 Dienste hinweg können Logs Ihnen nur sagen "jeder Dienst ist langsam", während Traces Ihnen sagen können "die Datenbankabfrage in Dienst 3 ist für 80 % der Zeit verantwortlich."
1. OpenTelemetry-Architektur
Die Kernarchitektur von OpenTelemetry: API → SDK → Exporter → Collector → Backend
[App] → [OTel API] → [OTel SDK] → [OTLP Exporter] → [OTel Collector] → [Jaeger/Tempo]
| Komponente | Verantwortung | Erforderlich |
|---|---|---|
| OTel API | Instrumentierungsschnittstelle | Ja |
| OTel SDK | Sampling, Batching, Export | Ja |
| OTLP Exporter | Senden an Collector | Ja |
| OTel Collector | Empfangen, verarbeiten, weiterleiten | Empfohlen (Produktion) |
| Backend | Speicherung, Abfrage, Anzeige | Ja |
1.1 OpenTelemetry-Provider Initialisieren
package tracing
import (
"context"
"fmt"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)
func InitProvider(serviceName, collectorURL string) (func(context.Context) error, error) {
exporter, err := otlptracegrpc.New(context.Background(),
otlptracegrpc.WithEndpoint(collectorURL),
otlptracegrpc.WithInsecure(),
)
if err != nil {
return nil, fmt.Errorf("creating exporter: %w", err)
}
res, err := resource.New(context.Background(),
resource.WithAttributes(
semconv.ServiceNameKey.String(serviceName),
semconv.ServiceVersionKey.String("1.0.0"),
semconv.DeploymentEnvironmentKey.String("production"),
),
)
if err != nil {
return nil, fmt.Errorf("creating resource: %w", err)
}
provider := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter,
sdktrace.WithBatchTimeout(5*time.Second),
sdktrace.WithMaxExportBatchSize(512),
),
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.TraceIDRatioBased(0.1)),
)
otel.SetTracerProvider(provider)
return provider.Shutdown, nil
}
2. Auto-Instrumentierung vs. Manuelle Instrumentierung
2.1 HTTP-Auto-Instrumentierung
import (
"net/http"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
func main() {
shutdown, err := tracing.InitProvider("user-service", "otel-collector:4317")
if err != nil {
log.Fatal(err)
}
defer shutdown(context.Background())
mux := http.NewServeMux()
mux.HandleFunc("/users", handleGetUsers)
mux.HandleFunc("/orders", handleGetOrders)
handler := otelhttp.NewHandler(mux, "user-service",
otelhttp.WithMessageEvents(otelhttp.Read, otelhttp.Write),
)
http.ListenAndServe(":8080", handler)
}
2.2 gRPC-Auto-Instrumentierung
import (
"google.golang.org/grpc"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
)
func createGRPCServer() *grpc.Server {
return grpc.NewServer(
grpc.StatsHandler(otelgrpc.NewServerHandler()),
)
}
func createGRPCClient(target string) (*grpc.ClientConn, error) {
return grpc.Dial(target,
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
)
}
2.3 Datenbank-Auto-Instrumentierung
import (
"database/sql"
"go.opentelemetry.io/contrib/instrumentation/database/sql/otsql"
)
func initDB() *sql.DB {
db, err := otsql.Open("postgres", "postgres://localhost/mydb",
otsql.WithAttributes(semconv.DBSystemPostgreSQL),
)
if err != nil {
log.Fatal(err)
}
return db
}
2.4 Manuelle Instrumentierung
func ProcessOrder(ctx context.Context, order *Order) error {
tracer := otel.Tracer("order-service")
ctx, span := tracer.Start(ctx, "ProcessOrder",
trace.WithAttributes(
attribute.String("order.id", order.ID),
attribute.Float64("order.amount", order.Amount),
),
)
defer span.End()
ctx, validateSpan := tracer.Start(ctx, "ValidateOrder")
if err := validate(order); err != nil {
validateSpan.RecordError(err)
validateSpan.SetStatus(codes.Error, err.Error())
validateSpan.End()
return err
}
validateSpan.End()
ctx, paySpan := tracer.Start(ctx, "ProcessPayment")
if err := processPayment(ctx, order); err != nil {
paySpan.RecordError(err)
paySpan.SetStatus(codes.Error, err.Error())
paySpan.End()
return err
}
paySpan.End()
return nil
}
Auto vs. Manuell Vergleich:
| Dimension | Auto-Instrumentierung | Manuelle Instrumentierung |
|---|---|---|
| Invasivität | Keine | Erfordert Codeänderungen |
| Granularität | Framework-Ebene (HTTP/gRPC/DB) | Geschäftsebene (beliebige Funktion) |
| Attributreichtum | Standardattribute | Benutzerdefinierte Attribute |
| Leistungsaufwand | Niedrig (framework-optimiert) | Abhängig von der Anzahl der Instrumentierungen |
| Empfohlene Strategie | Auto für Framework-Schicht | Manuell für geschäftskritische Pfade |
3. Trace-Kontextweitergabe
Die dienstübergreifende Weitergabe des Trace-Kontexts ist der Kern des verteilten Tracings. OpenTelemetry verwendet den W3C Trace Context Standard.
3.1 HTTP-Weitergabe
import (
"go.opentelemetry.io/otel/propagation"
)
func callDownstream(ctx context.Context, url string) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))
return http.DefaultClient.Do(req)
}
3.2 Nachrichtenwarteschlangen-Weitergabe
func publishMessage(ctx context.Context, topic string, msg []byte) error {
carrier := propagation.MapCarrier{}
otel.GetTextMapPropagator().Inject(ctx, carrier)
kafkaMsg := &kafka.Message{
TopicPartition: kafka.TopicPartition{Topic: &topic},
Value: msg,
Headers: make([]kafka.Header, 0, len(carrier)),
}
for k, v := range carrier {
kafkaMsg.Headers = append(kafkaMsg.Headers, kafka.Header{
Key: k, Value: []byte(v),
})
}
return producer.Produce(kafkaMsg, nil)
}
4. Jaeger- und Tempo-Integration
4.1 Jaeger All-in-One (Entwicklung)
apiVersion: apps/v1
kind: Deployment
metadata:
name: jaeger
spec:
selector:
matchLabels:
app: jaeger
template:
spec:
containers:
- name: jaeger
image: jaegertracing/all-in-one:1.60
ports:
- containerPort: 16686
name: ui
- containerPort: 4317
name: otlp-grpc
env:
- name: COLLECTOR_OTLP_ENABLED
value: "true"
4.2 Grafana Tempo (Produktion)
apiVersion: apps/v1
kind: Deployment
metadata:
name: tempo
spec:
selector:
matchLabels:
app: tempo
template:
spec:
containers:
- name: tempo
image: grafana/tempo:2.6
args: ["-config.file=/etc/tempo/tempo.yaml"]
volumeMounts:
- name: config
mountPath: /etc/tempo
volumes:
- name: config
configMap:
name: tempo-config
---
apiVersion: v1
kind: ConfigMap
metadata:
name: tempo-config
data:
tempo.yaml: |
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
storage:
trace:
backend: s3
s3:
bucket: tempo-traces
endpoint: minio:9000
4.3 OTel-Collector-Konfiguration
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 5s
send_batch_size: 512
filter:
error_mode: ignore
traces:
span:
- 'attributes["http.route"] == "/healthz"'
tail_sampling:
decision_wait: 10s
policies:
- name: error-policy
type: status_code
status_code:
status_codes:
- ERROR
- name: slow-policy
type: latency
latency:
threshold_ms: 1000
- name: always-keep
type: probabilistic
probabilistic:
sampling_percentage: 10
exporters:
otlp:
endpoint: tempo:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [filter, tail_sampling, batch]
exporters: [otlp]
Backend-Vergleich:
| Dimension | Jaeger | Grafana Tempo |
|---|---|---|
| Speicher | Elasticsearch/Cassandra | Objektspeicher (S3/GCS) |
| Kosten | Hoch (ES-Cluster) | Niedrig (Objektspeicher) |
| Abfragelatenz | Niedrig (indiziert) | Mittel (Trace-ID-Abfragen sehr schnell) |
| Grafana-Integration | Erfordert Plugin | Native Integration |
| Anwendungsfall | Entwicklung/kleiner Maßstab | Produktion/großer Maßstab |
5 Häufige Fallstricke
| # | Fallstrick | Konsequenz | Lösung |
|---|---|---|---|
| 1 | Sampling-Rate auf 100 % gesetzt | Speicherkostenexplosion, Leistungseinbußen | 0,1 %–10 % in der Produktion, 100 % für Fehler-Traces |
| 2 | Trace-Kontext nicht weitergegeben | Dienstübergreifender Trace unterbrochen | TextMapPropagator.Inject/Extract verwenden |
| 3 | span.End() vergessen | Unvollständige Spans, Speicherlecks | defer span.End() verwenden |
| 4 | Zu viele Spans auf heißen Pfaden erstellt | Übermäßiger Leistungsaufwand | Manuelle Instrumentierung für kritische Pfade, Auto für den Rest |
| 5 | Einzelner Collector-Einsatz | Collector-Ausfall führt zu Datenverlust | Mehrere Collector-Instanzen + Lastausgleich bereitstellen |
10 Fehlerbehebungsfälle
| # | Fehlersymptom | Mögliche Ursache | Behebungsmethode |
|---|---|---|---|
| 1 | Keine Traces in Jaeger sichtbar | Exporter nicht mit Collector verbunden | Collector-URL und Port prüfen |
| 2 | Dienstübergreifender Trace unterbrochen | Kontext nicht weitergegeben | Prüfen, ob Propagator.Inject aufgerufen wird |
| 3 | Fehlende Span-Attribute | Ressourcenattribute nicht gesetzt | resource.New WithAttributes prüfen |
| 4 | Kritische Traces nach Sampling verloren | Sampling-Rate zu niedrig | Tail Sampling verwenden, um Fehler-Traces zu priorisieren |
| 5 | Collector OOM | Batch-Warteschlange zu groß | Batch-Größe und Timeout reduzieren |
| 6 | Tempo-Abfragezeitüberschreitung | Kein Index für Nicht-Trace-ID-Abfragen | Sicherstellen, dass Trace-ID-Abfragen verwendet werden |
| 7 | Unvollständige gRPC-Traces | otelgrpc-Interceptor nicht hinzugefügt | StatsHandler zu Client und Server hinzufügen |
| 8 | Fehlende DB-Spans | otsql verwendet, aber Treiber nicht ersetzt | Bestätigen, dass otsql.Open statt sql.Open verwendet wird |
| 9 | Kafka-Nachrichten-Trace unterbrochen | Trace nicht in Nachrichten-Header eingefügt | Beim Produzieren injecten, beim Konsumieren extrahieren |
| 10 | Zu viele Spans | Überschneidung von Auto- und manueller Instrumentierung | Manuelle Spans vermeiden, wo Auto-Instrumentierung abdeckt |
Werkzeugempfehlungen
Bei der Implementierung von verteiltem Tracing helfen diese Werkzeuge bei Datenformat- und Kodierungsaufgaben:
- JSON-Formatierer — OTel-Collector-Konfiguration und Span-JSON-Daten zum Debuggen formatieren
- Base64-Kodierer — Trace-IDs und Span-IDs für die systemübergreifende Übertragung kodieren
- Hash-Rechner — Hashes für Sampling-Entscheidungen generieren, um konsistentes Sampling für denselben Trace sicherzustellen
Zusammenfassung: Verteiltes Tracing ist die "Röntgenmaschine" der Microservice-Observability – ohne sie sehen Sie nur Symptome, nicht Ursachen. OpenTelemetry vereinheitlicht API und SDK, Auto-Instrumentierung deckt HTTP/gRPC/DB ab, manuelle Instrumentierung ergänzt geschäftskritische Pfade, Tail Sampling stellt sicher, dass Fehler-Traces nicht verloren gehen, und der Collector übernimmt Batching und Weiterleitung. Im Jahr 2026 ist Jaeger für Entwicklungs-Debugging und Tempo für Produktionsspeicherung die optimale Kombination. Denken Sie daran: Ein Microservice-System ohne verteiltes Tracing ist wie eine unüberwachte Blackbox – wenn etwas schiefgeht, können Sie nur raten.
Probiere diese browser-lokalen Tools aus — keine Registrierung erforderlich →