Headers HTTP para rastro distribuído (observabilidade) - Parte 2: OpenTracing e OpenTelemetry

🇬🇧 🇧🇷

Esta série de posts investiga os headers HTTP usados para apoiar o rastro distribuído (tracing), que é um dos pilares da observabilidade (composta também por logs e métricas). Embora foquemos na investigação sobre esses headers, estes posts servem também como uma introdução sobre rastro distribuído e como apresentação de algumas alternativas tecnológicas para sua implementação. Como valor agregado extra, destrinchamos e interpretamos a documentação de algumas dessas alternativas; documentação essa, francamente, não tão fácil de assimilar.

Retomando o assunto

O rastro distribuído (tracing) diz respeito aos processos, padrões e ferramentas que possibilitam que a equipe de desenvolvimento compreenda as requisições feitas a partir da invocação a um determinado serviço, dando a possibilidade de correlacionar erros ocorridos em diferentes serviços. Exemplo: entender que o erro interno no serviço A foi devido a um determinado problema no serviço B (sendo o serviço B invocado pelo serviço A).

Na solução caseira de rastro distribuído de minha equipe, usamos alguns headers na invocação de um serviço para o outro: o X-Request-ID, que identifica a transação distribuída em execução; o CLIENT_APPLICATION_NAME, que transporta o nome do serviço cliente que realizou a invocação; e o CLIENT_CHAIN, com a lista dos nomes dos serviços que fizeram parte da cadeia de invocações até o momento.

E aí surge a indagação: o que fizemos seria muito diferente do preconizado pelas soluções de mercado? Diante disso, nossa questão é: poderíamos ter uma solução caseira utilizando headers de apoio a rastro distribuído que fossem compatíveis com soluções de mercado? Ou seja, não estou muito interessado em adotar uma biblioteca ou framework específico, mas acharia interessante que os headers utilizados na solução caseira fossem padronizados. E uma alternativa que me pareceu promissora em relação a “padronizar o rastro distribuído” foi o OpenTracing, que vamos analisar agora.

Ah, se essa recontextualização não lhe foi o suficiente, considere rever as seções iniciais de nosso post anterior (i.e., as seções antes de iniciarmos a descrição sobre o Spring Cloud Sleuth).

Introduzindo o OpenTracing

Na documentação do Spring Cloud Sleuth [1], encontramos um parágrafo sobre o OpenTracing, no qual se declara que “The OpenTracing initiative is an effort to standardize the vocabulary and concepts of modern tracing for multiple languages and platforms”.

Note que a padronização proposta pelo OpenTracing é sobre “vocabulário e conceitos”. Ou seja, para mim que imaginava o OpenTracing como algum tipo de padrão, fiquei bem surpreso ao descobrir que não há especificação de headers de propagação de contexto para rastro distribuído no OpenTracing!

Mas vamos ao OpenTracing por ele mesmo [2]:

What is Distributed Tracing?

Distributed tracing, also called distributed request tracing, is a method used to profile and monitor applications, especially those built using a microservices architecture. Distributed tracing helps pinpoint where failures occur and what causes poor performance.

What is OpenTracing?

OpenTracing is not a download or a program. OpenTracing is not a standard. OpenTracing is comprised of an API specification, frameworks and libraries that have implemented the specification, and documentation for the project. OpenTracing allows developers to add instrumentation to their application code using APIs that do not lock them into any one particular product or vendor.

Apesar dessa descrição do OpenTracing focar na especificação de APIs, na sequência a documentação retoma o aspecto conceitual afirmando que o OpenTracing é composto da “OpenTracing Semantic Specification” e de “Semantic Conventions”.

No cenário, em que eu me encontrava, em que não necessariamente desejava adotar alguma biblioteca de mercado para rastro distribuído, me pareceu interessante investigar esses conceitos do OpenTracing. Assim, poderíamos ter uma implementação com termos minimamente compatíveis (em termos de semântica) com alguma referência mais bem estabelecida (digo “referência” já que não podemos chamar o OpenTracing de padrão). Então, vamos lá cavar mais.

The OpenTracing Semantic Specification

A especificação semântica do OpenTracing [3] contém:

O principal aqui é que temos então a definição de termos como “trace” e “span”, que são os mesmos termos usados pelo Spring Cloud Sleuth. Lembrando: “trace” (rastro) diz respeito a um conjunto de processamentos ocorridos em diferentes serviços em uma transação distribuída (i.e., uma sequência de invocações entre serviços); já “span” diz respeito ao processamento ocorrido em um serviço específico durante a execução de uma transação distribuída.

Outros termos também definidos nessa especificação e usados no Sleuth é o de “SpanContext” (o que contém o trace id e o span id) e o de “baggage” (which are just key:value pairs that cross process boundaries). Além da bagagem, que é transferida entre serviços, cada span também possui em seu contexto um conjunto de chaves-valores denominados de “tags”.

Até aqui temos o reforço de que pode ser boa ideia utilizar certos termos (como trace, span, context, baggage e tags) em soluções caseiras, uma vez que não são termos específicos de apenas um framework.

Semantic Conventions

Nas convenções semânticas do OpenTracing [4] encontramos que:

Aqui há uma sugestão de um conjunto de valores para serem usados como chaves das tags que formam o contexto do span. Exemplos: component, db.instance, db.statement, db.user, error, http.method, http.status_code, http.url. Além disso, há uma sugestão de campos que devem aparecer em cada log, que seriam: error.kind, error.object, event, message, stack.

Embora o título “semantic conventions” parecesse promissor, achei essas convenções menos úteis para meu propósito. Vamos pular essa parte.

Carrier

Para nós que estamos preocupados com a concretização da propagação de contexto por meio de headers HTTP, um conceito importante do OpenTracing é o de carrier (em português seria “portador”) [5]:

A Carrier is an interface or data structure that’s useful for inter-process communication (IPC); that is, the Carrier is something that “carries” the tracing state from one process to another.

What the OpenTracing implementations choose to store in these Carriers is not formally defined by the OpenTracing specification, …

Interoperability of OpenTracing implementations across process boundaries: There is no expectation that different OpenTracing implementations Inject and Extract SpanContexts in compatible ways. Though OpenTracing is agnostic about the tracing implementation across an entire distributed system, for successful inter-process handoff it’s essential that the processes on both sides of a propagation use the same tracing implementation.

The precise representation of the “Carrier formats” may vary from platform to platform.

Daí que concluímos que OpenTracing não padroniza headers HTTP para a propagação de contexto de rastro distribuído.

Spans e traces no OpenTracing

Vejamos a visão do OpenTracing sobre esses dois importantes conceitos [6]:

The OpenTracing model of computation specifies two primary object types, Spans and Traces, but does not specify identifiers for these objects.

Before discussing changes to the OpenTracing specification, it’s worth reviewing several popular wire protocols which contain these trace identifiers.

Ou seja, a documentação do OpenTracing cita os mesmos padrões de headers HTTP para rastro distribuído que são suportados pelo Spring Cloud Sleuth. Além disso, apresenta os termos trace e span de forma compatível ao utilizado no Spring.

Relação entre o Spring Cloud Sleuth e o OpenTracing

Em resumo, o Spring Cloud Sleuth [1] promete que:

We will keep Spring Cloud Sleuth conceptually compatible with this effort

Ou seja, o Sleuth adota de forma consistente a terminologia promovida pelo OpenTracing, principalmente no que diz respeito aos termos trace e span.

OpenTelemetry

The OpenTracing project is archived. Learn more. Migrate to OpenTelemetry today!

Pois é, segundo a documentação do OpenTelemetry [7], acontece que:

OpenTelemetry is a Cloud Native Computing Foundation (CNCF) project that is the result of a merger between two prior projects, OpenTracing and OpenCensus. Both of these projects were created to solve the same problem: the lack of a standard for how to instrument code and send telemetry data to an Observability backend.

Ou seja, o OpenTracing agora é o OpenTelemetry, mas que considera não apenas a questão do rastro distribuído, mas também os outros pilares da observabilidade (métricas e logs). O projeto provê especificações, protocolos, convenções semânticas, bibliotecas e ferramentas para “generation, collection, management, and export of telemetry”. E aí esses dados expostos de telemetria podem ser consumidos por outras ferramentas, como o Jaeger (rastro distribuído) e o Prometheus (métricas), para a visualização por humanos.

Mas parece que em relação aos conceitos, as coisas mudaram um tanto. Segundo a página de migração do OpenTracing para o OpenTelemetry [8], parece que termos do Open Tracing como “tags” e “baggage” ficaram defasados ou possuem uma aplicação diferenciada no OpenTelemetry, uma vez que OpenTelemetry has an improved model for instrumenting software. Por exemplo, os dados que contextualizam um span não são mais chamados de tags (como era no OpenTracing), mas de “attributes”.

Por outro lado, as opções de headers HTTP para propagação de contexto continuam sendo as mesmas (B3 e W3C) [9].

Ou seja, por um lado a padronização terminológica ambicionada pelo OpenTracing não vingou, sendo oficialmente sobrescrita pelo modelo evoluído do OpenTracing. Contudo, por outro lado, essa terminologia do OpenTracning continua lá no Spring Sleuth.

Ah, o próprio OpenTelemetry oferece também bibliotecas para instrumentação personalizada de código. Assim o programador pode definir diferentes trechos de código como correspondentes a diferentes spans [10], além de atribuir “attributes” a esses spans. Fazendo uma mescla de exemplos da documentação [11], teríamos algo como:

import io.opentelemetry.api.trace.Span;

// ...

    Span span = tracer.spanBuilder("myBusinessWork").startSpan();

    span.setAttribute("http.method", "GET"); // attributes, não tags!
    span.setAttribute("http.url", url.toString());

    // Make the span the current span
    try (Scope scope = span.makeCurrent()) {

        // trecho de código que faz alguma coisa a ser instrumentada
        // exemplos: processamento de uma requisição http,
        // mesmo uma chamada http a outro serviço
        // ou algum processamento mais pesado, como manipulação de arquivo.
      
    } finally {
      span.end();
    }

Conclusão

Em essência OpenTracing provê um modelo de rastro distribuído. I.e., define a terminologia dessa atividade, especialmente sobre os termos “trace” e “span”. Essa terminologia é abraçada pelo Spring Cloud Sleuth, de forma que podemos dizer que o Sleuth é compatível com o OpenTracing.

No mais, em particular, o OpenTracing não define os nomes dos headers HTTP que devem transferir de um serviço para o outro as informações relativas ao rastro distribuído. A documentação do OpenTracing acaba citando os padrões da W3C e o B3, que são as opções disponíveis no Spring Sleuth.

Contudo, o OpenTracing está agora obsoleto e foi absorvido pelo projeto OpenTelemetry, que possui um modelo evoluído, de forma a não necessariamente se manter a compatibilidade com a terminologia pregada pelo OpenTracing. Esse desfecho nos deixa mais a vontade para adotarmos em soluções caseiras os termos que acharmos mais convenientes, independentemente dos termos usados em eventuais padrões, uma vez que essa parece ser uma área ainda não tão consolidada.

Na próxima parte dessa série vamos pontuar mais algumas referências que podem nortear as escolhas de nomes de termos e headers de HTTP em soluções caseiras de rastro distribuído.


Referências

[1] Documentação sobre o Spring Cloud Sleuth citando por cima o OpenTracing: https://spring.io/blog/2016/02/15/distributed-tracing-with-spring-cloud-sleuth-and-spring-cloud-zipkin#the-opentracing-initiative

[2] Documentação do OpenTracing sobre “What is Distributed Tracing?”: https://opentracing.io/docs/overview/what-is-tracing/

[3] The OpenTracing Semantic Specification: https://github.com/opentracing/specification/blob/master/specification.md

[4] OpenTracing Semantic Conventions: https://github.com/opentracing/specification/blob/master/semantic_conventions.md

[5] OpenTracing - Inject, Extract, and Carriers: https://opentracing.io/docs/overview/inject-extract/

[6] OpenTracing Specification - Trace Identifiers: https://github.com/opentracing/specification/blob/master/rfc/trace_identifiers.md

[7] What is OpenTelemetry?: https://opentelemetry.io/docs/what-is-opentelemetry/

[8] Migrating from OpenTracing: https://opentelemetry.io/docs/migration/opentracing/

[9] Observability in Java: Getting Started with OpenTelemetry (em 27’04’’): https://youtu.be/C374RMCK3kY?si=DnjNsWB3A2uNQt4V

[10] Observabilidade e Open Telemetry na Prática (a partir do 22’06’’): https://youtu.be/Y0gNpjHwx2M?si=dZ82PAewrZK3D_pN

[11] Manual instrumentation for OpenTelemetry Java (seções “Create Spans” e “Span Attributes”): https://opentelemetry.io/docs/languages/java/instrumentation/