Rastreamento Com OpenTelemetry e Jaeger

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:

  1. A fonte deste é no PART11_TRACING branch.
  2. Construa todas as coisas necessárias (depois de tirar um checkout da branch):
make build

  1. 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
  1. 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:

  1. 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).
  2. 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:

YAML

 

Bem simples, isso configura dois serviços no nosso ambiente Docker:

  1. 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).
  2. jaeger: Nossa instância de Jaeger que vai receber e armazenar rastreamentos (exportados pelo otel-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.
  3. 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 o otel-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:

YAML

 

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):

YAML

 

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:

YAML

 

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.

  1. traces:

  • Receba sinais de clientes SDKs
  • Sem processamento
  • Exportar rastreios para console e Jaeger
  1. 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.

YAML

 

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:

Go

 

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:

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:

Go

 

  • 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:

Go

 

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.

Go

 

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 este SpanNameFormatter, 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:

Go

 

Agora passar uma conexão diferente é simples:

Go

 

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):

Go

 

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):

Go

 

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:
Go

 

  • datastore/topicds.go:
Go

 

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