Rastreio, um componente crítico, segue pedidos através de sistemas complexos. Esta visibilidade revela engastes e erros, permitindo resoluções mais rápidas. Em uma postagem anterior da nossa série de serviços web Go, exploramos a importância da observabilidade. Hoje, nos focamos no rastreio. O Jaeger coleta, armazena e visualiza rastreamentos de sistemas distribuídos. Ele fornece insigts críticas em fluxos de pedidos entre serviços. Conectando o Jaeger com o OpenTelemetry, os desenvolvedores podem unificar seu método de rastreamento, garantindo visibilidade consistente e abrangente. Esta integração simplifica a diagnose de problemas de desempenho e melhora a confiabilidade do sistema. Neste post, configuraremos o Jaeger, integraremos-no com o OpenTelemetry em nosso aplicativo e exploraremos visualizações de rastreamentos para maiores insights.
Motivação
O que estamos trabalhando para é um painel do Jaeger que se parece com isto:
Enquanto nós percorremos diferentes partes do app (no frontend do Onehub), os rastreamentos de pedidos variam (a partir do ponto de entrada no grpc-gateway) com um resumo de cada um deles. Podemos até mesmo perfurar em um dos rastreamentos para uma visão mais detalhada. Olhe para o primeiro POST
request (para criar/enviar uma mensagem em um tópico):
Aqui nós vemos todos os componentes que o pedido Criar
toca, juntamente com seus tempos de entrada/saída e o tempo levado para e das funções. Muito poderoso, de fato.
Agora Vamos Começar
TL;DR: Para ver isso em ação e validar o resto do blog:
- A fonte deste é no PART11_TRACING branch.
- Construa todas as coisas necessárias (depois de tirar um checkout da branch):
make build
- Nós dividimos o docker-compose em duas partes (isso será explicado mais adiante), então certifique-se de que você tem duas janelas rodando.
- Terminal 1:
make updb dblogs
- Terminal 2:
make up logs
- Navegue em localhost:7080, e vá para a frente.
Visão de nível alto
Nosso sistema atualmente é:
Com instrumentação com OpenTelemetry, nosso sistema evoluirá para:
Como mencionamos anteriormente, é muito oneroso para cada serviço usar clientes separados para enviar para determinados fornecedores. Em vez disso, com um coletor OTel executando separadamente, podemos garantir que todos (interessados) os serviços simplesmente podem enviar métricas/logs/rastreamentos para este coletor, que em seguida os pode exportar para eles vários backends conforme necessário — neste caso, Jaeger para rastreamentos.
Vamos começar.
Configurar o Coletor OTel
O primeiro passo é adicionar nosso coletor OTel executando no ambiente Docker, juntamente com Jaeger para que eles sejam acessíveis.
Nota: Nós dividimos nossa configuração original abrangente docker-compose.yml
em duas partes:
- db-docker-compose.yml: Contém todos os componentes relacionados a banco de dados e infraestrutura (não-aplicação) como bancos de dados (Postgres, Typesense) e serviços de monitoramento (coletor OTel, Jaeger, Prometheus, etc).
- docker-compose.yml: Contém todos os serviços relacionados à aplicação (Nginx, gRPC Gateway, dbsync, frontend, etc.)
As duas ambientes de docker-compose estão conectados por uma rede compartilhada (onehubnetwork
) através da qual os serviços nesses ambientes podem se comunicar uns com os outros. Com essa separação, só precisamos reiniciar um subconjunto de serviços após as mudanças, acelerando assim o nosso desenvolvimento.
Voltando à nossa configuração: no nosso db-docker-compose.yml
, adicionamos os seguintes serviços:
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
Bem simples, isso configura dois serviços no nosso ambiente Docker:
otel-collector
: O ponto final de todas as sinais (metrics/logs/traces) enviadas pelos vários serviços monitorados (nós vamos continuar adicionando a este lista ao longo do tempo), ele usa a imagem padrão OTel juntamente com a nossa configuração OTel personalizada (abaixo) que descreve vários pipelines de observabilidade (isto é, como as sinais devem ser recebidas, processadas e exportadas de várias formas).jaeger
: Nossa instância de Jaeger que vai receber e armazenar rastreamentos (exportados pelootel-collector
), este hospeda tanto o armazenamento quanto o dashboard (UI) que nós exportaremos no prefixo de caminho HTTP/jaeger
para que seja acessível via nginx.prometheus
: Embora não obrigatório para este post, nós também exportaremos métricas para que possa ser rascunhadas por Prometheus. Não discutiremos isto em detalhe neste post.
Algumas coisas para notar:
- Embora não obrigatório para este post, estamos passando os detalhes de conexão com
POSTGRES
(como variáveis de ambiente) para ootel-collector
para que possa rascunhar as métricas de saúde do Postgres. - Jaeger (a partir da versão 1.35) suporta OTLP de forma nativa.
- A beleza do OTLP é que os coletores OTel podem ser encadeados, formando uma rede de coletores/processadores/encaminhadores/roteadores, etc.
- O OTLP pode ser fornecido através de um ponto de extremidade GRPC ou de HTTP (na porta 4317 e 4318 respectivamente).
- Por padrão, os serviços OTLP são iniciados em
localhost:4317/4318
. Isto é bom se o Jaeger for executado no mesmo host/pod onde os serviços (monitorados) estão rodando. No entanto, já que o Jaeger está sendo executado em um pod separado, eles têm que estar vinculados a endereços externos (0.0.0.0). Isso não estava claro na documentação e resultou em muito confusão. COLLECTOR_OTLP_ENABLED: true
é agora o padrão e não precisa ser especificado explicitamente.
Configuração de OTel
O OTel também precisa ser configurado com receptores específicos, processadores e exportadores. Vamos fazer isso em configs/otel-collector.yaml.
Adicionando Receptores
Nós precisamos informar o coletor OTel quais receptores devem ser ativados. Isto é especificado na seção 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
Isto ativa um receptor OTLP nas portas 4317 e 4318 (grpc
, http
respectivamente). Existem muitos tipos de receptores que podem ser iniciados. Por exemplo, também adicionamos um receptor “postgresql
” que irá coletar ativamente os metrícos do Postgres (embora isso não seja relevante para este post). Os receptores podem também ser baseados em pull ou push. Os receptores baseados em pull coletam periodicamente alvos específicos (por exemplo, postgres
), enquanto os receptores baseados em push ouvir e “receber” enviam metrícos/logs/rastreamentos de aplicações usando o SDK de cliente OTel.
É isso mesmo. Agora, o nosso coletor está pronto para receber (ou coletar) os metrícos apropriados.
Adicionar Processadores
Os processadores em OTel são uma maneira de transformar, mapear, agrupar, filtrar e/ou enriquecer sinais recebidos antes de exportá-los. Por exemplo, os processadores podem amostrar metrícos, filtrar logs ou até agrupar sinais para melhor eficiência. Por padrão, nenhum processador é adicionado (fazendo o coletor um pass-through). Iremos ignorar isso por agora.
Adicionar Exportadores
Agora é hora de identificar onde queremos que os sinais sejam exportados para backends que são otimizados para os respectivos sinais. Tal como os receptores, os exportadores também podem ser baseados em pull ou push. Os exportadores baseados em push são usados para emitir sinais para outro receptor que age em modo push. Estes são saídas. Os exportadores baseados em pull expõem pontos finais que podem ser raspados por outros receptores baseados em pull (por exemplo, prometheus
). Vamos adicionar um exportador de cada tipo: um para rastreamento e um para que o Prometheus possa raspar dele (apesar do Prometheus não ser o assunto deste post):
exporters
otlp/jaeger
endpoint jaeger4317
tls
insecuretrue
prometheus
endpoint 0.0.0.09090
namespace onehub
debug
Aqui temos um exportador para o Jaeger que está executando o coletor OTLP, conforme indicado por otlp/jaeger
. Este exportador vai enviar regularmente rastreamentos para o Jaeger. Também estamos adicionando um “ponto de raspagem” na porta 9090 que o Prometheus vai raspar regularmente dele.
O exportador “debug” é simplesmente usado para gravarmos sinais em saídas padrão de dados/erros.
Defina Pipelines
As seções de receptor, processador e exportador simplesmente definem os módulos que serão habilitados pelo coletor. Eles ainda não são chamados. Para chamar/ativá-los realmente, eles devem ser referenciados como “pipelines”. As pipeline definem como os sinais fluem e são processados pelo coletor. As nossas definições de pipeline (na seção services
) vão clarificar isso:
service
extensions health_check
pipelines
traces
receivers otlp
processors
exporters otlp/jaeger debug
metrics
receivers otlp
processors
exporters prometheus debug
Aqui estamos definindo duas pipelines. Note quão semelhantes as pipelines são, mas permitem dois modos de exportação diferentes (Jaeger e Prometheus). Agora estamos vendo o poder de OTel e na criação de pipelines dentro dele.
traces
:
- Receba sinais de clientes SDKs
- Sem processamento
- Exportar rastreios para console e Jaeger
metrics
:
- Receber sinais de SDKs do cliente
- Sem processamento
- Exportar métricas para o console e Prometheus (ao expor um endpoint para que ele cole).
Expor Dashboards através de Nginx
O Jaeger fornece um dashboard para visualizar dados de rastreio sobre todas nossas solicitações. Isto pode ser visualizado em um navegador, habilitando o seguinte em nosso configuração de Nginx. Novamente, porém, não é o assunto deste post – estamos também expondo a interface do Prometheus via nginx na prefixo de caminho HTTP /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;
...
Visualizar Rastreios no Jaeger
A interface do Jaeger UI é bastante abrangente e tem várias funcionalidades que você pode explorar. Navegue até o Jaeger UI no navegador. Você verá uma interface abrangente para procurar e analisar rastreios. Vá à frente e familiarize-se com as seções principais, incluindo a barra de pesquisa e a lista de rastreios. Você pode procurar vários rastreios por critérios de pesquisa e filtrar por serviço, tempo de duração, componentes, etc.
Analise as linhas de tempo dos rastreios em diferentes solicitações para entender a sequência de operações. Cada span representa uma unidade de trabalho, mostrando horários de início e fim, duração e metadados relacionados. Esta visão detalhada é muito útil para identificar pontos de estrangulamento de performance e erros dentro do rastreamento.
Integrar o SDK do Cliente
Até agora, nós configuramos os nossos sistemas para visualizar, consumir sinais, etc.然而,我们的服务仍然没有更新以向OTel发送信号。在这里,我们将使用(Golang)客户端SDK在代码的各种部分进行集成。SDK的文档是熟悉一些概念的绝佳地方。
我们将处理的关键概念描述如下。
资源
资源是产生信号的实体。在我们这种情况下,资源的作用域是托管服务的二进制文件。目前,我们整个Onehub服务的单一资源,但稍后可以分开。
这定义在cmd/backend/obs.go中。请注意,客户端SDK不需要我们明确进入资源定义的详细信息。标准助手(sdktrace.WithResource
)让我们通过推断最有用的部分(如进程名称、容器名称等)来创建资源定义。
我们只覆盖了一件事:在docker-compose.yml中的onehub
服务中,覆盖了OTEL_RESOURCE_ATTRIBUTES: service.name=onehub.backend,service.version=0.0.1
环境变量。
Propagação de Contextos
Propagação de contexto é um tópico muito importante em observabilidade. Os diferentes pilares são de forma exponencial poderosos quando podemos correlacionar sinais de cada um dos pilares para identificar problemas no nosso sistema. Pense em contextos como extra bits de dados que podem ser associados aos sinais: por exemplo, podem ser “unidos” de alguma maneira única para relacionar os diferentes sinais a um determinado grupo (digamos uma solicitação).
Fornecedores/Exportadores
Para cada sinal, o OTel fornece uma interface de Fornecedor (por exemplo, TracerProvider
para exportar spans/rastreios, MeterProvider
para exportar métricas, LoggerProvider
para exportar logs, e assim por diante). Para cada destas interfaces, pode haver várias implementações, por exemplo, um fornecedor de Debug para enviar para fluxos stdout/err, um fornecedor OTel para exportar para outro ponto final OTel (em uma cadeia), ou até mesmo diretamente por meio de uma variedade de exportadores. No entanto, na nossa situação, queremos adiar a escolha de qualquer fornecedor para fora de nossos serviços e, em vez disso, enviar todos os sinais para o coletor OTel rodando no nosso ambiente.
Para abstrair isso, vamos criar um tipo “OTELSetup
” que mantenha o controle dos vários fornecedores que possamos querer usar ou substituir. Em cmd/backend/obs.go, temos:
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)
}
Este é um simples envoltorio para manter controle de aspectos comuns necessários pelo OTel SDK. Aqui temos provedores (Logger, Tracer e Metric) assim como formas de fornecer contexto (para rastreamento). O recurso abrangente usado por todos os provedores também é especificado aqui. As funções de desligamento são interessantes. São funções chamadas pelos provedores quando o exportador subjacente termina (gradativamente ou devido a um encerramento). O próprio envoltorio permite uma instância genérica, permitindo que instanciadores específicos deste Setup usem seus próprios dados customizados.
O repositório contém duas implementações deste:
- Logging para sinais de saída padrão/erro – cmd/backend/stdout.go
- Exportação para outro coletor OTel – cmd/backend/otelcol.go
Nossa aplicação instantiará a segunda. Não entraremos em detalhes das implementações específicas, já que foram tiradas das exemplos no SDK com pequenas correções e refatorações. Especificamente, olhem para o exemplo otel-collector para inspiração.
Inicialize os OTelProviders.
A essência de habilitar o coletor em nossos serviços é que algum tipo de “contexto” relacionado a OTel é iniciado em todos os “pontos de entrada”. Se este contexto for criado no início, ele será enviado para todos os destinos chamados aqui, que depois será propagado subsequentemente (a menos que nós façamos o que é certo).
Tomando a simples chamada de API ListTopics
(api/vi/topics
), nossa solicitação percorre o seguinte caminho e volta:
[ Browser ] ---> [ Nginx ] ---> [ gRPC Gateway ] ---> [ gRPC Service ] ---> [ Database ]
No nosso caso, os pontos de entrada aqui são no início quando o gRPC Gateway recebe uma solicitação de API de Nginx (podemos começar a fazer rastreamento deles a partir do ponto em que a solicitação HTTP atinge o Nginx para mesmo destacar latências no Nginx, mas vamos adiar isso por um pouco).
O que é necessário é:
- O gRPC Gateway recebe uma solicitação.
- Cria uma instância “personalizada” de
context.Context
específica de OTel. - Cria uma conexão personalizada com o respectivo serviço gRPC (ex.,
TopicService
) passando este contexto em vez do padrão - O respectivo serviço então usa este contexto quando emite as pistas.
Vamos passar por isso passo a passo.
Inicializar e Preparar o SDK OTel para Uso
Em main.go, vamos primeiro inicializar a conexão coletora:
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
...
}
- Linhas 8-16: Criamos uma conexão com o
otel-collector
rodando em nosso ambiente Docker. - Linhas 17-21: Em seguida, configuramos o OTel com provedores de rastreamento e métricas para que o nosso coletor possa agora enviar todos os rastreamentos e métricas (lembrar que anteriormente definimos os receptores na configuração do OTel).
- Linhas 23-25: Configuramos finalizadores para limpar conexões/provedores do OTel, etc., ao encerrar.
- Linhas 27: Configuramos nossa base de dados e conexões como antes.
- Linhas 29 +: Anteriormente, simplesmente iniciavamos os serviços GRPC e Gateway em segundo plano e era isso. Não importava muito sobre sua retorno ou status de saída. Para um sistema mais resistente, é importante ter melhor visão dos ciclos de vida dos serviços que estamos iniciando. Então agora passamos o canal “callback” para cada um dos serviços que estamos iniciando. Quando os servidores saem, os métodos respectivos chamarão de volta nestes canais disponíveis para indicar que saíram com granaça. Nossa binário inteiro sairá quando algum destes serviços sair.
Por exemplo, vejamos como o serviço de gateway nosso aproveita este canal.
Em vez de iniciar o servidor HTTP (para o grpc-gateway) como:
1 http.ListenAndServe(gw_addr, mux)
Temos agora:
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()
Dê atenção às linhas 9-14 onde o encerramento do servidor é observado em uma goroutine separada e à linha 15 onde, se houver um erro quando o servidor sair, é enviado de volta via o canal “notificação” que foi passado como um argumento para este método.
Agora, as várias partes dos nossos serviços têm acesso a uma “conexão OTLP ativa” para usar quando forem enviar sinais.
Middleware OTel para gRPC Gateway
Acima, a instância de http.Server
usada para iniciar o gRPC Gateway está usando um manipulador personalizado: o http.Handler
no pacote OTel HTTP. Este manipulador pega uma instância existente de http.Handler
, decorada com o contexto OTel e garante sua propagação para qualquer outro downstream que é chamado.
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)
})),
}
O nosso manipulador HTTP é simples:
- Linha 4: Criamos um novo envoltório específico de OTel para lidar com pedidos HTTP.
- Linha 5: Configuramos a opção
SpanFormatter
para que as traças sejam identificadas unicamente pelo método e pelas rotas de pedido HTTP. Sem esteSpanNameFormatter
, o nome padrão de nossas traças no gateway seria simplesmente"gateway"
, resultando em todas as traças parecendo assim:
Envolvendo o Gateway para Chamadas gRPC com OTel
Por padrão, a biblioteca gRPC Gateway cria um “contexto simples” quando criando/gerenciando conexões com os serviços gRPC subjacentes. Afinal, o gateway não sabe nada sobre OTel. Neste modo, uma conexão (do usuário/navegador) para o gRPC Gateway e a conexão do gateway para o serviço gRPC serão tratadas como duas traças diferentes.
Então, é importante remover a responsabilidade de criar conexões gRPC do Gateway e, em vez disso, fornecer uma conexão que é já consciente de OTel. Vamos fazer isso agora.
Antes da integração com o OTel, nós registrávamos um manipulador de Gateway para nossos gRPCs com:
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
Agora passar uma conexão diferente é simples:
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...
O que nós fizemos primeiro é criar um client
(Linha 6) que age como uma fábrica de conexões para nosso servidor gRPC. O cliente é bastante simples. Apenas o gRPC ClientHandler
(otelgrpc.NewClientHandler
) está sendo usado para criar a conexão. Isto garante que o contexto na atual trace que começou em uma nova solicitação HTTP agora é propagado para o servidor gRPC via este manipulador.
É assim que fica. Agora nós devemos começar a ver a nova solicitação para o Gateway e a solicitação Gateway->gRPC em uma única trace consolidada, em vez de duas traces diferentes.
Início e Fim de Spans
Estamos quase lá. Até agora:
- Nós habilitamos o coletor OTel e o Jaeger para receber e armazenar dados de trace (span) (em docker-compose).
- Nós configuramos o coletor básico de OTel (executando como um pod separado) como nosso “fornecedor” de tracers, métricas e logs (isto é, a integração de OTel em nossa aplicação usaria este ponto de extremidade para depositar todas as sinais).
- Nós envolvemos o manipulador HTTP do Gateway para ser habilitado para OTel, para que as traces e seus contextos sejam criados e propagados.
- Nós substituímos o cliente (gRPC) no gateway, para que agora envolva o contexto de OTel de nossa configuração OTel em vez de usar um contexto padrão.
- Nós criamos instâncias globais de tracer/medidor/logger, para que possamos enviar sinais reais usando eles.
Agora precisamos de emitir spans para todos os “locais interessantes” em nosso código. Tomem por exemplo o método ListTopics
(em services/topics.go):
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
}
Chamamos a base de dados para obter os tópicos e os retornamos. Similar ao método de acesso à base de dados (em 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
}
Aqui, nossa principal integração seria em quanto tempo é gasto em cada um destes métodos. Simplesmente criamos spans em cada um e pronto. As nossas adições aos métodos de serviço e armazenamento de dados (respectivamente) são:
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
}
O padrão geral é:
1. Criar um span:
ctx, span := Tracer.Start(ctx, "<span name>")
Aqui, o contexto fornecido (ctx
) é “envolvido” e um novo contexto é retornado. Podemos (e devemos) passar este novo contexto envolvido a métodos adicionais. Fazemos exatamente isso quando chamamos o método de armazenamento de dados ListTopics
.
2. Encerrar o span:
defer span.End()
Encerrar um span (sempre que o método retornar) garante que os tempos de finalização certos/códigos, etc., sejam registrados. Também podemos fazer outras coisas como adicionar tags e statuses a isso, se necessário, para carregar mais informações para ajudar na depuração.
É isso mesmo. Você pode ver suas belas tracers no Jaeger e adquirir mais e melhores insights sobre o desempenho das suas requisições, de ponta a ponta!
Conclusão
Cobrimos muito neste post e ainda pouco mais do que tocaram as superfícies dos detalhes por trás do OTel e do rastreamento. Em vez de sobrecarregar este post (já carregado), vamos apresentar conceitos mais novos e detalhes intrincados em posts futuros. Por agora, tente isso nos seus próprios serviços e experimente brincar com outros exportadores e receptores no repositório otel-contrib.
Source:
https://dzone.com/articles/tracing-with-opentelemetry-and-jaeger