Você está rastreando um bug durante a produção. Você olha através dos logs. A única coisa que você precisa não está lá… Beco sem saída. Alguns anos atrás, eu estava acompanhando um problema de produção com um servidor que acionou uma solicitação para uma leitura de banco de dados devido a falhas de cache . Isso disparou nosso custo devido ao alto volume de leitura. Infelizmente, não havia como saber o que desencadeou isso, pois não havia registro de falhas de cache.
O outro lado disso é que adicionar o registro aqui ajudaria a rastrear a falta de cache, mas teria disparado nossos custos de ingestão de registro. O armazenamento de log é incrivelmente caro. Isso é algo que eu me deparo muito; um desenvolvedor que precisa adicionar um log à produção precisa passar por PR, aprovação, mesclagem, CI/CD etc., apenas para descobrir que outro log é necessário. Em nossa equipe, apelidamos isso de ciclo da morte CI/CD. Adoramos o processo básico, mas ele não foi projetado para ser usado como um “depurador de pobre”.
É uma dor que todos nós sentimos. A observabilidade do desenvolvedor é o nome de uma família de ferramentas projetadas para resolver esse problema. As ferramentas de observabilidade existentes foram projetadas com o DevOps em mente, a observabilidade do desenvolvedor muda a observabilidade para o ciclo de vida de desenvolvimento de software. Neste artigo, explicarei o que são, como são construídos, o que oferecem e como escolher aquele que atende às suas necessidades (sem endossar ferramentas específicas).
Um novo mundo complexo
O mundo do software de hoje é bem diferente daquele que tínhamos quando comecei a programar. Quando eu era um jovem programador, monitorar seu servidor de produção significava caminhar e chutar o hardware para ouvir o disco rígido girar. “Sim, está funcionando.”
Isso obviamente não é mais sustentável. Nossa escala moderna simplesmente não permite isso. Agora temos a equipe de DevOps protegendo a produção – isso é bom. Desde que adotamos o DevOps, contratamos SREs, implementamos CI/CD, etc., a produção se tornou muito mais estável e as startups escalaram com muito mais eficiência do que nunca.
Mas os bugs de produção ainda estão aqui. O progresso que fizemos como indústria tem uma desvantagem significativa, pois esses bugs de produção são MUITO mais difíceis de rastrear do que eram no passado. A escala dificulta. Nuvem, orquestração de contêineres, sem servidor , etc., nos permite implantar centenas ou milhares de instâncias imediatamente. Isso permite capacidade de resposta, confiabilidade e flexibilidade como nunca antes, mas também apresenta problemas de simultaneidade como nunca antes. A corrupção de dados em escala devido a um bug ou configuração incorreta é desenfreada. Apenas uma parte de nossa produção está acessível para nós – isso torna a depuração ainda mais difícil.
Muito poucos desenvolvedores usam ferramentas de observabilidade e monitoramento. A maioria dos fornecedores de ferramentas de observabilidade cria seus produtos com o DevOps em mente e não visa engenheiros. Faz sentido: o DevOps lida com a produção. Mas a nova geração de ferramentas apresenta uma opção: e se você pudesse depurar problemas diretamente na produção?
E se você pudesse fazer isso sem nenhum risco?
O que é observabilidade do desenvolvedor?
A observabilidade do desenvolvedor é um novo pilar da observabilidade adaptado para as necessidades dos desenvolvedores. Ao contrário das soluções típicas de observabilidade, ele é direcionado diretamente aos desenvolvedores e não ao DevOps. Como tal, ele fornece uma conexão direta entre o código-fonte e a produção observável.
A observabilidade do desenvolvedor inclui as duas propriedades distintas a seguir:
- Com base nas solicitações do usuário
- Funciona com código fonte
As ferramentas típicas de observabilidade colocam a instrumentação em todo o aplicativo – por exemplo, em cada ponto de entrada de serviço da Web e geralmente mais profundo. Essas ferramentas usam a instrumentação para amostrar dados e enviar informações. Como tal, eles enviam dados de observabilidade para seu servidor de gerenciamento.
As ferramentas de observabilidade do desenvolvedor não fazem nada por padrão. Um desenvolvedor precisa adicionar explicitamente observabilidade a uma linha (ou linhas) de arquivo de origem específica. Funciona em modo “puxar”.
Uma boa analogia seria que a observabilidade do desenvolvedor é como um depurador, enquanto as ferramentas de observabilidade atuais são como um criador de perfil. Quando você executa com um depurador, ele não faz muito até que você adicione pontos de interrupção para extrair informações. Um criador de perfil constantemente obtém informações durante a execução. Ambos são muito úteis e atendem a diferentes casos de uso.
Registro sob demanda
O registro em produção pode ser inestimável no rastreamento de problemas relacionados a threads. Por causa da escala de produção, alguns problemas de simultaneidade só aparecem lá.
Infelizmente, o registro de produção extensivo não é algo que podemos fazer de forma realista para a maioria dos casos de uso. Se adicionarmos um log em cada entrada/saída de método, nossos logs irão explodir. Eles se tornarão ilegíveis, dispararão nossos custos de armazenamento e diminuirão o desempenho do servidor. Adicionar alguns logs a um servidor específico pode fazer uma grande diferença no processo de depuração sem um impacto perceptível nos logs em geral.
É aqui que as ferramentas de observabilidade do desenvolvedor podem intervir. Um recurso típico dessas ferramentas é a capacidade de adicionar um novo log à produção sem alterar o código. Um desenvolvedor pode adicionar um logon nos pontos de entrada e saída do método diretamente em seu IDE. Como os loggers normalmente incluem os detalhes do encadeamento, podemos inspecionar o log para ver possíveis condições de corrida.
Nos bastidores, as ferramentas de observabilidade do desenvolvedor contam com um serviço de agente instalado em seu servidor de produção. Ele adiciona o log para você como se você mesmo o tivesse escrito no código. Para manter a produção segregada e segura, essas ferramentas se comunicam externamente com um servidor de gerenciamento. Seu IDE se conecta diretamente a esse servidor e não tem acesso direto à produção. Como a produção está envolvida, essas ferramentas incluem recursos de segurança, como sandboxing, para evitar que um método chamado de um log mude de estado. Por exemplo, eu posso adicionar um log como: “User {user.getUserId()} reached myMethod”
.
Algumas ferramentas verificam se a invocação do método é de fato somente leitura.
Podemos então revisar o log para verificar se diferentes threads acessam o estado. Isso funciona razoavelmente bem para casos simples, mas ainda há vários desafios com os quais precisamos lidar:
- Impacto no desempenho de novos logs – Algumas ferramentas fornecem a capacidade de solicitações de sandboxing, que pausarão os logs se consumirem muita CPU.
- Problemas que podem não ser reproduzíveis em um único contêiner/servidor – eu ignorei o fato de que, quando você adiciona um novo log, pode direcionar um agente específico (processo de aplicativo). Em vez disso, muitas vezes podemos segmentar tags e o log será aplicado instantaneamente a todas as tags aplicáveis.
- Ruído em nossos logs – Algumas ferramentas podem logar no registrador de aplicativos. Isso significa que os logs aparecem como se você os chamasse em código. Com a tubulação, podemos redirecionar os logs adicionados para a interface do usuário do IDE e remover todo o ruído (e custo) do log real.
Visão profunda da produção
Os logs são ótimos quando temos uma noção geral do problema que estamos enfrentando. Mas há muitos casos, como falhas de transação, que podem ser mais amorfos. Precisamos ver mais detalhes, como pilha de chamadas e valores de variáveis, para nos orientarmos. O problema é que não sabemos necessariamente o que estamos procurando, mas podemos saber quando o vemos.
Ao trabalhar localmente, adicionamos um ponto de interrupção e observamos as variáveis locais e os quadros de pilha. Se esta for uma falha ocasional, podemos usar um ponto de interrupção condicional para obter as informações em caso de falha. Você pode fazer algo semelhante com uma ferramenta de observabilidade do desenvolvedor. A única diferença é que você não pode quebrar, pois passar por cima da produção não é prático. Você não pode “segurar” o encadeamento do servidor de produção.
Algumas ferramentas se referem a esse recurso como instantâneos, outras chamam de captura ou pontos de interrupção sem quebra.
Um aplicativo Spring Boot de produção pode ocasionalmente obter reversões de transação. Usando nossa ferramenta de observabilidade do desenvolvedor, colocamos um snapshot condicional em uma classe interna do Spring ( TransactionAspectSupport
). Em seguida, recebemos o rastreamento de pilha completo e todos os valores de variáveis para a transação com falha. Ao revisar o estado, pudemos entender a causa raiz das transações com falha.
Instantâneos condicionais (como usamos neste exemplo) são muito parecidos com pontos de interrupção condicionais. Podemos usar uma condição booleana referenciando o código-fonte para restringir o escopo para que recebamos apenas o instantâneo aplicável. As condições podem ser qualquer coisa; por exemplo, “user.getId() == 5999965”. Observe que neste caso eu usei Java para definir a condição mas normalmente seria na linguagem do ambiente atual, você também tem acesso a variáveis, métodos/funções no escopo do Snapshot.
Uma das coisas mais difíceis de depurar são os bugs desagradáveis que acontecem uma vez em uma lua azul. Não podemos reproduzi-los localmente e recebemos uma pilha “estranha” do servidor. Sabemos onde está o problema, mas não podemos imaginar o que o causaria!
Nesses casos, podemos colocar um instantâneo condicional na linha aplicável, mas aumentar seu tempo de expiração. A maioria das ferramentas neste campo expira implicitamente as ações para reduzir a sobrecarga, embora isso às vezes seja configurável. Então podemos voltar no dia seguinte ou uma semana depois, quando o problema for reproduzido para nós.
Neste ponto, teremos a pilha e os valores de todas as variáveis aplicáveis na pilha. Esta é uma dádiva de Deus para esta classe desagradável de bugs ocultos.
Alguém está usando esse código?
Mesmo em uma base de código de tamanho moderado, pode não ser óbvio se o código implantado na produção realmente é chamado na produção. Infelizmente, a resposta geralmente é um encolher de ombros. Podemos usar o recurso “localizar uso” do IDE, mas ele fornece apenas um pouco da história. O código pode ser “alcançável” em um nível técnico, mas nenhum usuário realmente alcançará essa linha.
Alguns anos atrás, tínhamos um recurso em um aplicativo e depois o removemos da interface do usuário. Não tínhamos como saber se as pessoas ativaram essa configuração na interface do usuário e simplesmente a deixaram. Portanto, o código de back-end para suportar esse “recurso antigo” ainda estava por aí.
Isso também significava que tínhamos testes de unidade cobrindo isso para aumentar a cobertura e, com cada refatoração, tínhamos que garantir que funcionasse. Um log simples mostrava que ainda era usado. Mas queríamos ter uma noção melhor dos números.
Um contador é incrementado toda vez que a linha do contador é alcançada e pode ser adicionada como um instantâneo ou log. Pode ser condicional assim como as outras ações, então podemos contar o número de vezes que pessoas de um país específico alcançaram uma linha de código específica. Isso é extremamente útil quando precisamos tomar decisões de arquitetura sobre o código.
Podemos aproveitar o princípio de Pareto (a regra 80-20) para focar nossas otimizações no código que é realmente usado para crescimento e melhoria futuros. Usando contadores, podemos descobrir a área do código que é realmente usada.
Identifique problemas de desempenho
Um erro muito comum são as consultas N+1 em ferramentas ORM (object Relationship Mapping). Isso acontece quando uma única operação que deveria ter buscado todo o conjunto de resultados acaba acionando uma nova consulta para cada linha.
Você pode ignorar esses erros, pois o banco de dados recebe muitas consultas e geralmente é difícil associar o código ao SQL resultante. Localmente, isso pode passar despercebido sem causar problemas, mas em conjuntos de dados de produção maiores, o impacto no desempenho pode ser significativo. Infelizmente, em produção o volume de consultas é tão grande que é ainda mais difícil perceber o conjunto específico de pequenas consultas que causam isso. Como cada consulta individual parecerá ter um bom desempenho, até mesmo um DBA experiente pode perder isso.
Uma ferramenta típica de observabilidade provavelmente nos apontará para o problema geral. Por exemplo, suponha que o serviço da Web X esteja funcionando mal. Um único serviço da Web pode acionar muitas operações nesse contêiner e possivelmente por meio de microsserviços. Como podemos restringir isso?
Ao depurar o código localmente, frequentemente salvo a hora do relógio atual e algumas linhas abaixo que imprimem a diferença entre a hora atual e a hora original. Isso fornece medições precisas de baixo nível sobre o desempenho de um bloco de código. Esse é um padrão comum que às vezes é incorporado às APIs de linguagem . O nome tictoc refere-se ao som do relógio de parede e representa as duas chamadas para ele: o tic e o toc. Podemos adicionar esse log à nossa produção, mas nossos logs de produção serão preenchidos com impressões difíceis de ler e quantificar.
As métricas nos permitem medir o desempenho de um bloco de código ao longo do tempo. Podemos marcar uma região no IDE e adicionar uma medida que funcione como as outras ações que discutimos. Convenientemente, podemos usar métricas para restringir o escopo. Por exemplo, se um usuário específico estiver enfrentando um problema de desempenho, podemos configurar uma métrica condicional em seu ID de usuário para ver as linhas de código específicas que estão com falha.
Graças a isso, notamos uma configuração incorreta em nosso comportamento de transação Spring Boot que acionou consultas redundantes. Como o código parecia eficiente na superfície e era realmente eficiente quando alcançado de um caminho diferente, nunca suspeitamos de um problema!
Rastreando uma vulnerabilidade de dia zero
A recente vulnerabilidade do Log4j foi difícil. Era fácil explorar antes que um patch estivesse disponível e era muito difícil testar. Muitos desenvolvedores não tinham ideia de que usavam o Log4j porque era uma dependência de código de terceiros que poderia ser vulnerável!
Não sou especialista em segurança, mas a abrangência do problema era imensa. O Log4J está em muito mais lugares do que as pessoas imaginavam, as empresas nem sabiam que usavam Java e eram vulneráveis. A gravidade ficou imediatamente clara para mim, pois o bug permitia a fácil execução remota de código. No mesmo dia em que o problema se tornou público, adicionamos um instantâneo ao arquivo Log4j vulnerável em nosso projeto. Isso não resolveu o problema nem impediu um hacker malicioso. Mas se alguém tivesse explorado essa vulnerabilidade, teríamos obtido informações sobre o ataque.
Mais tarde, usei essa abordagem também no exploit Spring4Shell . Nesse caso, eu poderia usar o exploit para verificar se nenhum de nossos servidores estava vulnerável a esse ataque específico.
Implicações de segurança da observabilidade do desenvolvedor
O rastreamento de zero dias não é tão impactante se a própria ferramenta for vulnerável ou expor nossos servidores de produção a riscos. Todas as ferramentas neste campo (que eu conheço) não expõem a produção de forma alguma. Um agente é adicionado aos aplicativos de produção e se comunica com o servidor do fornecedor.
Os desenvolvedores têm acesso apenas ao servidor do fornecedor e não à produção. Desta forma o DevOps ainda mantém 100% de controle e isola a produção sem risco. Existem muitos outros recursos orientados à segurança que as ferramentas podem incorporar, como redução de PII, fixação de certificados, sandboxing, etc., mas as listas de bloqueio são as mais importantes!
Alguns de vocês podem ter lido este artigo pensando em depuração remota. Isso tem muitas desvantagens / problemas, mas um brilha acima de tudo. Imagine um desenvolvedor em sua empresa colocando um ponto de interrupção no código de autenticação do usuário e desviando as credenciais do usuário.
60% das violações de segurança da empresa vêm de dentro da organização. Engenheiros descontentes poderiam usar ferramentas como essas para seus próprios fins. Isso também pode violar os regulamentos de privacidade, como o GDPR, expondo efetivamente informações privadas.
Blockists são a solução para esse problema. Neles, você pode especificar os arquivos/classes que devem ser excluídos das ações. Um engenheiro não pode adicionar uma ação a esses arquivos e é efetivamente bloqueado a partir daí.
Ao configurar o ambiente do servidor, essas áreas devem ser mapeadas para evitar intenções maliciosas.
Conclusão
As ferramentas de observabilidade do desenvolvedor são um depurador projetado para execução de código em produção. Eles são uma mudança sísmica na desconstrução dos silos DevOps/Developer, pois nos permitem examinar a produção sem os riscos associados.
Existem muitos casos de uso para os quais podemos aplicar o poder dessas ferramentas. Neste artigo, eu mal arranhei a superfície do que é possível. A criatividade dos desenvolvedores que usam essas ferramentas nunca deixa de me surpreender. Eu encorajo todos vocês a passarem pela lista de recursos e revisarem alguns dos principais fornecedores da área. Algumas dessas ferramentas são totalmente gratuitas em escalas menores, para que você possa realizar uma investigação/prova de conceito sem o incômodo de compras.