Tracing is een kritisch onderdeel dat verzoeken volgt door complexe systemen. Deze zichtbaarheid onthult knelpunten en fouten, waardoor sneller oplossingen mogelijk zijn. In een vorige post van onze serie over Go-webdiensten, onderzocht we de significantie van de observabiliteit. Vandaag聚焦我们 op tracing. Jaeger verzamelt, bewaart en visualiseert sporen uit geDistribueerde systemen. Het biedt belangrijke inzichten in de verzoekstromen over services. Door Jaeger te integreren met OpenTelemetry, kunnen ontwikkelaars hun traceringenadoptie eenvoudig te unificeren, ervoor zorgend dat er consistent en uitgebreid zicht is. Deze integratie simplificeert het diagnoseren van prestatiewijzigingen en verbetert daarmee de systemefficiëntie. In dit bericht zal we Jaeger instellen, het integreren met OpenTelemetry in onze toepassing en de sporen visualiseren voor dieper inzicht.
Motivatie
Wat we naderen is een Jaeger dashboard dat er ongeveer zo uitziet:
Als we naar verschillende delen van de app gaan (op de Onehub frontend), worden de sporen van de diverse verzoeken verzameld (vanaf het moment dat het op de grpc-gateway aankomt) met een samenvatting ervan. We kunnen zelfs in een van de sporen kijken voor een meer gedetailleerd overzicht. Bekijk het eerste POST
-verzoek (voor het aanmaken/verzenden van een bericht in een topic):
Hier zien we alle componenten die het Create
-verzoek aanraakt, samen met hun invoer-/uitvoertijden en de tijd die werd gebruikt binnen en buiten de methodes. Echt krachtig.
Het Begin
Korte versie: Om dit in actie te zien en de rest van de blog te valideren:
- De bron van dit bestand is in de PART11_TRACING branch.
- Bouw alle nodige dingen (als je de branch hebt uitgecheckt):
make build
- We hebben docker-compose verdeeld in twee delen ( dit zal later uitgelegd worden), dus zorg ervoor dat je twee vensters draait.
- Terminal 1:
make updb dblogs
- Terminal 2:
make up logs
- Navigeer naar localhost:7080, en we gaan erop aan.
Hoog niveau overzicht
Ons systeem is momenteel:
Met instrumentatie met OpenTelemetry, zal ons systeem evolueren naar:
Als we eerder noemen, is het voor elk dienst aparte klanten gebruiken om naar specifieke leveranciers te sturen, erg belastend. In plaats daarvan met een apart draaiende OTel-verzamelaar, kunnen we ervoor zorgen dat alle (belangrijke) diensten eenvoudig metrics/logs/sporen naar deze verzamelaar kunnen sturen, die dan aan hen verschillende achtergronden als nodig exporteert — in dit geval, Jaeger voor sporen.
Laat ons beginnen.
Installeer de OTel-verzamelaar
Het eerste stap is om onze OTel-verzamelaar toe te voegen die draait in de Docker-omgeving samen met Jaeger zodat ze bereikbaar zijn.
Opmerking: We hebben onze originele alomvattende docker-compose.yml
configuratie in twee delen verdeeld:
- db-docker-compose.yml: Bevat alle database en ondersteuning (niet-toepassings)gerelateerde componenten zoals databases (Postgres, Typesense) en monitoren diensten (OTel-verzamelaar, Jaeger, Prometheus, enzovoort).
- docker-compose.yml: Bevat alle toepassingsgerelateerde services (Nginx, gRPC Gateway, dbsync, frontend, enzovoort.)
De twee docker-compose omgevingen zijn via een gedeelde netwerk (onehubnetwork
) verbonden waardoor services in deze omgevingen met elkaar kunnen communiceren. Met deze scheiding hoeven we slechts een deel van de services opnieuw opstarten bij wijzigingen, wat ons ontwikkelingsproces versnelt.
Terug aan onze setup: in ons db-docker-compose.yml
, voeg de volgende services toe:
services
...
otel-collector
networks
onehubnetwork
image otel/opentelemetry-collector-contrib0.105.0
command"--config=/etc/otel-collector.yaml"
volumes
./configs/otel-collector.yaml:/etc/otel-collector.yaml
environment
POSTGRES_DB $ POSTGRES_DB
POSTGRES_USER $ POSTGRES_USER
POSTGRES_PASSWORD $ POSTGRES_PASSWORD
jaeger
networks
onehubnetwork
image jaegertracing/all-in-one1.59
container_name jaeger
environment
QUERY_BASE_PATH'/jaeger'
COLLECTOR_OTLP_GRPC_HOST_PORT'0.0.0.0:4317'
COLLECTOR_OTLP_HTTP_HOST_PORT'0.0.0.0:4318'
COLLECTOR_OTLP_ENABLEDtrue
prometheus
networks
onehubnetwork
image prom/prometheus v2.53.1
command
'--config.file=/etc/prometheus/prometheus.yml'
'--web.external-url=/prometheus/'
'--web.route-prefix=/prometheus/'
volumes
./configs/prometheus.yaml:/etc/prometheus/prometheus.yml
ports
9090:9090
Vrij eenvoudig, dit configureert twee services in ons Docker-omgeving:
otel-collector
: De sink voor alle signalen (metrics/logs/traces) die worden verzonden door de verschillende services die worden gemonitord (we zullen deze lijst over de tijd steeds verder uitbreiden), het gebruikt de standaard OTel-image samen met onze aangepaste OTel-config (onder) die verschillende observabiliteitspipelines beschrijft (d.w.z., hoe signalen moeten worden ontvangen, verwerkt en uitgevoerd in verschillende manieren).jaeger
: Ons Jaeger-exemplaar dat de traces (geexporteerd door deotel-collector
) zal ontvangen en opslaan, dit host zowel de opslag als het dashboard (UI) dat we exporteren onder de HTTP-padprefix/jaeger
om via nginx bereikbaar te zijn.prometheus
: Hoewel dit niet vereist voor deze post, exporteren we ook metrics zodat ze door Prometheus gescraped kunnen worden. We zullen dit in detail niet bespreken in deze post.
Enige dingen die opgemerkt moeten worden:
- Hoewel dit niet vereist is voor deze post, geven we de
POSTGRES
-verbindingsgegevens (als omgevingsvariabelen) aan deotel-collector
door, zodat het Postgres-gezondheidmetrics kan scrapen. - Jaeger (vanaf v1.35) ondersteunt OTLP nativel.
- Het voordeel van OTLP is dat OTel-verzamelaars gekoppeld kunnen worden, vorming een netwerk van OTel-verzamelaars/verwervers/doorstuwers/routeringscomponenten, enzovoort.
- OTLP kan worden aangeboden via een GRPC- of HTTP-eindpunt (op respectievelijk poort 4317 en 4318).
- Standaard worden de OTLP-diensten op
localhost:4317/4318
gestart. Dit is voldoende als Jaeger op hetzelfde host/pod wordt uitgevoerd waar de services (die worden gemonitord) zijn ingesteld. Echter, omdat Jaeger op een aparte pod draait, moeten ze worden gekoppeld aan externe adressen (0.0.0.0). Dit was niet duidelijk in de documentatie, wat tot significante verveling leidde. COLLECTOR_OTLP_ENABLED: true
is nu de standaard en moet niet expliciet worden gespecificeerd.
OTel-configuratie
OTel moet ook worden geconfigureerd met specifieke ontvangers, verwervers en exporteurs. We zullen dat doen in configs/otel-collector.yaml.
Receptoren toevoegen
We moeten de OTel-verzamelaar vertellen welke ontvangers moeten worden geactiveerd. Dat wordt gespecificeerd in de sectie receivers
:
receivers
otlp
protocols
http
endpoint 0.0.0.04318
grpc
endpoint 0.0.0.04317
postgresql
endpoint postgres5432
transport tcp
username $ POSTGRES_USER
password $ POSTGRES_PASSWORD
databases
$ POSTGRES_DB
collection_interval 10s
tls
insecuretrue
Dit activeert een OTLP ontvanger op poorten 4317 en 4318 (respectievelijk grpc
en http
). Er zijn vele soorten ontvangers die kunnen worden gestart. Als een voorbeeld, hebben we ook een “postgresql
” ontvanger toegevoegd die actief Postgres aanraakt voor metrieken ( hoewel dat niet relevant is voor dit bericht). Ontvangers kunnen ook op basis van pull of push zijn. Pull-gebaseerde ontvangers periodiek specifieke doelen aanraakt (bijvoorbeeld postgres
), terwijl push-gebaseerde ontvangers luisteren en “ontvangen” metrieken/logboeken/sporen van applicaties gebruikmakend van de OTel client SDK.
Dat is het. Nu is onze verzamelaar klaar om metrieken te ontvangen (of aan te raken).
Voeg Processors toe
Processors in OTel zijn een manier om ontvangen signalen te transformeren, te mapen, in batch te plaatsen, te filteren en/of op te waardeggen voordat ze worden geexporteerd. Bijvoorbeeld, processors kunnen metrieken samplen, logboeken filteren of zelfs signalen in batch plaatsen voor efficiëntie. Standaard worden geen processors toegevoegd (maak de verzamelaar een doorstroom). We zullen dit nu negeren.
Voeg Exporters toe
Nu is het tijd om te identificeren waar we de signalen willen exporteren naar: backends die het beste geschikt zijn voor de respectievelijke signalen. Net zoals ontvangers, kunnen exporteerders ook op basis van aanklikken of duwen werken. Duw-gebaseerde exporteerders worden gebruikt om signalen aan een andere ontvanger te verzenden die in duwmodus actief is. Deze zijn uitgaand. Aanklikkingsgebaseerde exporteerders bieden eindpunten die kunnen worden gescraped door andere aanklikkingsgebaseerde ontvangers (bijvoorbeeld prometheus
). We zullen een exporteur van elk soort toevoegen: een voor traceren en een voor Prometheus om vanaf te halen ( hoewel Prometheus niet het onderwerp van dit artikel is ):
exporters
otlp/jaeger
endpoint jaeger4317
tls
insecuretrue
prometheus
endpoint 0.0.0.09090
namespace onehub
debug
Hier hebben we een exporteur naar Jaeger die de OTLP-collector uitvoert, geïdentificeerd door otlp/jaeger
. Deze exporteur zal periodiek traceringen naar Jaeger verzenden. We voegen ook een “scraper”-eindpunt toe op poort 9090 die Prometheus periodiek vanaf halen zal.
De “debug”-exporteur wordt eenvoudigweg gebruikt om signalen af te voeren naar standaarduitvoer/foutstromen.
Defineer Pipelines
De ontvanger, processor en exporteursecties definiëren simpelweg de modules die door de collector ingeschakeld zullen worden. Ze worden nog niet opgeroepen. Om ze echt aan te roepen/te activeren, moeten ze vermeld worden als “pipelines”. Pipelines bepalen hoe signalen door de collector lopen en worden verwerkt. Onze pipeline-definitie (in de services
-sectie) zal dit duidelijk maken:
service
extensions health_check
pipelines
traces
receivers otlp
processors
exporters otlp/jaeger debug
metrics
receivers otlp
processors
exporters prometheus debug
Hier definiëren we twee pipelines. Noteer hoe de pipelines op elkaar lijken maar toestaan om twee verschillende exportmodi te gebruiken (Jaeger en Prometheus). Nu zien we de kracht van OTel en het maken van pipelines binnen het.
traces
:
- Ontvang signale van client SDKs
- Geen verwerking
- Exporteer traceringen naar de console en Jaeger
metrics
:
- Ontvang signaalkenmerken van client SDK’s
- Geen verwerking
- Exporteer metrieken naar de console en Prometheus (door een endpoint beschikbaar te maken om te verzamelen).
Meld Dashboards via Nginx
Jaeger biedt een dashboard aan voor het visualiseren van traceringgegevens over alle onze verzoeken. Dit kan in een browser worden gevisualiseerd door de volgende optie in onze Nginx config in te schakelen. Opnieuw, hoewel dit niet het onderwerp van deze post is – we expose ook de Prometheus UI via nginx op de HTTP-padvoorvoegsel /prometheus
.
...
location ~ ^/jaeger
if ($request_method = OPTIONS ) return 200;
proxy_pass http://jaeger:16686; # Note that JaegerUI starts on port 16686 by default
proxy_pass_request_headers on;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header Host-With-Port $http_host;
proxy_set_header Connection '';
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-HTTPS on;
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Prefix /;
proxy_http_version 1.1;
chunked_transfer_encoding off;
location ~ ^/prometheus
if ($request_method = OPTIONS ) return 200;
proxy_pass http://prometheus:9090;
proxy_pass_request_headers on;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header Host-With-Port $http_host;
proxy_set_header Connection '';
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-HTTPS on;
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Prefix /;
proxy_http_version 1.1;
chunked_transfer_encoding off;
...
Visualiseren van Traceringen in Jaeger
De Jaeger UI is vrij compleet en biedt verschillende functies die u kunt verkennen. Ga naar de Jaeger UI in de browser. U zult een uitgebreide interface voor het zoeken en analyseren van traceringen zien. Bezoek de hoofdsecties, inclusief de zoekbalk en de traceringenlijst, en maak uzelf bekend met ze. U kunt verschillende traceringen zoeken op zoekcriteria en filteren op dienst, tijdsduur, componenten, enzovoort.
Bekijk de traceringstijdlijnen in de verschillende verzoeken om de sequence van de operationele stappen te begrijpen. Elk span representeert een werkunit, met start- en eindtijd, duur en gerelateerde metadata. Deze gedetailleerde weergave is zeer handig bij het identificeren van prestatiebekkens en fouten binnen de tracering.
Integreren van de Client SDK
Tot nu toe hebben we onze systemen geconfigureerd om te visualiseren, signalen te consumeren, enzovoort. Echter, onze services worden nog niet bijgewerkt om de signalen uit te stralen naar OTel. Hier zal integration gerealiseerd worden met de (Golang) client SDK in verschillende delen van ons code. De SDK documentatie is een uitstekende plek om je eerst bekend te maken met enkele concepten.
De belangrijkste concepten waarmee we zullen werken, zijn beschreven hieronder.
Resources
Resources zijn de entiteit die de signalen produceert. In onze geval is de scope van de resource het binary dat de services bevat. Momenteel hebben we een enkele resource voor de gehele Onehub service, maar dit kan later verdeeld worden.
Dit is gedefinieerd in cmd/backend/obs.go. Merk op dat de client SDK ons niet noodzakelijkerwijs verplichtte om de details van de resource definitie uitdrukkelijk in te gaan. De standaard helper (sdktrace.WithResource
) laat ons een resource definitie maken door belangrijke delen (zoals process naam, pod naam, enzovoort) in runtime af te leiden.
We moesten maar een ding overschrijven: de OTEL_RESOURCE_ATTRIBUTES: service.name=onehub.backend,service.version=0.0.1
omgevingsvariabele voor de onehub
service in docker-compose.yml.
Contextuele Propagatie
Context propagation is een erg belangrijk onderwerp in hetgebied van observability. De verschillende pijlers zijn exponentieel krachtig wanneer we signaalen uit elke pijler kan correleren om problemen met ons systeem te identificeren. Denk aan contexten als extra stukken data die aan de signaalen kunnen worden gekoppeld: d.i., kunnen worden “geassocieerd” op een unieke manier om de verschillende signaalen aan een bepaald groepje (bijvoorbeeld een verzoek) te relateren.
Provider/Exporters
Voor elk van de signaalen biedt OTel een Provider interface (bijvoorbeeld TracerProvider
voor het exporteren van span/sporen, MeterProvider
voor het exporteren van metrieken, LoggerProvider
voor het exporteren van logboeken, enzovoort). Voor elk van deze interfaces kunnen er verschillende implementaties zijn, bijvoorbeeld een Debug provider voor verzenden naar stdout/errstromen, een OTel provider voor exporteren naar een andere OTel eindpunt (in een keten) of zelfs direct via een verscheidenheid aan exporteurs. Echter, in ons geval willen we de keuze voor enige leveranciers uit onze services uitstellen en in plaats daarvan alle signalen naar de OTel collector die in ons milieu draait sturen.
Om dit abstraheren zullen we een “OTELSetup
” type aanmaken dat de verschillende providers bijhoudt die we misschien willen gebruiken of vervangen. In cmd/backend/obs.go, hebben we:
type OTELSetup[C any] struct {
ctx context.Context
shutdownFuncs []ShutdownFunc
Resource *resource.Resource
Context C
SetupPropagator func(o *OTELSetup[C])
SetupTracerProvider func(o *OTELSetup[C]) (trace.TracerProvider, ShutdownFunc, error)
SetupMeterProvider func(o *OTELSetup[C]) (otelmetric.MeterProvider, ShutdownFunc, error)
SetupLogger func(o *OTELSetup[C]) (logr.Logger, ShutdownFunc, error)
SetupLoggerProvider func(o *OTELSetup[C]) (*log.LoggerProvider, ShutdownFunc, error)
}
Dit is een eenvoudige wrapper die de door de OTel SDK nodig zijnde algemene aspecten bijhoudt. Hier hebben we providers (Logger, Tracer en Metric) evenals manieren om context aan te bieden (voor tracing). De bovenliggende Resource die door alle providers wordt gebruikt, wordt hier ook gespecificeerd. Shutdown-functies zijn interessant. Ze zijn functies die worden aangeroepen door de providers wanneer de onderliggende exporter is beëindigd (met respectievelijk of door een afsluiten). De wrapper zelf neemt een generieke zodat specifieke instantieën van deze Setup hun eigen aangepaste gegevens kunnen gebruiken.
Het repository bevat twee implementaties hiervan:
- Logging Signals naar standaard uitvoer/fout – cmd/backend/stdout.go
- Exporteren Signals naar een andere OTel collector – cmd/backend/otelcol.go
We zullen de tweede instantieren in onze app. We zullen geen details over de specifieke implementaties binnenkomen omdat deze zijn onttrokken aan de voorbeelden in de SDK met kleine reparaties en refactoring. Specifiek, kijk voor inspiratie naar het otel-collector voorbeeld.
Initialiseer de OTelProviders.
Het kerndoel van het inschakelen van de collector in onze diensten is dat er bij alle “ingangs” punten iets van een OTel-gerelateerde “context” wordt gestart. Als deze context aan het begin wordt aangemaakt, zal hij naar alle hier genoemde doelen worden gestuurd, die vervolgens wordt doorgegeven (zolang we het juiste doen).
Bij het eenvoudige ListTopics
API-verzoek (api/vi/topics
) gaat ons verzoek de volgende weg en terug:
[ Browser ] ---> [ Nginx ] ---> [ gRPC Gateway ] ---> [ gRPC Service ] ---> [ Database ]
In ons geval zijn de ingangs punten hier aan het begin als de gRPC Gateway een API-verzoek van Nginx ontvangt (we kunnen ze vanaf het punt beginnen te traceren vanaf de HTTP-verzoek dat Nginx raakt, maar we zullen dat even uitstellen).
Wat nodig is:
- gRPC Gateway ontvangt een verzoek.
- Het maakt een “aangepaste” OTel-specifieke
context.Context
-instantie. - Maakt een aangepaste verbinding tot de respectievelijke gRPC-dienst (bijvoorbeeld
TopicService
) en geeft deze context aan in plaats van de standaard een. - De respectievelijke dienst gebruikt deze context bij het verzenden van de sporen.
Laten we dit stap voor stap doorlopen.
Initializeer en voorbereid de OTel-SDK voor gebruik
In main.go, laten we eerst de collector verbinding initialiseren:
func main() {
flag.Parse()
// Handle SIGINT (CTRL+C) gracefully.
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
collectorAddr := cmdutils.GetEnvOrDefault("OTEL_COLLECTOR_ADDR", "otel-collector:4317")
conn, err := grpc.NewClient(collectorAddr,
// Note the use of insecure transport here. TLS is recommended in production.
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Println("failed to create gRPC connection to collector: %w", err)
return
}
setup := NewOTELSetupWithCollector(conn)
err = setup.Setup(ctx)
if err != nil {
log.Println("error setting up otel: ", err)
}
defer func() {
err = setup.Shutdown(context.Background())
}()
ohdb := OpenOHDB()
srvErr := make(chan error, 2)
httpSrvChan := make(chan bool)
grpcSrvChan := make(chan bool)
go startGRPCServer(*addr, ohdb, srvErr, httpSrvChan)
go startGatewayServer(ctx, *gw_addr, *addr, srvErr, grpcSrvChan)
// Wait for interruption.
select {
case err = <-srvErr:
log.Println("Server error: ", err)
// Error when starting HTTP server or GRPC server
return
case <-ctx.Done():
// Wait for first CTRL+C.
// Stop receiving signal notifications as soon as possible.
stop()
}
// When Shutdown is called, ListenAndServe immediately returns ErrServerClosed.
httpSrvChan <- true
grpcSrvChan <- true
...
}
- Lijnen 8-16: We maken een verbinding met de
otel-collector
die wordt uitgevoerd in ons Docker-omgeving. - Lijnen 17-21: We configureren dan OTel met tracer- en metriekproviders zodat onze collector nu alle sporen en metrieken (onthouden we de ontvangers die we eerder in de OTel-config definieerden) kunnen verzenden.
- Lijnen 23-25: We configureren sluiters om bij afsluiten schoon te maken van OTel-verbindingen/providers, enzovoort.
- Lijn 27: We configureren onze database en verbindingen net zoals eerder.
- Lijnen 29 +: Vroeger startten we gewoon de GRPC- en Gateway-diensten in de achtergrond en dat was het. We kregen niet echt om hun terugkeer of afsluitstatus. Voor een sterker systeem is het belangrijk om beter inzicht te krijgen in het levenscyclus van de services die we starten. Nu geven we de “terugkoppeling” kanaal voor elke van de services die we starten. Wanneer de servers afsluiten, roepen de respectievelijke methodes terug op deze beschikbare kanalen aan om te kennen dat ze afgesloten zijn op een mooie manier. Ons gehele binary sluit af wanneer een van deze services afsluit.
Bijvoeglijk, laten we kijken hoe onze gateway-dienst deze kanaal gebruikt.
In plaats van het HTTP-server (voor de grpc-gateway) te starten:
1 http.ListenAndServe(gw_addr, mux)
We hebben nu:
server := &http.Server{
Addr: gw_addr,
BaseContext: func(_ net.Listener) context.Context { return ctx },
Handler: otelhttp.NewHandler(mux, "gateway", otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string {
return fmt.Sprintf("%s %s %s", operation, r.Method, r.URL.Path)
})),
}
go func() {
<-stopChan
if err := server.Shutdown(context.Background()); err != nil {
log.Fatalln(err)
}
}()
srvErr <- server.ListenAndServe()
Let op lijnen 9-14 waar de server afsluiten wordt bewaakt in een apart goroutine en lijn 15 waar, als er een fout was bij het afsluiten van de server, deze terug wordt gestuurd via het “notificatie” kanaal dat als argument aan deze methode werd gegeven.
Nu hebben de verschillende delen van onze diensten toegang tot een “actieve” OTLP-verbinding, die gebruikt kan worden wanneer signaalen verzonden moeten worden.
OTel Middleware voor gRPC Gateway
Bovenstaande http.Server
-instantie die gebruikt wordt om de gRPC Gateway te starten, maakt gebruik van een aangepaste handelaar: de http.Handler
uit het OTel HTTP-pakket. Deze handelaar neemt een bestaande http.Handler
-instantie in beslag, decoreert hem met het OTel-context en zorgt ervoor dat zijn voortzetting door deelsystemen die daaronder staan.
server := &http.Server{
Addr: gw_addr,
BaseContext: func(_ net.Listener) context.Context { return ctx },
Handler: otelhttp.NewHandler(mux, "gateway", otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string {
return fmt.Sprintf("%s %s %s", operation, r.Method, r.URL.Path)
})),
}
Onze HTTP-handelaar is eenvoudig:
- Regel 4: We maken een nieuwe OTel-specifiekeomslag om HTTP-verzoeken af te handelen.
- Regel 5: We stellen de
SpanFormatter
-optie in zodat de sporen uniek kunnen worden geïdentificeerd op basis van de methode en HTTP-verzoekpaden. Zonder dezeSpanNameFormatter
, zou de standaard “naam” voor onze sporen op de gateway simpelweg"gateway"
zijn, waardoor alle sporen eruit zien zoals dit:
Omslag Gateway naar gRPC-aanroepen met OTel
Standaard creert de gRPC Gateway-bibliotheek een “gewone” context bij het aanmaken/beheren van verbindingen naar de onderliggende GRPC-diensten. Na allebei, kent de gateway immers niets van OTel. In dit modus, zal een verbinding (van de gebruiker/browser) naar de gRPC Gateway en de verbinding van de gateway naar de gRPC-dienst worden behandeld als twee verschillende sporen.
Daarom is het belangrijk om de verantwoordelijkheid voor het maken van gRPC-verbindingen af te nemen van de Gateway en in plaats daarvan een al OTel-bewuste verbinding aan te bieden. Dat zal nu gebeuren.
Voor de integratie van OTel registreerden we een Gateway handler voor onze gRPC’s met:
ctx := context.Background()
mux := runtime.NewServeMux() // Not showing the interceptors
opts := []grpc.DialOption{grpc.WithInsecure()}
// grpc_addr = ":9090"
v1.RegisterTopicServiceHandlerFromEndpoint(ctx, mux, grpc_addr, opts)
v1.RegisterMessageServiceHandlerFromEndpoint(ctx, mux, grpc_addr, opts)
// And other servers
Nu is het eenvoudig om een andere verbinding doorgeven:
mux := // Creat the mux runtime as usual
// Use the OpenTelemetry gRPC client interceptor for tracing
trclient := grpc.WithStatsHandler(otelgrpc.NewClientHandler())
conn, err := grpc.NewClient(grpc_addr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
trclient)
if err != nil {
srvErr <- err
}
err = v1.RegisterTopicServiceHandler(ctx, mux, conn)
if err != nil {
srvErr <- err
}
// And the Message and User server too...
Wat we hebben gedaan is eerst een client
(Regel 6) maken die als verbindingsfabriek voor onze gRPC-server fungeert. De client is vrij eenvoudig. Er wordt alleen maar de gRPC ClientHandler
(otelgrpc.NewClientHandler
) gebruikt om de verbinding aan te maken. Dit zorgt ervoor dat het context in de huidige trace die is begonnen op een nieuwe HTTP-verzoek nu wordt doorgegeven aan de gRPC-server via deze handler.
Dat is het. Nu zouden we moeten beginnen met het zien van de nieuwe verzoeken aan de Gateway en de Gateway->gRPC-verzoeken in een enkele geconsolideerde trace in plaats van als twee verschillende tracen.
Begin en Einde Spans
We zijn bijna klaar. tot nu toe:
- We hebben de OTel-verzamelaar en Jaeger ingeschakeld om trace (span) gegevens te ontvangen en op te slaan (in docker-compose).
- We hebben de basis OTel-verzamelaar (die als aparte pod draait) als onze “provider” van tracers, meetbalken en logboeken ingesteld (d.w.z., onze toepassing zal deze endpoint gebruiken om alle signalen af te leveren).
- We hebben de Gateway HTTP-handler om OTel-compatibel te maken zodat tracen en hun contexten worden aangemaakt en doorgegeven.
- We hebben de (gRPC)-client in de gateway overschreven zodat hij nu de OTel-context van onze OTel-instellingen omvat in plaats van een standaard context te gebruiken.
- We hebben globale tracer/meter/logger-instanties gemaakt zodat we echt signalen kunnen verzenden met behulp van hen.
Nu moeten we spans aanmaken voor alle “interessante” plaatsen in ons code. Neem bijvoorbeeld de methode ListTopics
(in services/topics.go) voorbeeld:
func (s *TopicService) ListTopics(ctx context.Context, req *protos.ListTopicsRequest) (resp *protos.ListTopicsResponse, err error) {
results, err := s.DB.ListTopics(ctx, "", 100)
if err != nil {
return nil, err
}
resp = &protos.ListTopicsResponse{Topics: gfn.Map(results, TopicToProto)}
return
}
We bellen de database aan om de topics op te halen en terug te geven. Het is vergelijkbaar met de database toegangsmethode (in datastore/topicds.go):
func (tdb *OneHubDB) ListTopics(ctx context.Context, pageKey string, pageSize int) (out []*Topic, err error) {
query := tdb.storage.Model(&Topic{}).Order("name asc")
if pageKey != "" {
count := 0
query = query.Offset(count)
}
if pageSize <= 0 || pageSize > tdb.MaxPageSize {
pageSize = tdb.MaxPageSize
}
query = query.Limit(pageSize)
err = query.Find(&out).Error
return out, err
}
Hier zullen we vooral geïnteresseerd zijn in hoeveel tijd er wordt gespendeerd in elk van deze methodes. We maken simpelweg spans in elk van deze en dat is het. Onze toevoegingen aan de service- en datastoremethodes (respectievelijk) zijn:
services/topics.go
:
func (s *TopicService) ListTopics(ctx context.Context, req *protos.ListTopicsRequest) (resp *protos.ListTopicsResponse, err error) {
ctx, span := Tracer.Start(ctx, "ListTopics")
defer span.End()
... rest of the code to query the DB and return a proto response
}
datastore/topicds.go
:
func (tdb *OneHubDB) ListTopics(ctx context.Context, pageKey string, pageSize int) (out []*Topic, err error) {
_, span := Tracer.Start(ctx, "db.ListTopics")
defer span.End()
... rest of the code to fetch rows from the DB and return them
}
Het algemeen patroon is:
1. Maak een span:
ctx, span := Tracer.Start(ctx, "<span name>")
Hier wordt de gegeven context (ctx
) “omwikkeld” en een nieuwe context wordt teruggegeven. We kunnen (en moeten) deze nieuwe omwikkelde context verder doorsturen aan andere methodes. We doen exact dat als we de datastore ListTopics
methode aanroepen.
2. Einde van de span:
defer span.End()
Eindigen van een span (wanneer de methode terugkeert) zorgt ervoor dat de juiste afsluitertijden/codes, enzovoort, worden geregistreerd. We kunnen ook andere dingen doen zoals tags en status’ toevoegen aan dit als nodig om meer informatie bij te houden om de foutopsporing te helpen.
Dat is het. U kunt uw prachtige sporen in Jaeger zien en krijgt steeds meer inzicht in de prestaties van uw aanvragen, van begin tot eind!
Conclusie
We hebben in dit bericht veel opgeschoond en nog steeds zijn we maar bij de basis gebleven van OTel en spoorbaarheid. In plaats van deze (al alomvuldende) post te overbelasten, zullen we in toekomstige berichten nieuwere concepten en ingewikkelde details introduceren. Voor nu kunt u deze proberen uit te voeren in uw eigen diensten en probeer met andere exporteurs en ontvangers te spelen in de otel-contrib repo.
Source:
https://dzone.com/articles/tracing-with-opentelemetry-and-jaeger