Tracing von Go-Webanwendungen Datenbank- und HTTP-Client-Aufrufe mit OpenTelemetry
James Reed
Infrastructure Engineer · Leapcell

Tracing von Go-Webanwendungen Datenbank- und HTTP-Client-Aufrufe
In heutigen komplexen, verteilten Systemen ist das Verständnis des Anfrageflusses und die Identifizierung von Leistungsengpässen von größter Bedeutung. Go-Webanwendungen, die oft mit Datenbanken und externen HTTP-Diensten interagieren, bilden da keine Ausnahme. Ohne entsprechende Beobachtbarkeit kann die Fehlersuche zu einer zeitaufwändigen und frustrierenden Angelegenheit werden, die zu einer erhöhten mittleren Lösungszeit (MTTR) und Kundenzufriedenheit führt. Hier kommt verteiltes Tracing ins Spiel. Durch Instrumentierung unserer Anwendungen können wir unschätzbare Einblicke in die Lebensdauer einer Anfrage gewinnen, während sie verschiedene Komponenten durchläuft. Dieser Artikel befasst sich mit den praktischen Aspekten der manuellen Integration von OpenTelemetry in Go-Webanwendungen zum Tracing von Datenbankabfragen und HTTP-Client-Aufrufen und bietet eine detaillierte Ansicht des Verhaltens unserer Anwendung.
Verständnis der Säulen des verteilten Tracings
Bevor wir uns dem Code widmen, wollen wir ein gemeinsames Verständnis der Kernkonzepte von OpenTelemetry schaffen, die unsere Tracing-Bemühungen unterstützen werden.
- Trace: Ein Trace stellt eine einzelne, End-to-End-Transaktion oder Anfrage innerhalb eines verteilten Systems dar. Er ist eine Sammlung von kausal zusammenhängenden Spans, die gemeinsam die gesamte Operation beschreiben.
- Span: Ein Span ist eine einzelne atomare Operation innerhalb eines Traces. Er stellt eine Arbeitseinheit dar, wie z. B. eine eingehende HTTP-Anfrage, eine Datenbankabfrage oder ein ausgehender API-Aufruf. Jeder Span hat einen Namen, eine Startzeit und eine Endzeit sowie Attribute, die kontextbezogene Informationen liefern. Spans können verschachtelt sein und eine Eltern-Kind-Beziehung bilden.
- Tracer: Ein Tracer ist eine Schnittstelle zur Erstellung von Spans. Er wird typischerweise von einem
TracerProvider
bezogen. - TracerProvider: Ein
TracerProvider
verwaltet und konfiguriertTracer
-Instanzen. Er ist für die Einrichtung der Tracing-Pipeline verantwortlich, einschließlich Span-Prozessoren und Exportern. - Span-Prozessor: Ein
SpanProcessor
ist eine Schnittstelle, die die Verarbeitung von Spans ermöglicht, bevor sie exportiert werden. Er kann Aufgaben wie Sampling, Anreicherung von Spans mit zusätzlichen Attributen oder Pufferung von Spans durchführen. - Exporter: Ein
Exporter
sendet gesammelte Telemetriedaten (Spans, Metriken, Logs) an ein Backend-System wie Jaeger, Zipkin oder den OpenTelemetry Collector. - Kontextweitergabe: Der Mechanismus, durch den der Trace-Kontext (der die Trace-ID und Span-ID enthält) über Dienstgrenzen hinweg weitergegeben wird. Dies ist entscheidend, um Spans zu verknüpfen und einen vollständigen Trace zu bilden. In HTTP geschieht dies typischerweise über Anfrage-Header.
Das Prinzip hinter dem Tracing besteht darin, für jede signifikante Operation einen neuen Span zu erstellen und ihn über die Kontextweitergabe mit seinem übergeordneten Span zu verknüpfen. Dies bildet einen gerichteten azyklischen Graphen (DAG) von Spans, der eine detaillierte Zeitleiste des Anfrageflusses liefert.
Manuelle Integration von OpenTelemetry für Datenbank- und HTTP-Client-Tracing
Unser Ziel ist es, eine Go-Webanwendung manuell zu instrumentieren, um Aufrufe zu einer Datenbank (z. B. PostgreSQL) und ausgehende HTTP-Anfragen zu tracen. Wir werden eine einfache OpenTelemetry-Pipeline einrichten, um Traces an einen lokalen OpenTelemetry Collector zu exportieren, der sie dann an ein Tracing-Backend wie Jaeger weiterleiten kann.
Beginnen wir damit, unseren OpenTelemetry Collector und Jaeger lokal mithilfe von Docker Compose zu Demonstrationszwecken einzurichten.
# docker-compose.yaml version: '3.8' services: jaeger: image: jaegertracing/all-in-one:1.35 ports: - "6831:6831/udp" # Agent UDP - "16686:16686" # UI - "14268:14268" # Collector HTTP environment: COLLECTOR_ZIPKIN_HOST_PORT: 9411 COLLECTOR_OTLP_ENABLED: true otel-collector: image: otel/opentelemetry-collector:0.86.0 command: ["--config=/etc/otel-collector-config.yaml"] volumes: - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml ports: - "4317:4317" # OTLP gRPC receiver - "4318:4318" # OTLP HTTP receiver depends_on: - jaeger
Und die OpenTelemetry Collector-Konfiguration:
# otel-collector-config.yaml receivers: otlp: protocols: grpc: http: exporters: jaeger: endpoint: jaeger:14250 tls: insecure: true service: pipelines: traces: receivers: [otlp] exporters: [jaeger]
Führen Sie diese mit docker-compose up -d
aus. Sie können die Jaeger UI dann unter http://localhost:16686
aufrufen.
Nun schreiben wir unsere Go-Anwendung. Dieses Beispiel enthält einen einfachen HTTP-Server, der eine Anfrage bearbeitet, indem er Daten aus einer PostgreSQL-Datenbank abruft und dann einen externen HTTP-Aufruf tätigt.
Zuerst müssen wir unseren OpenTelemetry TracerProvider
initialisieren und ihn für den Export von Spans konfigurieren.
package main import ( "context" "fmt" "log" "net/http" "time" "github.com/gin-gonic/gin" "github.com/jackc/pgx/v5" "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/otlp/otlptrace" "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.21.0" "go.opentelemetry.io/otel/trace" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) var tracer = otel.Tracer("my-go-web-app") // InitTracerProvider initialisiert den OpenTelemetry TracerProvider func InitTracerProvider() *sdktrace.TracerProvider { ctx := context.Background() // Konfigurieren Sie den OTLP-Exporter, um Traces an den Collector zu senden conn, err := grpc.DialContext(ctx, "otel-collector:4317", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock(), ) if err != nil { log.Fatalf("failed to dial gRPC: %v", err) } exporter, err := otlptrace.New( ctx, otlptracegrpc.WithGRPCConn(conn), ) if err != nil { log.Fatalf("failed to create OTLP trace exporter: %v", err) } // Erstellen Sie einen neuen Trace-Prozessor, der Spans exportiert bsp := sdktrace.NewBatchSpanProcessor(exporter) // Definieren Sie die Ressource, die diesen Dienst identifiziert res, err := resource.New(ctx, resource.WithAttributes( semconv.ServiceNameKey.String("go-web-app"), semconv.ServiceVersionKey.String("1.0.0"), ), ) if err != nil { log.Fatalf("failed to create resource: %v", err) } // Erstellen Sie einen neuen TracerProvider tp := sdktrace.NewTracerProvider( sdktrace.WithBatchSpanProcessor(bsp), sdktrace.WithResource(res), ) // Registrieren Sie den globalen TracerProvider otel.SetTracerProvider(tp) return tp } // simulateDBQuery simuliert eine Datenbankabfrage. func simulateDBQuery(ctx context.Context, userID string) (string, error) { // Erstellen Sie einen neuen Span für die Datenbankabfrage ctx, span := tracer.Start(ctx, "db.get_user_data", trace.WithSpanKind(trace.SpanKindClient), trace.WithAttributes( attribute.String("db.system", "postgresql"), attribute.String("db.statement", "SELECT * FROM users WHERE id = $1"), attribute.String("db.user_id", userID), attribute.String("database.connection_string", "user=postgres password=password dbname=test host=localhost port=5432 sslmode=disable"), ), ) defer span.End() // Simulieren Sie die Datenbankverbindung und -abfrageausführung conn, err := pgx.Connect(ctx, "postgresql://postgres:password@localhost:5433/test?sslmode=disable") if err != nil { span.RecordError(err) span.SetStatus(trace.StatusError, "Failed to connect to database") return "", fmt.Errorf("failed to connect to database: %w", err) } defer conn.Close(ctx) // Simulieren Sie eine Abfrageverzögerung time.Sleep(100 * time.Millisecond) var username string err = conn.QueryRow(ctx, "SELECT username FROM users WHERE id = $1", userID).Scan(&username) if err != nil { span.RecordError(err) if err == pgx.ErrNoRows { span.SetStatus(trace.StatusError, "User not found") return "", fmt.Errorf("user %s not found", userID) } span.SetStatus(trace.StatusError, "Failed to query database") return "", fmt.Errorf("failed to query database: %w", err) } span.SetStatus(trace.StatusOK, "Successfully retrieved user data") return username, nil } // makeExternalAPIRequest simuliert einen HTTP-Client-Aufruf an einen externen Dienst. func makeExternalAPIRequest(ctx.Context, endpoint string) (string, error) { // Erstellen Sie einen neuen Span für den HTTP-Client-Aufruf ctx, span := tracer.Start(ctx, fmt.Sprintf("http.client.get_%s", endpoint), trace.WithSpanKind(trace.SpanKindClient), trace.WithAttributes( attribute.String("http.method", "GET"), attribute.String("http.url", endpoint), ), ) defer span.End() req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil) if err != nil { span.RecordError(err) span.SetStatus(trace.StatusError, "Failed to create HTTP request") return "", fmt.Errorf("failed to create HTTP request: %w", err) } // Leiten Sie den Trace-Kontext an die ausgehende Anfrage weiter otel.GetTextMapPropagator().Inject(ctx, otel.HeaderCarrier(req.Header)) client := http.DefaultClient resp, err := client.Do(req) if err != nil { span.RecordError(err) span.SetStatus(trace.StatusError, "Failed to make HTTP request") return "", fmt.Errorf("failed to make HTTP request: %w", err) } defer resp.Body.Close() span.SetAttributes( attribute.Int("http.status_code", resp.StatusCode), ) if resp.StatusCode != http.StatusOK { span.SetStatus(trace.StatusError, fmt.Sprintf("HTTP request failed with status: %d", resp.StatusCode)) return "", fmt.Errorf("external API call failed with status: %d", resp.StatusCode) } // Simulieren Sie die Verarbeitung der Antwort time.Sleep(50 * time.Millisecond) span.SetStatus(trace.StatusOK, "Successfully made external API request") return "External API response for: " + endpoint, nil } func main() { // Initialisieren Sie den OpenTelemetry TracerProvider tp := InitTracerProvider() defer func() { if err := tp.Shutdown(context.Background()); err != nil { log.Printf("Error shutting down tracer provider: %v", err) } }() // Datenbank-Setup (zur Demonstration) // Sie müssen eine PostgreSQL-Instanz laufen lassen, // z. B. mit einem Docker-Container: // docker run --name some-postgres -p 5433:5432 -e POSTGRES_PASSWORD=password -d postgres // Verbinden Sie sich dann und erstellen Sie eine 'users'-Tabelle: // CREATE TABLE users (id VARCHAR(255) PRIMARY KEY, username VARCHAR(255)); // INSERT INTO users (id, username) VALUES ('123', 'john_doe'); // INSERT INTO users (id, username) VALUES ('456', 'jane_doe'); router := gin.Default() router.Use(otelgin.Middleware("my-go-web-app")) // Verwenden Sie die Gin OpenTelemetry Middleware für eingehende Anfragen router.GET("/user/:id", func(c *gin.Context) { ctx := c.Request.Context() // Holt den Kontext von Gin, der bereits den Trace-Kontext enthält userID := c.Param("id") // Trace-Datenbank-Aufruf username, err := simulateDBQuery(ctx, userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Trace eines externen HTTP-Client-Aufrufs externalAPIResponse, err := makeExternalAPIRequest(ctx, "http://jsonplaceholder.typicode.com/todos/1") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "message": fmt.Sprintf("User %s found: %s", userID, username), "external_api_result": externalAPIResponse, }) }) log.Fatal(router.Run(":8080")) // Abhören und dienen auf 0.0.0.0:8080 }
Um dieses Beispiel auszuführen, benötigen Sie die folgenden Go-Module:
go get github.com/gin-gonic/gin go get go.opentelemetry.io/otel go get go.opentelemetry.io/otel/attribute go get go.opentelemetry.io/otel/exporters/otlp/otlptrace go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc go get go.opentelemetry.io/otel/sdk/resource go get go.opentelemetry.io/otel/sdk/trace go get go.opentelemetry.io/otel/semconv/v1.21.0 go get google.golang.org/grpc go get go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin go get github.com/jackc/pgx/v5
Erklärung des Codes:
-
InitTracerProvider()
:- Diese Funktion richtet die OpenTelemetry-Tracing-Pipeline ein.
- Sie erstellt einen gRPC-Client, um eine Verbindung zum OpenTelemetry Collector unter
otel-collector:4317
herzustellen. otlptracegrpc.New()
erstellt einen OTLP-Trace-Exporter, der Spans über gRPC sendet.sdktrace.NewBatchSpanProcessor()
puffert Spans und sendet sie in Batches, was die Leistung verbessert.- Eine
resource
wird mitServiceNameKey
undServiceVersionKey
definiert, um unsere Anwendung im Tracing-Backend zu identifizieren. sdktrace.NewTracerProvider()
erstellt denTracerProvider
mit dem konfigurierten Prozessor und der Ressource.otel.SetTracerProvider(tp)
registriert diesen Provider global, sodass andere Teile der Anwendung einenTracer
abrufen können.
-
main()
Funktion:- Ruft
InitTracerProvider()
zu Beginn auf und stellt sicher, dasstp.Shutdown()
beim Beenden aufgerufen wird, um alle gepufferten Spans zu leeren. - Gin-Integration:
router.Use(otelgin.Middleware("my-go-web-app"))
nutzt die OpenTelemetry-Middleware von Gin. Diese Middleware erstellt automatisch einen Root-Span für jede eingehende HTTP-Anfrage, extrahiert den Trace-Kontext aus eingehenden Headern (sofern vorhanden) und injiziert den Kontext in den GinRequest.Context()
. Dies ist entscheidend für die Kontextweitergabe bei HTTP-Anfragen. - Die Variable
tracer
wird mitotel.Tracer("my-go-web-app")
bezogen, die den global konfiguriertenTracerProvider
verwendet.
- Ruft
-
simulateDBQuery()
Funktion:tracer.Start(ctx, "db.get_user_data", ...)
erstellt manuell einen neuen Span für die Datenbankoperation.trace.WithSpanKind(trace.SpanKindClient)
gibt an, dass dieser Span einen Client-Aufruf darstellt (unsere Anwendung agiert als Client für die Datenbank).trace.WithAttributes(...)
fügt dem Span semantische Konventionen und benutzerdefinierte Attribute hinzu, die reichhaltige Kontextinformationen über den Datenbankaufruf liefern (System, Anweisung, Benutzer-ID). Dies ist entscheidend für das Filtern und Analysieren von Traces in Jaeger.defer span.End()
stellt sicher, dass der Span immer beendet wird, sodass seine Dauer aufgezeichnet wird.- Die Fehlerbehandlung umfasst
span.RecordError(err)
undspan.SetStatus(trace.StatusError, ...)
zur Anzeige von Fehlern innerhalb des Traces.
-
makeExternalAPIRequest()
Funktion:- Ähnlich wie die Datenbankfunktion wird ein neuer Span für den HTTP-Client-Aufruf erstellt.
otel.GetTextMapPropagator().Inject(ctx, otel.HeaderCarrier(req.Header))
ist ein kritischer Schritt für die verteilte Kontextweitergabe. Er nimmt den aktuellen Trace-Kontext ausctx
und injiziert ihn in die ausgehenden HTTP-Anforderungsheader (z. B.traceparent
). Wenn der externe Dienst ebenfalls mit OpenTelemetry instrumentiert ist, extrahiert er diesen Kontext und setzt den Trace fort, wodurch ein einzelner End-to-End-Trace gebildet wird.- Attribute wie
http.method
undhttp.url
liefern Details zur HTTP-Anfrage. Der HTTP-Statuscode der Antwort wird ebenfalls als Attribut hinzugefügt.
Beobachten der Traces
Nachdem Sie docker-compose up -d
ausgeführt und dann Ihre Go-Anwendung gestartet haben:
- Machen Sie eine Anfrage an Ihre Anwendung:
curl http://localhost:8080/user/123
. - Öffnen Sie die Jaeger UI unter
http://localhost:16686
. - Wählen Sie "go-web-app" aus dem Dienst-Dropdown und klicken Sie auf "Find Traces".
Sie sollten einen Trace sehen, der diesem ähnelt:
- GET /user/
(Gin HTTP Server Span - von otelgin.Middleware
)- db.get_user_data (Unser manuell erstellter Datenbank-Span)
- http.client.get_http://jsonplaceholder.typicode.com/todos/1 (Unser manuell erstellter HTTP-Client-Span)
Jeder Span hat detaillierte Attribute, einschließlich der von uns gesetzten, und bietet ein klares Bild davon, was während der Anfrage passiert ist. Sie können die Dauer jeder Operation untersuchen und potenzielle Engpässe identifizieren.
Fazit
Die manuelle Integration von OpenTelemetry zum Tracing von Datenbank- und HTTP-Client-Aufrufen in Go-Webanwendungen bietet eine granulare Sicht auf das Verhalten Ihres Systems. Durch das Verständnis der Kernkonzepte wie Traces, Spans und Kontextweitergabe sowie durch sorgfältige Instrumentierung mit dem OpenTelemetry SDK können Sie ein robustes Observability-Framework aufbauen. Dieser Ansatz ermöglicht es Entwicklern, Leistungsprobleme effizient zu diagnostizieren und die komplexen Interaktionen in ihren verteilten Diensten zu verstehen, was letztendlich zu stabileren und leistungsfähigeren Anwendungen führt. Die Einführung von OpenTelemetry ist ein wichtiger Schritt zur Erreichung umfassender System-Observability.