Introdução

Todo desenvolvedor em algum momento de sua carreira já passou raiva com mudança de alguma tecnologia. Sempre que isso acontece, é necessário fazer uma série de modificações em um, ou vários, sistemas que tomamos conta e é exatamente isso que torna todo o processo de mudança doloroso. Atualmente, a empresa que estou trabalhando, decidiu por mudar a ferramenta de APM que utilizamos para centralizar o trace de todos os sistemas em um lugar só. O problema que essa decisão gera é a mudança de todos os pontos do código que contém a geração de trace utilizando a biblioteca de uma ferramenta para utilizar a biblioteca de outra ferramenta, ou seja, sempre que houver esse tipo de mudança todo esse trabalho precisará ser refeito.

Mas qual é o problema de verdade? 🤔

Nós, programadores, somos contratados para resolver problemas, porém, sempre que precisamos fazer algum tipo de “trabalho braçal”, ou seja, ficar realizando alguma tarefa repetitiva, nos sentimos angustiados e desmotivados, e é exatamente esse tipo de trabalho que é gerado quando realizamos alterações de ferramentas em algum nível.

E não tem nada que possamos fazer nessa situação a não ser fazer esse trabalho chato? 🥹

Ainda bem que a maioria dos desenvolvedores pensam igual, e com isso, criaram uma ferramenta genérica capaz de centralizar a criação e envio de métricas, traces e logs, assim, podendo ser enviado para vários backends diferentes bastando mudar algumas configurações. Essa ferramenta “milagrosa” que será abordada nesse post é o OpenTelemetry.

🚨 Observação importante: Como nem tudo são flores, o trabalho chato precisará ser feito pelo menos uma vez, mudando da biblioteca atual para o OpenTelemetry.

Explorando a Solução

Aplicação de Exemplo

Para mostrar como tudo funciona, vamos utilizar uma API simples desenvolvida utilizando o framework web FastAPI:

1
pip install fastapi uvicorn

app.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from random import randint
from typing import Optional
import logging

from fastapi import FastAPI


logging.basicConfig(level=logging.INFO)

app = FastAPI()
logger = logging.getLogger(__name__)


def roll():
    return randint(1, 6)


@app.get("/rolldice")
async def roll_dice(player: Optional[str] = "Anonymous"):
    # Joga o dado e recebe o resultado aleatório
    result = roll()
    logger.warn("%s is rolling the dice: %d", player, result)

    return {"result": result}

Para testar a aplicação, basta executar:

1
2
3
4
5
6
7
# inicia a aplicação
uvicorn app:app --port 8080

# realiza uma requisição usando o player "Anônimo"
curl -s http://localhost:8080/rolldice
# realiza uma requisição usando o player "Lukerops"
curl -s http://localhost:8080/rolldice?player=Lukerops

Instrumentando o OpenTelemetry

Existem duas formas de se instrumentar o OpenTelemetry no python, mas vou focar apenas na instrumentação automática por fins de simplicidade.

Instalação

A instalação se divide em alguns pacotes diferentes, mas, para o nosso caso, em que o foco está na simplicidade, podemos resumir apenas no pacote opentelemetry-distro. Ele irá instalar todas as bibliotecas e ferramentas necessárias para fazer a instrumentação automática.

1
pip install opentelemetry-distro

Após fazer a instalação, vamos fazer o bootstrap, assim, o opentelemetry identificará as bibliotecas que utilizamos e instalará tudo que será necessário para instrumenta-las corretamente.

1
opentelemetry-bootstrap -a install

É possível ver quais as bibliotecas extras que serão instaladas através do comando opentelemetry-bootstrap -a requirements

Executando a Aplicação

Agora que temos a ferramenta instalada, o comando para iniciar a aplicação é alterado para que o opentelemetry consiga carregar dinamicamente algumas coisas e já entregar alguns traces automáticamente, ficando:

1
2
3
4
opentelemetry-instrument \
    --traces_exporter console \
    --service_name otl-example \
        uvicorn app:app --port 8080

Os parâmetros também podem ser configurados através de variáveis de ambiente, o que torna mais simples o comando, ficando:

1
2
3
export OTEL_TRACES_EXPORTER=console
export OTEL_SERVICE_NAME=otl-example
opentelemetry-instrument uvicorn app:app --port 8080

A instrumentação automática é desabilitada sempre que usamos o --reload no uvicorn.

Como o foco do post é no APM, apenas as configurações para habilita-lo estão sendo mostras, mas, se necessário, é preciso definir outros parâmetros para habilitar o envio de métricas e logs.

Após a execução da aplicação usando o comando novo algumas informações novas serão mostradas no console em formato json. Essas informações são alguns traces que são capturados automaticamente pela ferramenta sem precisarmos fazer nenhum tipo de definição.

Adicionando Traces Manuais

Em alguns momentos é interessante criarmos alguns traces manuais para medir o tempo gasto em alguns pontos da nossa aplicação (ex: tempo gasto para obter um dado do banco de dados ou o tempo gasto em alguma requisição externa) ou salvar alguma informação importante do momento.

Para fazer essa adição, primeiro precisamos obter um tracer (parecido com o que fazemos para obter um logger):

1
2
from opentelemetry import trace
tracer = trace.get_tracer("diceroller.tracer")

Para criar um span nesse tracer é necessário criar um contexto novo contendo o código que será monitorado, exemplo:

1
2
with tracer.start_as_current_span("example") as example_span:
    # código que obtem algum dado no banco de dados

As vezes salvar algumas informações presentes somente naquele momento do código pode ser interessante para ajudar a identificar o que estava sendo executado no momento que o trace foi gerado. Essas informações podem ser adicionas ao span de duas formas:

1
2
3
4
5
6
7
8
# adicionado uma a uma
example_span.set_attribute("roll.value", result)

# adicionando multiplas informações de uma só vez
example_span.set_attributes({
    "roll.value": result,
    "player": player,
})

Código Python Final

Após as modificações descritas anteriormente, nosso código ficou da seguinte forma:

app.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from random import randint
from typing import Optional
import logging

from fastapi import FastAPI
from opentelemetry import trace


logging.basicConfig(level=logging.INFO)

app = FastAPI()
logger = logging.getLogger(__name__)
tracer = trace.get_tracer("diceroller.tracer")


def roll():
    return randint(1, 6)


@app.get("/rolldice")
async def roll_dice(player: Optional[str] = "Anonymous"):
    with tracer.start_as_current_span("example") as example_span:
        # Joga o dado e recebe o resultado aleatório
        result = roll()
        example_span.set_attributes({
            "roll.value": result,
            "player": player,
        })

    logger.warn("%s is rolling the dice: %d", player, result)

    return {"result": result}

Enviando as informações coletadas para o Servidor de APM

Agora que temos nosso código escrito utilizando o OpenTelemetry precisamos apenas configurar o backend que será utilizado para salvar essas informações. Para fazer isso vamos definir o endpoint do coletor de dados OpenTelemetry e mudar o exporter de trace do console para o otlp (formato utilizado pelo coletor de dados OpenTelemetry). Podemos fazer isso adicionando alguns parâmetros ao opentelemetry-instrument ou atráves de variáveis de ambiente, como mostrado a seguir:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
opentelemetry-instrument \
    --exporter_otlp_endpoint 'http://<dominio do coletor>:4317' \
    --traces_exporter otlp \
    --service_name otl-example \
        uvicorn app:app --port 8080

export OTEL_EXPORTER_OTLP_ENDPOINT='http://<dominio do coletor>:4317'
export OTEL_TRACES_EXPORTER=otlp
export OTEL_SERVICE_NAME=otl-example
opentelemetry-instrument uvicorn app:app --port 8080

Caso o exporter otlp endpoint não seja definido, é utilizado o valor padrão http://localhost:4317/

É possível enviar as informações para o coletor e o console ao mesmo tempo. Para fazer isso basta definir o exporter como console,otlp.

Colocando tudo para rodar

Para mostrar tudo que foi ensinado nesse post funcionando, vamos utilizar o Jaeger como coletor dos dados e observar os dados chegando nele. Para fazer isso vamos executa-lo utilizando o docker:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# executa o jaeger em background
docker run -d --rm --name jaeger \
    -p 16686:16686 `# porta da interface web` \
    -p 4317:4317 `# porta do coletor OpenTelemetry` \
    jaegertracing/all-in-one:1.52

# inicia nossa aplicação utilizando o endpoint padrão
export OTEL_TRACES_EXPORTER=console,otlp
export OTEL_SERVICE_NAME=otl-example
opentelemetry-instrument uvicorn app:app --port 8080

Após a aplicação iniciar e fazermos algumas requisições, podemos ver os traces chegando no jaeger através da interface que pode ser acessada pelo link http://localhost:16686/.

Conclusão

As vezes a mudança de ferramenta é necessária, seja para reduzir custos, adicionar funcionalidades ou para padronizar em uma ferramenta só, mas, nem sempre essa mudança precisa ser sinônimo de algo ruim. Como vimos nesse post, é possível utilizar de boas ferramentas para das suporte a outras, e assim, criar um ecosistema sustentável em que não precisamos ficar recriando a roda o tempo todo.