<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Patrick Otto on Medium]]></title>
        <description><![CDATA[Stories by Patrick Otto on Medium]]></description>
        <link>https://medium.com/@patrickotto.dev?source=rss-daa0cab73ef2------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*NmtmBUKqeJY4oHmPuwOCtg.png</url>
            <title>Stories by Patrick Otto on Medium</title>
            <link>https://medium.com/@patrickotto.dev?source=rss-daa0cab73ef2------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 22 May 2026 22:21:54 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@patrickotto.dev/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Além do Código: o impacto oculto do Social Selling Index na carreira técnica]]></title>
            <link>https://medium.com/@patrickotto.dev/al%C3%A9m-do-c%C3%B3digo-o-impacto-oculto-do-social-selling-index-na-carreira-t%C3%A9cnica-08d45773984c?source=rss-daa0cab73ef2------2</link>
            <guid isPermaLink="false">https://medium.com/p/08d45773984c</guid>
            <dc:creator><![CDATA[Patrick Otto]]></dc:creator>
            <pubDate>Fri, 22 May 2026 21:50:42 GMT</pubDate>
            <atom:updated>2026-05-22T21:54:14.934Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9cvreEo1CqikN0F58mZtiA.png" /></figure><blockquote>Em um mercado cada vez mais filtrado por algoritmos, visibilidade profissional deixou de ser vaidade. Para engenheiros de software, ela passou a ser parte da estratégia de carreira.</blockquote><p>Historicamente, a reputação na engenharia de software foi construída no silêncio dos repositórios. Durante muito tempo, a senioridade de um profissional técnico parecia depender quase exclusivamente da robustez de suas entregas, da elegância de suas arquiteturas, da qualidade das decisões técnicas e da capacidade de resolver problemas complexos sob pressão. Esse modelo ajudou a consolidar um mito que ainda persiste em parte da comunidade: o de que o bom desenvolvedor deve ser invisível e de que o código, por si só, deveria ser suficiente para abrir portas. 📈</p><p>Durante um bom tempo, essa lógica pareceu razoável. Em mercados menos saturados, em ecossistemas mais locais e em um cenário em que o networking digital ainda não era tão dominante, ser tecnicamente muito bom de fato gerava reconhecimento de maneira relativamente orgânica. A reputação circulava por indicação, por histórico de entregas, por recomendações de líderes e pela capacidade de resolver problemas reais dentro das empresas.</p><p>O problema é que o mercado de tecnologia mudou.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cI_Ib-vGOoMb3REMpLFGHg.png" /><figcaption>Durante muito tempo, a engenharia romantizou a ideia de que o bom profissional técnico deveria ser invisível.</figcaption></figure><p>Hoje, a competição é global, os processos de recrutamento são mais orientados por dados, as plataformas intermediam visibilidade e os algoritmos passaram a influenciar de maneira concreta quem é encontrado, quem é lembrado e quem é abordado. Em um ambiente assim, competência continua sendo essencial, mas visibilidade também passou a ser estratégica.</p><p>Isso não significa superficialidade. Não significa transformar engenharia em performance vazia. Também não significa substituir profundidade técnica por conteúdo raso. Significa apenas reconhecer que, em um mercado filtrado por plataformas, não basta ser bom. É preciso ser encontrável.</p><p>Essa talvez seja uma das mudanças mais desconfortáveis para muitos profissionais técnicos. A comunidade de engenharia foi treinada, por muitos anos, para acreditar que marketing pessoal era quase sempre sinônimo de vaidade, e que exposição pública diminuía o rigor técnico. Só que essa leitura não resiste bem à realidade atual. Se o mercado simplesmente não consegue enxergar sua trajetória, sua experiência e sua capacidade de pensar tecnicamente, parte do seu valor profissional deixa de circular.</p><p>É nesse ponto que o Social Selling Index, o SSI do LinkedIn, ganha relevância.</p><p>Embora o nome tenha uma conotação comercial, o SSI funciona, na prática, como um indicador de maturidade da presença profissional na plataforma. Ele mede, em uma escala de zero a cem, o quanto o perfil demonstra consistência em quatro pilares: marca profissional, conexão com as pessoas certas, compartilhamento de insights e construção de relacionamentos. No universo da engenharia, isso pode ser traduzido como autoridade técnica percebida, posicionamento estratégico e capacidade de atrair oportunidades de forma orgânica.</p><p>Muitos profissionais de tecnologia ignoram essa métrica. E, de certa forma, é compreensível. O nome não ajuda. A aparência sugere algo mais voltado para vendas do que para carreira técnica. Mas o erro está justamente em parar na superfície. O SSI não é apenas sobre vender. Ele é, sobretudo, sobre ser encontrado, compreendido e considerado relevante por uma rede profissional que opera cada vez mais por sinais de autoridade.</p><p>Para tornar essa análise menos abstrata, vale usar um exemplo prático.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5FAbIOz978GFljStToljPw.png" /><figcaption>Mesmo uma pontuação que parece intermediária pode representar um posicionamento acima da média quando comparada ao comportamento real da indústria.</figcaption></figure><p>No meu caso, o gráfico recente aponta uma pontuação geral de 51. Em uma leitura apressada, esse número poderia parecer apenas mediano dentro de uma escala de zero a cem. Mas o dado realmente importante não está apenas no número absoluto. Está no posicionamento relativo que ele gera. Com essa pontuação, o perfil já aparece entre os 9% melhores da indústria de desenvolvimento de software. Ao mesmo tempo, a média geral do setor está em 31</p><p>Veja como está seu <strong>SSI do Linkedin</strong>, <a href="https://www.linkedin.com/sales/ssi">clicando aqui</a>. 🔎</p><p>Essa comparação é mais reveladora do que a pontuação isolada. Ela mostra que a maior parte dos profissionais da área ainda permanece distante de uma atuação estratégica dentro da rede. Em outras palavras, existe uma oportunidade real para quem consegue sair da invisibilidade técnica e construir presença com mais intenção.</p><p>Ao olhar os componentes do gráfico, o diagnóstico fica mais claro. Uma pontuação mais forte em marca profissional e construção de relacionamentos sugere que a base do perfil está relativamente sólida e que existe algum nível de rede ativa. Por outro lado, notas mais baixas em localização das pessoas certas e engajamento com insights revelam os gargalos da estratégia. O algoritmo, nesse caso, faz quase o papel de um avaliador silencioso. Ele mostra onde existe presença, mas também aponta onde ainda falta intencionalidade.</p><p>Essa leitura é importante porque ajuda a desmontar um equívoco comum. Muitas pessoas imaginam que autoridade digital é uma consequência espontânea da qualidade do trabalho. Na prática, não é bem assim. Autoridade percebida em plataformas depende de evidência distribuída. O mercado precisa ver sinais recorrentes de coerência, especialidade, profundidade e participação.</p><p>É por isso que o perfil importa.</p><p>Um perfil técnico mal estruturado costuma desperdiçar muito valor. Títulos genéricos, descrições superficiais, palavras-chave pouco aderentes ao mercado global, ausência de clareza sobre senioridade, setores de atuação e stack principal fazem com que o algoritmo entenda menos do que deveria. E se a plataforma entende menos, os recrutadores também enxergam menos.</p><p>No caso da área de software, isso fica ainda mais evidente quando o profissional busca mercado internacional. Um recrutador estrangeiro não parte do zero em uma busca manual. Ele utiliza filtros, palavras-chave, senioridade, sinais de atividade e contexto de perfil. Em muitos casos, o primeiro contato não nasce da candidatura do profissional, mas da capacidade do sistema de classificá-lo como relevante.</p><p>Isso muda completamente a lógica da carreira técnica.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zbkItMbG9YpPEO3L_KpjCg.png" /><figcaption>Antes de chegar ao recrutador, muitos profissionais precisam primeiro ser compreendidos pelo algoritmo.</figcaption></figure><p>Em vez de depender apenas de candidaturas ativas, o profissional passa a construir as condições para ser encontrado de forma passiva. Esse é um ponto especialmente importante para engenheiros mais experientes. Senioridade não deveria depender exclusivamente de enviar currículo em massa. Em muitos casos, ela deveria atrair conversas qualificadas.</p><p>Só que isso não acontece por acaso.</p><p>A estruturação da marca profissional é o primeiro passo. Isso significa um perfil bem escrito, headline objetiva, resumo coerente com posicionamento de carreira, experiências com narrativa clara, tecnologias relevantes descritas com contexto e uma linguagem que faça sentido tanto para humanos quanto para algoritmos. Não se trata de inflar o perfil. Trata-se de torná-lo inteligível.</p><p>No contexto internacional, isso ganha ainda mais peso. Termos locais, expressões muito informais ou descrições vagas reduzem a legibilidade do perfil para recrutadores estrangeiros. Em vez de usar apenas cargos pouco padronizados, faz mais sentido adotar nomenclaturas que dialoguem com o vocabulário global da indústria, como Senior Software Engineer, Backend Engineer, Full Stack Engineer, Distributed Systems Engineer ou Technical Lead, sempre de forma coerente com a trajetória real.</p><p>O segundo pilar do SSI envolve encontrar as pessoas certas. E esse ponto costuma ser subestimado por profissionais técnicos. Muitos enxergam a rede apenas como acúmulo de contatos, quando na prática o valor está muito mais na qualidade da malha de conexões. Conectar-se com recrutadores especializados, líderes de engenharia, CTOs, founders, staff engineers, principal engineers e profissionais de referência do setor amplia o contexto em que o perfil circula.</p><p>Essa conexão, por si só, não faz milagre. Mas ela muda o ambiente de distribuição da reputação.</p><p>Quando um profissional técnico publica bons insights, comenta com consistência ou participa de discussões relevantes dentro de uma rede qualificada, ele passa a ser visto no contexto certo. Isso é diferente de publicar para o vazio. É também diferente de falar apenas para a própria bolha imediata.</p><p>O terceiro pilar, o de insights, talvez seja o mais decisivo para quem quer sair do anonimato técnico sem perder profundidade. Muitos engenheiros continuam acreditando que produzir conteúdo é necessariamente superficial. Mas isso só é verdade quando o conteúdo é vazio. Existe um espaço enorme para publicações maduras, reflexivas, didáticas e estrategicamente técnicas.</p><p>Escrever sobre testes automatizados, arquitetura assíncrona, observabilidade, CI/CD, sistemas distribuídos, segurança, privacidade, performance, trade-offs de engenharia, liderança técnica e decisões reais de projeto não é virar influenciador genérico. É documentar inteligência aplicada. É transformar experiência em capital de reputação.</p><p>Essa transformação é importante porque ela rompe uma barreira silenciosa. Um excelente engenheiro pode construir soluções extraordinárias dentro da empresa em que atua e, ainda assim, permanecer invisível para o resto do mercado. Ao escrever, comentar e publicar com consistência, ele faz com que parte da sua capacidade deixe de ficar restrita aos sistemas internos que ajudou a construir.</p><p>Isso é especialmente poderoso quando o conteúdo é pensado de forma estratégica. Artigos no Medium, posts no LinkedIn, comentários maduros em discussões relevantes e análises próprias sobre temas da indústria criam sinais recorrentes de autoridade. O profissional deixa de ser apenas alguém que executa tecnicamente e passa a ser percebido como alguém que pensa tecnicamente.</p><p>Essa percepção tem impacto.</p><p>Ela influencia abordagens de recrutadores, qualidade das conexões, visibilidade em buscas, reconhecimento por pares e até confiança do mercado sobre seu nível de senioridade. Em muitos casos, o conteúdo não substitui a experiência, mas ajuda o mercado a reconhecer essa experiência com mais velocidade.</p><p>O quarto pilar, o de relacionamentos, é o que transforma visibilidade em rede. Porque autoridade digital sem relacionamento pode acabar se tornando apenas presença estática. E carreira, no fim, é profundamente relacional. Comentários relevantes, interações consistentes, trocas com outros profissionais, apoio a discussões qualificadas e manutenção de vínculos ao longo do tempo ajudam a construir profundidade.</p><p>Isso não precisa acontecer de maneira artificial. Muito pelo contrário. Em ambientes maduros, relacionamento digital funciona melhor quando nasce de interesse real, reciprocidade intelectual e contribuição consistente.</p><p>Ao juntar esses quatro pilares, o SSI se torna menos uma curiosidade estatística e mais um painel de diagnóstico da presença profissional. Ele não define a qualidade do engenheiro. Mas revela como essa qualidade está sendo percebida e distribuída dentro de uma das plataformas mais relevantes para recrutamento e posicionamento de carreira.</p><p>Existe ainda outro ponto importante: a volatilidade da reputação digital.</p><p>Muita gente acredita que autoridade no LinkedIn se comporta como um troféu permanente. Não funciona assim. Relevância em plataformas é dinâmica. Perfis que param de publicar, deixam de interagir, perdem clareza de posicionamento ou abandonam consistência tendem a reduzir presença. O mesmo vale para programas de reconhecimento mais visíveis, como ecossistemas de destaque editorial e selos de relevância. O mercado digital premia continuidade muito mais do que picos isolados.</p><p>Isso pode parecer duro, mas é coerente. O que a plataforma valoriza não é apenas a existência passada de um bom conteúdo, mas a recorrência da contribuição.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Og85fPkLfv7Aia7on5ucgQ.png" /><figcaption>Quando o conhecimento técnico ganha distribuição, ele deixa de ficar restrito à empresa atual e passa a circular no mercado global.</figcaption></figure><p>Para profissionais que desejam aumentar exposição internacional, esse ponto se torna ainda mais decisivo. Publicar em inglês, por exemplo, não é só uma escolha estética. É uma decisão de distribuição. Escrever apenas em português limita bastante o alcance geográfico do conteúdo. Isso não significa abandonar totalmente o idioma local, mas significa compreender que a língua também é uma camada de estratégia.</p><p>Outro ponto essencial é a qualidade semântica do perfil. O algoritmo trabalha por sinais. Se o perfil comunica com clareza tecnologias, senioridade, contexto de atuação e capacidade de entrega, ele se torna mais legível para ferramentas de busca e recrutamento. Se não comunica, perde espaço mesmo que a experiência real seja excelente.</p><p>No fundo, toda essa discussão gira em torno de uma verdade simples e desconfortável: muitos profissionais técnicos continuam escondidos. Não porque lhes falte competência. Mas porque lhes falta distribuição.</p><p>Essa distinção é central.</p><p>Ser bom continua indispensável. Mas ser visto passou a ser parte da equação profissional. O mercado não recompensa apenas quem sabe. Também recompensa quem consegue tornar o próprio conhecimento visível, confiável e encontrável.</p><p>Isso não reduz a engenharia. Isso amplia o alcance dela.</p><p>Talvez a frase que melhor resuma esse cenário seja esta: repositórios mostram o que você construiu, mas o LinkedIn mostra se o mercado consegue encontrar você.</p><p>A partir daí, a pergunta se torna inevitável. Sua carreira técnica está sendo descoberta pelo mercado ou continua presa aos bastidores da empresa onde você trabalha?</p><blockquote>Se a resposta ainda aponta para invisibilidade, talvez o problema não esteja na sua competência. Talvez esteja apenas no fato de que a sua inteligência profissional ainda não foi estruturada de forma visível o suficiente para circular.</blockquote><p><strong>E, em um mercado global, circular também é uma forma de crescer.</strong></p><p>Quando o conhecimento técnico ganha distribuição, ele deixa de ficar restrito à empresa atual e passa a circular no mercado global. 🌟</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=08d45773984c" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Sistemas que escalam de verdade seguem quatro pilares]]></title>
            <link>https://medium.com/@patrickotto.dev/sistemas-que-escalam-de-verdade-seguem-quatro-pilares-a079ae7446ef?source=rss-daa0cab73ef2------2</link>
            <guid isPermaLink="false">https://medium.com/p/a079ae7446ef</guid>
            <dc:creator><![CDATA[Patrick Otto]]></dc:creator>
            <pubDate>Mon, 18 May 2026 00:06:01 GMT</pubDate>
            <atom:updated>2026-05-18T00:15:00.712Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2Ydl3PGKhZOtrBjhcam9sQ.png" /></figure><blockquote>Testes automatizados, processos assíncronos, observabilidade e CI/CD não são apenas decisões técnicas. São a base que separa sistemas preparados para crescer de sistemas que apenas acumulam risco em silêncio.</blockquote><p>Existe uma diferença muito importante entre um sistema que cresce e um sistema que escala. Essa diferença costuma ser ignorada no início de muitos projetos, principalmente quando tudo ainda parece estar sob controle. O time consegue entregar, os usuários conseguem utilizar, os problemas ainda são pontuais e a operação parece responder bem ao ritmo do negócio.</p><p>Nesse momento, crescer parece simples.</p><p>Mais usuários entram. Mais funcionalidades são desenvolvidas. Mais integrações são adicionadas. Mais dados passam a circular pelo sistema. O produto ganha corpo, a operação aumenta e a empresa começa a depender cada vez mais da tecnologia para funcionar.</p><p>O problema é que crescimento não é a mesma coisa que escala.</p><p>Crescer significa aumentar volume. Escalar significa sustentar esse aumento sem perder estabilidade, previsibilidade e capacidade de evolução. Um sistema pode crescer bastante e, ainda assim, não estar preparado para escalar. Pode ter muitos usuários e continuar frágil. Pode ter muitas funcionalidades e ser difícil de manter. Pode ter boa infraestrutura e ainda assim sofrer com deploys arriscados, falhas silenciosas, gargalos escondidos e medo constante de mudança.</p><p>Essa é uma das armadilhas mais comuns em tecnologia.</p><p>A empresa olha para o sistema funcionando e conclui que ele está saudável. Mas funcionamento não significa maturidade. Um sistema pode estar no ar e, ao mesmo tempo, estar acumulando risco. Pode responder requisições e, ainda assim, estar cada vez mais difícil de evoluir. Pode parecer estável por fora e estar cheio de dependências frágeis por dentro.</p><p>No começo, esse risco costuma ser invisível.</p><p>O time compensa com esforço manual. Alguém sabe onde mexer. Alguém lembra quais telas precisam ser testadas. Alguém acompanha o deploy. Alguém confere os logs. Alguém resolve a fila travada. Alguém reinicia o serviço. Alguém valida a integração externa. Alguém sabe o caminho.</p><p>O problema é que escala não combina com dependência informal.</p><p>Quando um sistema depende demais de pessoas específicas, validações manuais, conhecimento não documentado e sorte operacional, ele pode até continuar funcionando, mas não está operando com maturidade. Está apenas sendo sustentado por esforço humano.</p><p>E esforço humano tem limite.</p><p>Em algum momento, a complexidade supera a capacidade do time de controlar tudo manualmente. Mais pessoas entram no projeto, mais serviços são criados, mais áreas dependem do sistema, mais integrações se tornam críticas e mais alterações precisam acontecer ao mesmo tempo. O que antes era administrável passa a se tornar imprevisível.</p><p>É nesse ponto que os fundamentos aparecem.</p><p>Sistemas que escalam de verdade não dependem apenas de bons desenvolvedores, boa infraestrutura ou ferramentas modernas. Eles dependem de uma base de engenharia capaz de sustentar crescimento com consistência.</p><p>Essa base passa por quatro pilares principais: testes automatizados, processos assíncronos, observabilidade e CI/CD.</p><p>Esses quatro elementos não resolvem todos os problemas de um sistema, mas reduzem drasticamente o risco de crescimento desorganizado. Eles criam uma estrutura para que o software possa mudar, processar, ser compreendido e ser entregue com segurança.</p><p>Sem eles, o sistema pode crescer. Mas cresce acumulando fragilidade.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*19QItPVMEq_Wiv1GbllfDQ.png" /><figcaption>Os quatro pilares não aparecem diretamente para o usuário, mas sustentam a capacidade do sistema de crescer sem perder estabilidade, controle e previsibilidade.</figcaption></figure><p><strong>O primeiro pilar são os </strong><a href="https://medium.com/@patrickotto.dev/sem-testes-automatizados-seu-sistema-não-está-pronto-para-crescer-506e9f5ddc9b"><strong>testes automatizados</strong></a><strong>.</strong></p><p>Testes automatizados são, muitas vezes, tratados como uma prática de qualidade. Essa leitura não está errada, mas é pequena demais. Em sistemas que precisam escalar, testes não existem apenas para encontrar bugs. Eles existem para criar confiança.</p><p>Confiança para alterar código. Confiança para refatorar. Confiança para corrigir problemas. Confiança para lançar novas funcionalidades. Confiança para permitir que mais pessoas trabalhem no mesmo sistema sem transformar cada mudança em risco.</p><p>Sem testes, o time passa a depender de validação manual. Isso funciona por um tempo, principalmente quando o sistema é pequeno e poucas pessoas conhecem bem o fluxo. Mas conforme o sistema cresce, a validação manual se torna insuficiente. Ninguém consegue lembrar de todos os cenários. Ninguém consegue testar tudo sempre. Ninguém consegue prever todos os impactos de uma alteração.</p><p>É nesse momento que o medo começa a substituir a fluidez.</p><p>Uma pequena mudança passa a exigir cuidado excessivo. Uma correção simples demanda múltiplas validações. Um deploy começa a gerar tensão. O time evita mexer em partes antigas do sistema. Bugs antigos reaparecem. Funcionalidades que funcionavam deixam de funcionar depois de mudanças aparentemente desconectadas.</p><p>O problema não é apenas técnico. É operacional.</p><p>Quando o time perde confiança no código, a empresa perde velocidade de evolução.</p><p>Ferramentas como Jest, Vitest, Testing Library, Cypress e Playwright ajudam no frontend, validando componentes, telas e jornadas do usuário. No backend, frameworks como xUnit, NUnit, PyTest, Mocha, Jest, PHPUnit e JUnit ajudam a validar regras de negócio, serviços, integrações e comportamentos críticos.</p><p>A diferença entre essas camadas é importante. No frontend, o foco está na experiência e na interação. No backend, o foco está na regra, no processamento e na consistência. Uma aplicação madura precisa olhar para as duas pontas.</p><p>Um teste simples de backend pode validar uma regra essencial de pedido:</p><pre>describe(&#39;OrderService&#39;, () =&gt; {<br>  it(&#39;should calculate the order total correctly&#39;, async () =&gt; {<br>    const order = await OrderService.create({<br>      customerId: 10,<br>      items: [<br>        { productId: 1, quantity: 2, price: 100 }<br>      ]<br>    });<br>    expect(order.total).toBe(200);<br>    expect(order.status).toBe(&#39;pending&#39;);<br>  });<br>});</pre><p>Esse exemplo é simples, mas representa algo fundamental: uma regra importante não depende apenas da memória de alguém para ser validada. Ela passa a fazer parte de uma base executável de confiança.</p><p>Testes automatizados não tornam o sistema perfeito. Nenhum teste faz isso. Mas eles reduzem a chance de erros previsíveis chegarem em produção. Eles ajudam a preservar comportamentos importantes. Eles tornam a evolução menos arriscada.</p><p>E, principalmente, eles permitem que o time continue mudando.</p><p>Porque um sistema que não pode ser alterado com segurança deixa de ser uma plataforma de crescimento e passa a ser uma fonte de medo.</p><p><strong>O segundo pilar é </strong><a href="https://medium.com/@patrickotto.dev/se-tudo-no-seu-sistema-precisa-acontecer-ao-mesmo-tempo-ele-já-está-em-risco-8eed2225c545"><strong>processamento assíncrono</strong></a><strong>.</strong></p><p>Um dos sinais mais claros de fragilidade em sistemas que crescem é a dependência excessiva entre operações. No início, é comum que tudo aconteça dentro do mesmo fluxo. Uma requisição chega, o backend processa todas as etapas e retorna uma resposta.</p><p>Esse modelo é simples, produtivo e fácil de entender. Frameworks como Express, ASP.NET, Django, Laravel e Spring Boot tornam esse tipo de desenvolvimento bastante direto. Para sistemas pequenos, funciona muito bem.</p><p>O problema começa quando o fluxo cresce junto com o negócio.</p><p>Uma operação que antes apenas salvava um cadastro passa a validar dados, consultar APIs externas, enviar e-mails, atualizar dashboards, gerar histórico, publicar notificações e disparar integrações. Um pedido deixa de ser apenas um pedido. Ele passa a envolver estoque, pagamento, nota fiscal, comunicação, relatórios e sistemas terceiros.</p><p>Quando tudo isso acontece de forma síncrona, cada etapa adiciona tempo e risco à operação principal.</p><p>Se uma API externa fica lenta, o usuário espera. Se o serviço de e-mail falha, uma operação que não deveria depender dele pode ser bloqueada. Se uma integração está fora do ar, o fluxo inteiro pode ser comprometido. Se uma rotina pesada consome recursos demais, outros usuários podem ser afetados.</p><p>Esse é o efeito dominó aplicado à arquitetura.</p><p>Muitas empresas tentam resolver esse problema aumentando infraestrutura. Colocam mais servidores, aumentam memória, escalam containers, adicionam cache e criam réplicas. Tudo isso pode ajudar, mas não corrige o problema central quando a causa é acoplamento.</p><p>Se tudo depende de tudo, o sistema continua frágil.</p><p>Processos assíncronos mudam essa lógica. Eles permitem separar o que precisa acontecer agora daquilo que pode acontecer depois. Em vez de executar todas as etapas dentro da mesma requisição, o sistema publica eventos, envia mensagens para filas ou delega tarefas para workers.</p><p>Ferramentas como RabbitMQ, Kafka, AWS SQS, Google Pub/Sub e Azure Service Bus ajudam a implementar essa separação. RabbitMQ funciona muito bem para filas tradicionais e distribuição de tarefas. Kafka é forte para alto volume de eventos, streaming e múltiplos consumidores. SQS, Pub/Sub e Azure Service Bus simplificam esse modelo em ambientes cloud.</p><p>Mais importante do que a ferramenta é a mudança de mentalidade.</p><p>A pergunta deixa de ser “como faço tudo isso mais rápido?” e passa a ser “o que realmente precisa acontecer agora e o que pode acontecer depois?”.</p><p>Essa pergunta melhora a arquitetura.</p><p>Um fluxo síncrono de pedido poderia ser assim:</p><pre>app.post(&#39;/orders&#39;, async (req, res) =&gt; {<br>  const order = await createOrder(req.body);<br>  await reserveStock(order);<br>  await processPayment(order);<br>  await issueInvoice(order);<br>  await sendConfirmationEmail(order);<br>  await notifyExternalMarketplace(order);<br>  res.json({<br>    success: true,<br>    orderId: order.id<br>  });<br>});</pre><p>Esse código parece organizado, mas concentra responsabilidades demais em um único fluxo. Uma versão mais resiliente poderia separar o essencial do complementar:</p><pre>app.post(&#39;/orders&#39;, async (req, res) =&gt; {<br>  const order = await createOrder(req.body);<br>  await reserveStock(order);<br>  await processPayment(order);<br>  await queue.publish(&#39;order.created&#39;, {<br>    orderId: order.id,<br>    customerId: order.customerId<br>  });<br>  res.json({<br>    success: true,<br>    orderId: order.id<br>  });<br>});</pre><p>Nesse modelo, o fluxo principal responde ao usuário depois do que é essencial. O restante pode ser processado em segundo plano. Um worker envia e-mail. Outro emite nota. Outro atualiza relatórios. Outro notifica sistemas externos.</p><p>Isso não elimina falhas, mas impede que falhas secundárias derrubem o fluxo principal.</p><p>Essa é a essência da resiliência.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dHMR4BHe1QXD3WdCggo3_Q.png" /><figcaption>Processos assíncronos não eliminam falhas, mas impedem que uma falha localizada bloqueie todo o fluxo do sistema.</figcaption></figure><p><strong>O terceiro pilar é </strong><a href="https://medium.com/@patrickotto.dev/se-você-não-sabe-o-que-acontece-no-seu-sistema-você-não-controla-nada-becd2ec1ccc6"><strong>observabilidade</strong></a><strong>.</strong></p><p>Se testes ajudam a validar o que deveria funcionar e processos assíncronos ajudam a organizar responsabilidades, observabilidade ajuda a responder uma pergunta essencial: o sistema está realmente se comportando como deveria em produção?</p><p>Essa pergunta é mais difícil do que parece.</p><p>Em sistemas simples, logs e métricas básicas costumam ser suficientes. Um erro aparece, alguém abre o log, encontra a exceção e corrige. Existe uma relação direta entre causa e efeito.</p><p>Em sistemas que crescem, essa relação fica menos óbvia.</p><p>Mais serviços entram no fluxo. Mais integrações são adicionadas. Filas processam eventos em segundo plano. Bancos diferentes são consultados. O frontend pode apresentar erros que o backend não enxerga. O backend pode estar saudável enquanto uma jornada do usuário está quebrada. Uma fila pode crescer silenciosamente sem afetar a interface imediatamente.</p><p>Nesse cenário, problemas não aparecem como respostas simples. Eles aparecem como sintomas.</p><p>Um endpoint fica lento. Uma integração falha de forma intermitente. Um usuário reclama de uma tela travada. Uma fila acumula mensagens. Um serviço consome mais memória do que deveria. O checkout demora apenas para alguns clientes. O erro real pode estar três serviços antes do ponto onde o sintoma apareceu.</p><p>Sem observabilidade, o time adivinha.</p><p>Com observabilidade, o time investiga.</p><p>Observabilidade normalmente se apoia em três sinais principais: métricas, logs e traces. Métricas mostram comportamento quantitativo ao longo do tempo, como latência, tráfego, erro e saturação. Logs registram eventos específicos. Traces mostram o caminho real de uma requisição entre diferentes serviços.</p><p>O valor aparece quando esses sinais são conectados.</p><p>Um trace pode mostrar algo assim:</p><pre>Client<br>  → API Gateway: 45ms<br>    → Auth Service: 30ms<br>    → Order Service: 120ms<br>      → Database: 80ms<br>      → Payment Service: 2,400ms<br>        → External Provider: 2,200ms<br>    → Response: 2,610ms</pre><p>Nesse exemplo, não é necessário adivinhar. O gargalo está no serviço de pagamento e, mais especificamente, no provedor externo. A discussão deixa de ser opinião e passa a ser diagnóstico.</p><p>Ferramentas como Datadog, New Relic, Grafana, Prometheus, Loki, Tempo, Jaeger e OpenTelemetry ajudam a construir essa visão. Algumas são mais completas e gerenciadas. Outras são mais flexíveis e exigem mais maturidade operacional. A escolha depende do contexto, mas o objetivo é o mesmo: transformar sinais dispersos em entendimento.</p><p>Observabilidade não é excesso de dashboard. Também não é coletar tudo para sempre. É saber quais sinais realmente ajudam a operar melhor o sistema.</p><p>Isso vale para backend, frontend, filas, infraestrutura e jornadas de negócio.</p><p>Em aplicações frontend, ferramentas como Sentry, LogRocket, Datadog RUM e New Relic Browser ajudam a enxergar erros JavaScript, lentidão em telas, falhas por navegador, problemas de dispositivo e impacto real na experiência do usuário. Isso é importante porque nem todo problema de produção está no servidor.</p><p>Às vezes, a API está saudável, mas a interface quebrou uma jornada crítica.</p><p>Sem observabilidade, a empresa enxerga apenas parte da realidade.</p><p>E meia realidade pode levar a decisões erradas.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5Or38LNz561-p6Dp_FdSsA.png" /><figcaption>Observabilidade permite enxergar o comportamento invisível do sistema e transformar sinais dispersos em contexto para decisão.</figcaption></figure><p><strong>O quarto pilar é </strong><a href="https://medium.com/@patrickotto.dev/se-seu-deploy-gera-medo-seu-problema-não-é-o-código-0284239b4836"><strong>CI/CD</strong></a><strong>.</strong></p><p>Se testes criam confiança, processos assíncronos aumentam resiliência e observabilidade gera entendimento, CI/CD transforma entrega em processo. Sem ele, toda essa base perde força no momento em que o código precisa chegar em produção.</p><p>Existe um sinal muito claro de baixa maturidade: deploy com medo.</p><p>Quando cada publicação exige tensão, checklist manual, acompanhamento excessivo e esperança, o problema raramente está apenas no código. O problema está no processo de entrega.</p><p>No início, publicar software manualmente parece aceitável. O time é pequeno, o sistema é simples e todos sabem o que precisa ser feito. Mas conforme o negócio cresce, a entrega manual passa a se tornar um risco.</p><p>Mais desenvolvedores contribuem. Mais serviços precisam ser publicados. Mais variáveis precisam ser configuradas. Mais clientes são impactados. Mais integrações dependem do sistema funcionando corretamente.</p><p>Nesse contexto, cada deploy manual aumenta a chance de erro.</p><p>CI/CD significa integração contínua e entrega contínua ou implantação contínua. Mais importante do que a sigla é o conceito: toda alteração deve seguir um caminho confiável de validação, build, testes, publicação e acompanhamento.</p><p>Ferramentas como GitHub Actions, Azure DevOps, Bitbucket Pipelines, GitLab CI e Jenkins ajudam a estruturar esse fluxo. GitHub Actions é produtivo para quem já usa GitHub. Azure DevOps é forte em ambientes corporativos e ecossistema Microsoft. Bitbucket Pipelines funciona bem para times que utilizam Bitbucket. GitLab CI é completo dentro do ecossistema GitLab. Jenkins é flexível, mas exige mais operação.</p><p>A ferramenta importa, mas não é o ponto principal.</p><p>Um pipeline ruim continua sendo ruim em qualquer plataforma.</p><p>O que define maturidade é o desenho do processo.</p><p>Um pipeline básico pode executar testes e build:</p><pre>name: CI Pipeline<br>on:<br>  push:<br>    branches:<br>      - main<br>  pull_request:<br>    branches:<br>      - main<br>jobs:<br>  validate:<br>    runs-on: ubuntu-latest<br>    steps:<br>      - name: Checkout repository<br>        uses: actions/checkout@v4<br>      - name: Setup Node.js<br>        uses: actions/setup-node@v4<br>        with:<br>          node-version: 20<br>      - name: Install dependencies<br>        run: npm ci<br>      - name: Run tests<br>        run: npm test<br>      - name: Build application<br>        run: npm run build</pre><p>Esse exemplo não resolve tudo, mas já cria um fluxo mínimo de validação. O código não depende apenas de alguém lembrar de rodar os comandos localmente. O processo passa a ser padronizado.</p><p>Em sistemas mais maduros, o pipeline pode incluir análise de segurança, validação de dependências, build de imagem Docker, publicação em registry, deploy em homologação, aprovação para produção, rollback, feature flags, canary release, blue-green deployment e validação pós-deploy.</p><p>O objetivo não é criar burocracia.</p><p>É reduzir risco.</p><p>CI/CD maduro não termina quando o código sobe. Ele precisa se conectar com observabilidade para confirmar se o sistema continua saudável depois da publicação. A taxa de erro aumentou? A latência piorou? O checkout falhou? A fila começou a acumular? A nova versão afetou uma jornada crítica?</p><p>Deploy bem-sucedido não é apenas aplicação no ar.</p><p>Deploy bem-sucedido é sistema se comportando como esperado.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7yoogkK33kLveI4KCrkBFw.png" /><figcaption>Assim como em uma missão espacial, uma entrega confiável valida, rastreia e promove o mesmo artefato até o lançamento final.</figcaption></figure><p>Quando esses quatro pilares trabalham juntos, o comportamento do sistema muda.</p><p>Testes automatizados reduzem medo de mudança. Processos assíncronos reduzem propagação de falhas. Observabilidade reduz trabalho no escuro. CI/CD reduz improviso na entrega.</p><p>Nenhum desses pilares funciona sozinho em sua melhor forma.</p><p>Testes sem CI/CD dependem de alguém lembrar de executá-los. CI/CD sem testes apenas automatiza risco. Processos assíncronos sem observabilidade podem transformar filas em caixas pretas. Observabilidade sem boas práticas de arquitetura apenas mostra com clareza um sistema que continua difícil de operar.</p><p>O valor está na combinação.</p><p>Essa combinação cria uma base onde o sistema pode evoluir com mais segurança. O time consegue alterar código com menos medo. A aplicação consegue absorver falhas sem derrubar tudo. A operação consegue entender problemas com mais rapidez. A entrega consegue seguir um caminho previsível até produção.</p><p>Isso não elimina complexidade.</p><p>Mas organiza a complexidade.</p><p>E sistemas que escalam não são sistemas sem complexidade. São sistemas onde a complexidade foi tratada com responsabilidade.</p><p>Essa responsabilidade aparece em decisões que muitas vezes não são visíveis para o usuário final. O cliente não vê os testes rodando. Não vê a fila processando eventos. Não vê os traces conectando serviços. Não vê o pipeline validando build. Não vê o rollback disponível. Não vê o alerta antes do incidente.</p><p>Mas ele sente o resultado.</p><p>Sente quando o sistema é estável. Sente quando a experiência é consistente. Sente quando problemas são resolvidos rápido. Sente quando novas funcionalidades chegam sem quebrar o que já funcionava. Sente quando a plataforma responde bem mesmo em momentos de pressão.</p><p>Engenharia boa nem sempre aparece.</p><p>Mas a ausência dela aparece muito.</p><p>Aparece em deploy com medo. Aparece em bug recorrente. Aparece em sistema lento sem explicação. Aparece em fila travada que ninguém viu. Aparece em integração que falha silenciosamente. Aparece em funcionalidade que quebra outra. Aparece em time que evita mexer no próprio código. Aparece em cliente que perde confiança.</p><p>Por isso, esses quatro pilares não devem ser tratados como luxo técnico.</p><p>Eles são parte da seriedade de uma empresa que depende de software para operar.</p><p>Uma empresa que cresce sem testes está acumulando risco de mudança. Uma empresa que cresce sem processos assíncronos está acumulando dependência. Uma empresa que cresce sem observabilidade está acumulando cegueira operacional. Uma empresa que cresce sem CI/CD está acumulando risco de entrega.</p><p>No começo, tudo isso parece administrável.</p><p>Depois, vira custo.</p><p>E quando vira custo, geralmente já está afetando o produto, o time e o negócio.</p><p>Existe uma frase que resume bem essa lógica: sistemas não quebram apenas porque cresceram. Eles quebram porque cresceram sobre uma base que não estava preparada.</p><p>Essa base precisa ser construída com intenção.</p><p>Não precisa começar perfeita. Nenhum sistema começa maduro em todos os pontos. Mas precisa começar em algum lugar. Um conjunto mínimo de testes. Uma fila para separar tarefas críticas. Um painel para enxergar fluxos importantes. Um pipeline para padronizar validações. Um processo simples de rollback. Uma métrica de negócio acompanhada em produção.</p><p>Maturidade técnica não nasce pronta.</p><p>Ela é construída por camadas.</p><p>O problema é quando a empresa adia todas essas camadas até o momento em que não consegue mais evoluir sem sofrimento. Quando isso acontece, cada melhoria passa a competir com urgências, incidentes, retrabalho e pressão de negócio.</p><p>É sempre mais caro organizar depois.</p><p>Não apenas em dinheiro, mas em energia, confiança e tempo.</p><p>E tempo é uma das coisas mais caras em empresas que estão crescendo.</p><p>Porque enquanto o time está apagando incêndio, o produto deixa de evoluir. Enquanto o time está investigando problema sem contexto, o cliente está sentindo o impacto. Enquanto o deploy exige tensão, a inovação desacelera. Enquanto o sistema depende de esforço manual, a empresa perde previsibilidade.</p><p>No fim, escala não é apenas uma questão de infraestrutura.</p><p>Escala é uma questão de maturidade.</p><p>Infraestrutura pode sustentar volume. Mas maturidade sustenta evolução.</p><p>E evolução é o que mantém um produto vivo.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*y8mArFNKxiniJVFUsFmBrA.png" /><figcaption>Sistemas preparados para escalar dependem de pilares que trabalham juntos, sustentando mudança, resiliência, visibilidade e entrega contínua.</figcaption></figure><p>Empresas que levam software a sério precisam olhar para esses pilares não como uma lista técnica, mas como parte da estratégia de crescimento. O sistema não é apenas um apoio ao negócio. Em muitos casos, ele é o próprio negócio. Ele vende, atende, processa, integra, calcula, comunica, valida, registra e entrega valor.</p><p>Se esse sistema não consegue mudar com segurança, o negócio desacelera.</p><p>Se não consegue absorver falhas, o negócio fica vulnerável.</p><p>Se não consegue ser observado, o negócio opera no escuro.</p><p>Se não consegue ser entregue com previsibilidade, o negócio passa a temer evolução.</p><p>Essa é a razão pela qual engenharia de software não pode ser vista apenas como execução. Engenharia é capacidade de sustentar crescimento com responsabilidade.</p><p>E responsabilidade, nesse contexto, significa construir sistemas que não dependem de sorte para continuar funcionando.</p><p>Sorte não é processo. Esperança não é estratégia. Esforço manual não é escala.</p><p>Um sistema sério precisa ser capaz de responder a perguntas simples.</p><p>Se eu alterar essa regra, o que garante que não quebrei outra? Se essa integração falhar, o que acontece com o fluxo principal? Se o usuário reclamar de lentidão, consigo entender onde está o problema? Se esse deploy falhar, consigo voltar com segurança? Se o volume dobrar, o sistema absorve ou colapsa? Se o time crescer, o processo continua previsível?</p><p>Essas perguntas dizem muito sobre a maturidade de uma empresa.</p><p>E a resposta raramente está em uma única ferramenta.</p><p>Está na arquitetura. Está no processo. Está na cultura. Está na disciplina de tratar software como algo vivo, crítico e em constante evolução.</p><p>No fim, sistemas que escalam de verdade não são aqueles que nunca falham. São aqueles que foram preparados para mudar, falhar parcialmente, se recuperar, ser compreendidos e continuar evoluindo.</p><p>Essa é a diferença entre crescimento e escala.</p><p>Crescimento pode acontecer por tração de mercado, esforço comercial ou aumento de demanda.</p><p>Escala exige engenharia.</p><p><strong>A pergunta que fica é simples: sua empresa está construindo uma base real para escalar ou apenas empilhando complexidade até o próximo incidente?</strong> 🚀</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a079ae7446ef" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Se seu deploy gera medo, seu problema não é o código]]></title>
            <link>https://medium.com/@patrickotto.dev/se-seu-deploy-gera-medo-seu-problema-n%C3%A3o-%C3%A9-o-c%C3%B3digo-0284239b4836?source=rss-daa0cab73ef2------2</link>
            <guid isPermaLink="false">https://medium.com/p/0284239b4836</guid>
            <dc:creator><![CDATA[Patrick Otto]]></dc:creator>
            <pubDate>Sun, 17 May 2026 21:58:33 GMT</pubDate>
            <atom:updated>2026-05-17T22:19:33.102Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZekeDXb8o-PWrt70UHbDSA.png" /></figure><blockquote>Quando a entrega depende de validação manual, cuidado excessivo e sorte, o deploy deixa de ser processo e passa a ser risco operacional.</blockquote><p>Existe um momento bastante claro na evolução de um sistema em que o deploy deixa de ser apenas uma etapa natural do desenvolvimento e passa a ser tratado como um evento. Esse momento nem sempre aparece em um relatório técnico, mas fica evidente no comportamento do time.</p><p>Antes, publicar uma nova versão era simples. O desenvolvedor terminava uma funcionalidade, fazia uma validação manual, subia o código e acompanhava se tudo continuava funcionando. Em sistemas pequenos, esse modelo parece suficiente. Poucas pessoas mexem no código, as regras são mais fáceis de entender, as integrações são limitadas e o impacto de uma falha geralmente pode ser corrigido rapidamente.</p><p>Durante um tempo, isso funciona.</p><p>O problema é que sistemas não permanecem pequenos quando o negócio começa a crescer. Mais desenvolvedores passam a contribuir. Mais funcionalidades entram no produto. Mais serviços se comunicam entre si. Mais clientes dependem da aplicação funcionando corretamente. Mais integrações externas entram no fluxo. O que antes era um processo simples começa a se tornar um ponto de tensão.</p><p>Nesse momento, o deploy muda de natureza.</p><p>Ele deixa de ser rotina e passa a ser risco.</p><p>Esse risco aparece de várias formas. O time evita publicar em determinados horários. Deploy em sexta-feira se torna assunto proibido. Cada nova versão exige uma reunião de alinhamento. Alguém precisa acompanhar logs manualmente. Outro precisa validar telas. Alguém fica responsável por testar uma funcionalidade específica. Se algo falha, a equipe precisa correr para entender o que aconteceu e, muitas vezes, desfazer a alteração às pressas.</p><p>Esse comportamento é um sintoma.</p><p>Ele mostra que a empresa não possui um processo confiável de entrega.</p><p>Quando uma empresa depende de cuidado manual para publicar software, ela está operando em um modelo frágil. Não importa se o time é bom, experiente ou cuidadoso. Em algum momento, a complexidade supera a capacidade humana de lembrar todos os detalhes, validar todos os caminhos e prever todos os impactos.</p><p>É aqui que CI/CD deixa de ser apenas uma prática técnica e passa a ser uma estrutura fundamental para crescimento.</p><p>CI/CD significa Continuous Integration e Continuous Delivery ou Continuous Deployment. Em português, costuma ser traduzido como integração contínua e entrega contínua ou implantação contínua. Mas mais importante do que a tradução é entender o conceito.</p><p>CI/CD é a criação de um fluxo confiável para que cada alteração no código seja validada, construída, testada e entregue de forma consistente.</p><p>Isso muda completamente a lógica da entrega.</p><p>Sem CI/CD, cada deploy depende de pessoas executando etapas manualmente. Com CI/CD, cada alteração passa por um caminho padronizado. O código é versionado, o pipeline é acionado, as validações são executadas, os testes rodam, o build é gerado e a entrega segue critérios definidos.</p><p>O objetivo não é apenas publicar mais rápido.</p><p>O objetivo é publicar com previsibilidade.</p><p>Essa distinção é importante porque muitas empresas confundem CI/CD com velocidade. Elas imaginam que o principal benefício é entregar mais vezes por dia ou reduzir o tempo de publicação. Embora isso possa acontecer, esse não é o ponto central.</p><p>O verdadeiro valor está em reduzir incerteza.</p><p>Em um sistema que cresce, incerteza é cara. Ela aparece no medo de alterar código, no receio de publicar uma nova versão, no aumento de validações manuais, no retrabalho depois de incidentes e na perda de confiança entre negócio e tecnologia.</p><p>Quando o pipeline é bem construído, a empresa cria uma espécie de trilho para a entrega. Cada mudança passa por etapas conhecidas. Cada validação ocorre da mesma forma. Cada falha interrompe o processo antes que o problema chegue em produção.</p><p>Esse é o ponto onde CI/CD se conecta diretamente com testes automatizados.</p><p>Um pipeline sem testes relevantes é apenas uma esteira levando código para produção. Ele pode até automatizar a entrega, mas não necessariamente reduz risco. Se a aplicação não possui validações automatizadas, o pipeline não tem como saber se uma regra importante foi quebrada.</p><p>Por isso, CI/CD e testes não deveriam ser tratados como temas separados.</p><p>Testes dão segurança para validar o comportamento do sistema. CI/CD garante que essa validação aconteça sempre, de forma consistente, antes da entrega.</p><p>Imagine uma aplicação Node.js simples. Sem pipeline, alguém precisa lembrar de instalar dependências, rodar testes, gerar build e publicar. Com pipeline, esse fluxo pode ser automatizado.</p><p>Um exemplo básico com GitHub Actions poderia ser assim:</p><pre>name: CI Pipeline<br>on:<br>  push:<br>    branches:<br>      - main<br>  pull_request:<br>    branches:<br>      - main<br>jobs:<br>  validate:<br>    runs-on: ubuntu-latest<br>    steps:<br>      - name: Checkout repository<br>        uses: actions/checkout@v4<br>      - name: Setup Node.js<br>        uses: actions/setup-node@v4<br>        with:<br>          node-version: 20<br>      - name: Install dependencies<br>        run: npm ci<br>      - name: Run tests<br>        run: npm test<br>      - name: Build application<br>        run: npm run build</pre><p>Esse pipeline ainda é simples, mas já resolve um problema importante: nenhuma alteração passa sem executar uma sequência mínima de validações. O repositório é baixado, a versão do Node.js é configurada, as dependências são instaladas, os testes são executados e o build é gerado.</p><p>Esse tipo de automação reduz a dependência de validação manual.</p><p>Mas CI/CD não termina aqui.</p><p>Em sistemas reais, o pipeline precisa evoluir conforme a maturidade da aplicação cresce. Pode incluir análise estática de código, validação de padrões, verificação de segurança, testes de integração, geração de artefatos, publicação de imagens Docker, deploy em ambiente de homologação, aprovação manual para produção, rollback e validação pós-deploy.</p><p>O ponto não é criar um pipeline complexo por vaidade técnica. O ponto é construir um fluxo que reflita o risco real do sistema.</p><p>Uma aplicação interna simples talvez precise de um pipeline mais direto. Um sistema financeiro, de saúde, e-commerce ou plataforma crítica provavelmente precisa de validações mais rigorosas. O nível de controle precisa acompanhar o impacto da falha.</p><p>Essa é uma diferença importante entre automatizar e amadurecer.</p><p>Automatizar é fazer uma máquina executar uma sequência de comandos. Amadurecer é desenhar uma sequência de validações que protege o negócio.</p><p>Ferramentas como GitHub Actions, Azure DevOps, Bitbucket Pipelines, GitLab CI e Jenkins existem para ajudar nessa construção. Todas elas permitem criar pipelines, executar tarefas, integrar com repositórios, rodar testes e publicar aplicações. Mas cada uma tem características diferentes.</p><p>GitHub Actions costuma ser muito produtivo para times que já utilizam GitHub. Ele se integra diretamente ao repositório, possui uma grande comunidade de actions prontas e permite configurar fluxos de forma relativamente simples. É uma ótima escolha para startups, produtos digitais, projetos open source e times que desejam velocidade sem muita complexidade inicial.</p><p>Azure DevOps costuma aparecer com força em ambientes corporativos, especialmente onde existe uso intenso de ecossistema Microsoft, .NET, Azure, boards, releases e governança mais centralizada. Ele oferece uma estrutura mais ampla, integrando repositório, pipeline, backlog, artefatos e controle de entrega.</p><p>Bitbucket Pipelines é uma opção prática para times que utilizam Bitbucket. Ele é direto, integrado ao repositório e costuma atender bem projetos que precisam de um fluxo mais enxuto, sem uma estrutura operacional muito pesada.</p><p>GitLab CI é bastante completo, principalmente para empresas que utilizam GitLab como plataforma central. Ele permite integrar repositório, issues, merge requests, pipelines, registry e deploy em um único ambiente.</p><p>Jenkins é uma ferramenta mais tradicional e extremamente flexível. Por outro lado, exige mais manutenção, configuração e cuidado operacional. Em ambientes onde já existe uma base consolidada, pode fazer sentido. Em times que querem reduzir esforço de operação, soluções mais gerenciadas podem ser mais interessantes.</p><p>A escolha da ferramenta importa, mas não é o ponto principal.</p><p>Um pipeline ruim no Azure DevOps continua sendo ruim. Um pipeline ruim no GitHub Actions continua sendo ruim. Um pipeline ruim no Jenkins continua sendo ruim.</p><p>A maturidade não está no nome da ferramenta.</p><p>Está no desenho do processo.</p><p>E um bom processo começa com uma pergunta simples: o que precisa ser verdade para que esse código possa chegar em produção com segurança?</p><p>Essa pergunta muda a forma como o pipeline é pensado.</p><p>Se a aplicação depende de testes unitários, eles precisam rodar. Se existem testes de integração, eles precisam ser considerados. Se há risco de vulnerabilidades em dependências, uma checagem de segurança pode fazer sentido. Se o sistema é containerizado, a imagem precisa ser construída de forma padronizada. Se o deploy ocorre em Kubernetes, os manifests ou charts precisam ser validados. Se existem variáveis sensíveis, secrets precisam ser tratados corretamente.</p><p>Cada etapa deve existir por um motivo.</p><p>Um pipeline cheio de etapas sem propósito gera lentidão e ruído. Um pipeline simples demais para um sistema crítico gera risco. O equilíbrio está em criar validações suficientes para proteger a entrega sem transformar o processo em burocracia inútil.</p><p>Esse equilíbrio é o que separa engenharia madura de ritual técnico.</p><p>Em muitos times, o primeiro grande ganho de CI/CD é eliminar o famoso “na minha máquina funciona”. Quando o build roda em um ambiente padronizado, com versões definidas e comandos consistentes, a subjetividade diminui. O código deixa de depender da configuração local de cada desenvolvedor e passa a ser validado em um ambiente reproduzível.</p><p>Isso parece básico, mas é extremamente importante.</p><p>Um sistema que depende da máquina de uma pessoa para ser publicado não possui processo. Possui dependência individual.</p><p>Essa dependência é perigosa. Se apenas uma pessoa sabe publicar, essa pessoa vira gargalo. Se apenas uma máquina tem a configuração correta, essa máquina vira risco. Se o deploy depende de passos manuais não documentados, a empresa está apostando que ninguém vai esquecer nada.</p><p>E esquecer acontece.</p><p>Principalmente sob pressão.</p><p>CI/CD reduz esse tipo de fragilidade porque transforma conhecimento operacional em processo executável. Aquilo que antes estava na cabeça de alguém passa a estar descrito no pipeline.</p><p>Isso não significa remover responsabilidade humana. Significa reduzir erro humano em etapas repetitivas e críticas.</p><p>Um pipeline mais completo poderia incluir build de imagem Docker:</p><pre>name: Build and Push Docker Image<br>on:<br>  push:<br>    branches:<br>      - main<br>jobs:<br>  docker:<br>    runs-on: ubuntu-latest<br>    steps:<br>      - name: Checkout repository<br>        uses: actions/checkout@v4<br>      - name: Login to container registry<br>        run: echo &quot;${{ secrets.REGISTRY_PASSWORD }}&quot; | docker login registry.example.com -u &quot;${{ secrets.REGISTRY_USER }}&quot; --password-stdin<br>      - name: Build image<br>        run: docker build -t registry.example.com/my-app:${{ github.sha }} .<br>      - name: Push image<br>        run: docker push registry.example.com/my-app:${{ github.sha }}</pre><p>Nesse exemplo, cada versão da aplicação pode ser empacotada em uma imagem identificada pelo hash do commit. Isso cria rastreabilidade. Se uma versão apresentou problema, fica mais fácil saber exatamente qual código foi publicado.</p><p>Rastreabilidade é um dos aspectos mais importantes de uma entrega madura.</p><p>Quando algo falha em produção, a empresa precisa saber o que mudou. Qual commit entrou? Qual versão foi publicada? Qual pipeline executou? Quais testes passaram? Quem aprovou? Em qual ambiente a falha apareceu?</p><p>Sem rastreabilidade, incidentes viram investigação manual.</p><p>Com rastreabilidade, a análise começa com evidência.</p><p>Esse ponto conecta CI/CD diretamente com observabilidade. O pipeline entrega a mudança, mas a observabilidade mostra como essa mudança se comporta em produção. Um bom processo de engenharia não termina quando o deploy é concluído. Ele continua depois da publicação.</p><p>É por isso que validação pós-deploy é importante.</p><p>Muitas empresas tratam deploy como sucesso no momento em que a aplicação sobe. Mas subir não significa funcionar corretamente. Um serviço pode estar no ar e ainda assim falhar em fluxos críticos. Uma API pode retornar status 200 em health check e mesmo assim quebrar uma jornada importante. Um frontend pode carregar e ainda assim impedir o usuário de concluir uma ação.</p><p>Deploy bem-sucedido não é apenas infraestrutura ativa.</p><p>Deploy bem-sucedido é sistema se comportando como esperado.</p><p>Uma abordagem mais madura inclui acompanhar métricas depois da publicação. Taxa de erro aumentou? Latência piorou? Fila começou a acumular? Checkout caiu? Login ficou lento? Algum serviço passou a consumir mais CPU ou memória? O número de exceções aumentou?</p><p>Essas perguntas deveriam fazer parte da cultura de entrega.</p><p>Sem isso, o deploy é visto como fim do processo, quando na verdade ele é apenas o começo da validação em ambiente real.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7LAeUfimrpRQE_aSH334XA.png" /><figcaption>CI/CD transforma a entrega de software em uma cadeia de confiança, onde cada etapa valida o código antes que ele chegue ao usuário.</figcaption></figure><p>Outro ponto essencial é rollback.</p><p>Todo sistema sério precisa considerar a possibilidade de falha. Não porque o time seja ruim, mas porque produção é um ambiente vivo. Usuários se comportam de formas inesperadas, integrações oscilam, dados reais expõem cenários não previstos e mudanças simples podem gerar efeitos colaterais.</p><p>A questão não é se um deploy algum dia vai falhar.</p><p>A questão é quanto tempo sua empresa leva para se recuperar quando isso acontece.</p><p>Rollback é a capacidade de voltar rapidamente para uma versão anterior quando algo dá errado. Em alguns ambientes, isso pode ser feito revertendo uma imagem Docker. Em outros, pode envolver troca de versão em Kubernetes, estratégias blue-green, canary releases ou feature flags.</p><p>Cada abordagem tem sua função.</p><p>Blue-green deployment trabalha com dois ambientes. Um está ativo, o outro recebe a nova versão. Depois da validação, o tráfego é direcionado para a nova versão. Se algo der errado, é possível voltar para o ambiente anterior com mais rapidez.</p><p>Canary release libera a nova versão para uma pequena porcentagem de usuários antes de expandir para todos. Isso reduz impacto, porque problemas podem ser detectados em uma amostra menor.</p><p>Feature flags permitem ativar ou desativar funcionalidades sem necessariamente fazer um novo deploy. Elas são muito úteis para reduzir risco em lançamentos, testar comportamentos e controlar exposição de funcionalidades.</p><p>Essas estratégias mostram que CI/CD não é apenas “subir código”.</p><p>É controlar risco.</p><p>Uma empresa que publica tudo de uma vez para todos os usuários assume um tipo de risco. Uma empresa que libera gradualmente, mede impacto e tem plano de retorno assume outro nível de maturidade.</p><p>Isso não significa que todo projeto precisa começar com blue-green, canary e feature flags. Mais uma vez, contexto importa. Mas empresas que crescem precisam entender que deploy não é apenas uma ação técnica. É uma decisão operacional.</p><p>E decisões operacionais precisam de processo.</p><p>Um dos erros comuns em empresas em crescimento é acreditar que CI/CD só deve ser implementado quando o sistema já estiver grande. Essa visão inverte a lógica. Quanto mais tarde o pipeline entra, mais difícil fica padronizar o processo.</p><p>Quando a aplicação já possui múltiplos serviços, várias formas de deploy, diferentes padrões de configuração e pouca documentação, criar CI/CD se torna mais trabalhoso. O pipeline passa a precisar organizar uma bagunça já existente.</p><p>O ideal é que a cultura de entrega comece cedo, ainda que simples.</p><p>Um pipeline inicial pode rodar testes e build. Depois pode publicar em homologação. Depois pode adicionar análise de segurança. Depois pode automatizar deploy. Depois pode incluir rollback, validação pós-deploy e estratégias progressivas.</p><p>A maturidade cresce por camadas.</p><p>O erro é não começar.</p><p>Também existe um impacto direto no time. Quando o deploy depende de etapas manuais, as pessoas ficam mais tensas. O medo de quebrar produção aumenta. Novos desenvolvedores demoram mais para ganhar autonomia. Pessoas específicas viram guardiãs do processo. O conhecimento fica concentrado.</p><p>Com CI/CD, a entrega se torna mais democrática e previsível.</p><p>Isso não significa que qualquer pessoa deve publicar qualquer coisa sem responsabilidade. Significa que o processo passa a proteger a entrega, independentemente de quem fez a alteração.</p><p>Essa mudança melhora a cultura do time.</p><p>O desenvolvedor passa a receber feedback mais rápido. Se um teste falha, ele descobre antes. Se o build quebra, ele descobre antes. Se uma dependência apresenta vulnerabilidade, ele descobre antes. Se a aplicação não empacota corretamente, ele descobre antes.</p><p>Feedback rápido reduz custo.</p><p>Quanto mais cedo um problema é identificado, mais barato ele é para corrigir. Um erro encontrado no pipeline é mais barato do que um erro encontrado em produção. Um build quebrado em pull request é mais barato do que um deploy emergencial. Uma falha detectada antes da publicação é mais barata do que uma falha percebida pelo cliente.</p><p>Esse é um dos motivos pelos quais CI/CD tem impacto direto no negócio.</p><p>Não se trata apenas de produtividade técnica. Trata-se de reduzir retrabalho, aumentar previsibilidade, diminuir incidentes, acelerar resposta ao mercado e aumentar confiança no produto.</p><p>Quando o negócio sabe que a engenharia consegue entregar com segurança, a relação muda.</p><p>As áreas deixam de tratar tecnologia como gargalo e passam a enxergar tecnologia como capacidade de execução.</p><p>Mas isso só acontece quando existe consistência.</p><p>Entrega irregular gera desconfiança. Deploys problemáticos geram medo. Incidentes repetidos geram resistência a mudanças. E quando o negócio começa a ter medo de mudança, a empresa perde velocidade estratégica.</p><p>Nesse sentido, CI/CD é mais do que uma prática de engenharia.</p><p>É uma ferramenta de confiança organizacional.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KLjYzZE0J_FKARTS7bxODA.png" /><figcaption>Um deploy maduro não termina quando o código sobe, mas quando o sistema confirma, por meio de métricas, que continua saudável em produção.</figcaption></figure><p>Outro aspecto importante é segurança.</p><p>Pipelines modernos podem incluir validações de segurança em diferentes etapas. Isso pode envolver análise de dependências vulneráveis, verificação de secrets expostos, análise estática de código, validação de imagens Docker e políticas de infraestrutura.</p><p>Ferramentas como Snyk, Dependabot, SonarQube, Trivy, Checkov e OWASP Dependency-Check podem ser integradas ao fluxo. O objetivo não é transformar o pipeline em uma barreira insuportável, mas identificar riscos antes que cheguem em produção.</p><p>Segurança não deveria ser uma etapa manual no fim do projeto.</p><p>Ela precisa fazer parte do fluxo.</p><p>Esse conceito se aproxima de DevSecOps: segurança integrada ao desenvolvimento e à operação. Quando bem aplicada, a segurança deixa de ser um bloqueio tardio e passa a ser uma validação contínua.</p><p>Isso é especialmente importante em ambientes regulados, sistemas financeiros, plataformas com dados sensíveis, health techs, marketplaces e produtos que lidam com informações críticas de usuários.</p><p>Um exemplo simples de verificação de dependências poderia ser integrado assim:</p><pre>name: Security Check<br>on:<br>  pull_request:<br>    branches:<br>      - main<br>jobs:<br>  audit:<br>    runs-on: ubuntu-latest<br>    steps:<br>      - name: Checkout repository<br>        uses: actions/checkout@v4<br>      - name: Install dependencies<br>        run: npm ci<br>      - name: Run npm audit<br>        run: npm audit --audit-level=high</pre><p>Esse exemplo é básico, mas mostra o princípio: problemas conhecidos podem ser identificados antes que a alteração entre na branch principal.</p><p>Em pipelines mais maduros, esse tipo de validação pode ser combinado com análise de imagem Docker, varredura de infraestrutura como código e políticas de aprovação.</p><p>Mais uma vez, o objetivo não é criar burocracia. É reduzir risco.</p><p>Existe uma diferença enorme entre um processo seguro e um processo pesado. Um bom pipeline protege sem paralisar. Um pipeline ruim gera lentidão, desvio e frustração. A maturidade está em calibrar o nível de controle ao risco real do sistema.</p><p>Também é importante falar sobre ambientes.</p><p>Muitas empresas possuem apenas desenvolvimento e produção. Isso pode funcionar em projetos muito pequenos, mas se torna arriscado conforme o sistema cresce. Ambientes como staging, homologação ou preview environments ajudam a validar mudanças antes da publicação final.</p><p>Em aplicações modernas, especialmente com GitHub Actions, Vercel, Netlify, Kubernetes ou plataformas cloud, é comum criar ambientes temporários para cada pull request. Isso permite que o time visualize a mudança funcionando antes de aprovar.</p><p>Esse tipo de prática reduz ruído.</p><p>Produto consegue validar. QA consegue testar. Desenvolvedores conseguem revisar comportamento. O negócio consegue enxergar a entrega antes que ela chegue ao usuário final.</p><p>Mas é preciso tomar cuidado para não transformar ambientes em uma fonte de inconsistência. Se homologação é muito diferente de produção, ela perde valor. Se staging não usa configurações parecidas, os testes podem enganar. Se dados são irreais demais, cenários importantes podem não aparecer.</p><p>Ambientes precisam ser suficientemente parecidos para gerar confiança.</p><p>Esse é outro ponto onde CI/CD ajuda, porque infraestrutura, variáveis, versões e deploys podem ser padronizados.</p><p>Quando a empresa trabalha com containers, por exemplo, a mesma imagem pode ser promovida entre ambientes. Isso reduz o risco de “funcionou em homologação, mas quebrou em produção” por diferença de build.</p><p>Essa ideia de promover artefatos é importante.</p><p>Em vez de reconstruir a aplicação várias vezes, o pipeline gera um artefato versionado e promove esse artefato entre ambientes. O que foi testado é o que será publicado. Isso aumenta rastreabilidade e reduz variação.</p><p>CI/CD bem feito cria esse tipo de consistência.</p><p>Sem isso, cada ambiente vira uma interpretação diferente do sistema.</p><p>E interpretação diferente gera surpresa.</p><p>A surpresa pode até ser interessante em produto, mas em deploy geralmente é problema.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ufVC9SrqJzq5jn_XktJVCQ.jpeg" /><figcaption>CI/CD maduro funciona como uma missão espacial: cada etapa valida o mesmo artefato antes do lançamento final.</figcaption></figure><p>Outro cuidado importante é com segredos e variáveis de ambiente. Um pipeline não pode expor senhas, tokens, chaves privadas ou credenciais sensíveis. Secrets precisam ser armazenados em locais apropriados, com controle de acesso, rotação e uso restrito.</p><p>Esse é um ponto onde muitos processos falham.</p><p>Às vezes, a empresa automatiza deploy, mas deixa credenciais expostas em arquivos, logs ou variáveis mal protegidas. Isso transforma o pipeline em risco de segurança.</p><p>CI/CD precisa ser confiável também do ponto de vista de proteção de dados e acesso.</p><p>O ideal é que cada ambiente tenha seus próprios secrets, que permissões sejam mínimas e que o pipeline execute apenas o que precisa executar. Contas de serviço devem seguir o princípio do menor privilégio. Logs do pipeline não devem exibir informações sensíveis. Aprovações devem existir quando o risco justificar.</p><p>Maturidade de pipeline também envolve governança.</p><p>Em ambientes mais controlados, pode fazer sentido exigir revisão em pull requests, bloquear merge se testes falharem, exigir aprovação para produção, registrar quem aprovou e manter histórico de deploys. Isso não precisa ser burocrático. Pode ser parte natural do fluxo.</p><p>O importante é que a empresa saiba responder perguntas básicas.</p><p>O que foi publicado? Quando foi publicado? Quem aprovou? Quais validações passaram? Qual versão está em produção? Como voltar se der errado?</p><p>Se a empresa não consegue responder isso rapidamente, ela ainda não possui controle real sobre a entrega.</p><p>Existe também uma relação direta entre CI/CD e arquitetura. Sistemas monolíticos podem ter pipelines eficientes. Microserviços podem ter pipelines ruins. A arquitetura por si só não garante maturidade de entrega.</p><p>Em um monólito bem organizado, o pipeline pode rodar testes, gerar build, publicar uma versão e manter rastreabilidade. Em microserviços desorganizados, cada serviço pode ter um processo diferente, dependências mal definidas e deploys difíceis de coordenar.</p><p>O problema não é monólito ou microserviço.</p><p>O problema é falta de padrão.</p><p>Quando a empresa começa a ter muitos serviços, padronizar pipelines se torna ainda mais importante. Caso contrário, cada time cria sua própria forma de publicar, validar e operar. Isso gera inconsistência e dificulta governança.</p><p>Uma boa prática é criar templates reutilizáveis de pipeline. Assim, serviços diferentes seguem uma base comum, mas ainda podem adaptar detalhes conforme sua necessidade.</p><p>Isso reduz manutenção e melhora previsibilidade.</p><p>Também ajuda novos desenvolvedores, porque eles não precisam aprender um processo completamente diferente para cada projeto.</p><p>Em empresas que escalam, padronização não é inimiga da flexibilidade. Pelo contrário. Ela cria uma base comum para que a flexibilidade aconteça com menos risco.</p><p>No fim, CI/CD é a disciplina de transformar entrega em processo.</p><p>E essa disciplina muda a forma como a empresa evolui.</p><p>Sem CI/CD, cada deploy depende de uma combinação de cuidado, memória, disponibilidade de pessoas e sorte. Com CI/CD, a entrega passa a depender de um fluxo validado, repetível e rastreável.</p><p>Isso não elimina todos os problemas. Nenhum processo elimina. Mas reduz drasticamente a chance de falhas previsíveis chegarem em produção.</p><p>Também reduz medo.</p><p>E medo é um dos maiores inimigos da evolução de sistemas.</p><p>Quando o time tem medo de publicar, a empresa desacelera. Quando a empresa desacelera, o produto perde capacidade de resposta. Quando o produto perde capacidade de resposta, o negócio perde competitividade.</p><p>Por isso, deploy não deveria ser um evento traumático.</p><p>Deveria ser uma consequência natural de um processo confiável.</p><p>Se cada deploy exige tensão, reunião emergencial, validação manual excessiva e esperança, talvez o problema não esteja apenas no código. Talvez esteja na ausência de uma estrutura real de entrega.</p><p>Empresas que querem escalar precisam entender isso cedo.</p><p>Não basta escrever código. Não basta ter bons desenvolvedores. Não basta ter cloud, Kubernetes, microserviços ou ferramentas modernas. Se a entrega continua manual, imprevisível e frágil, o sistema ainda carrega um risco estrutural.</p><p>CI/CD não é luxo. Não é detalhe. Não é apenas coisa de empresa grande.</p><p>É uma das primeiras evidências de que a engenharia está sendo tratada com seriedade.</p><p>Porque software que cresce sem processo de entrega não escala de forma saudável.</p><p>Ele apenas aumenta a distância entre mudança e controle.</p><p>E quanto maior essa distância, maior o risco.</p><p>A pergunta que fica é simples: seu deploy é um processo confiável ou ainda é um momento de tensão disfarçado de rotina? 🚀</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0284239b4836" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[If You Don’t Know What Is Happening in Your System, You Are Not in Control]]></title>
            <link>https://medium.com/@patrickotto.dev/if-you-dont-know-what-is-happening-in-your-system-you-are-not-in-control-4968a296fe8f?source=rss-daa0cab73ef2------2</link>
            <guid isPermaLink="false">https://medium.com/p/4968a296fe8f</guid>
            <dc:creator><![CDATA[Patrick Otto]]></dc:creator>
            <pubDate>Fri, 15 May 2026 13:43:25 GMT</pubDate>
            <atom:updated>2026-05-15T13:43:25.534Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*mbs_SX9Dh-h30L3UVQ4TPQ.png" /></figure><blockquote>Isolated logs may show that something failed, but only observability allows you to understand the real behavior of a system in production.</blockquote><p>There is an important difference between knowing that a system failed and understanding why it failed. This difference may seem small when the application is simple, but it becomes massive as the system grows, gains new integrations, processes more data, and starts depending on multiple services working at the same time.</p><p>In a small system, the relationship between cause and effect is usually direct. An error appears on a screen, the developer opens the log, finds the exception, understands the problem, and fixes it. This flow is simple because the path traveled by the request is also simple. The user performs an action, the backend processes it, the database responds, and the result returns to the interface.</p><p>For a while, this works.</p><p>The problem is that real systems rarely remain simple when the business starts to grow. New features are added, external integrations enter the flow, asynchronous routines start running in the background, queues begin processing events, different databases are queried, and multiple services start communicating with each other.</p><p>At that point, the system stops being a straight line.</p><p>It becomes a network.</p><p>And when a network starts having problems, looking at only one isolated point is not enough to understand the whole.</p><p>This is one of the reasons why many companies believe they have control over their systems when, in reality, they only have scattered records of events. There is a log. There may be a CPU and memory dashboard. There may even be an error alert. But there is no context. There is no correlation. There is no clear way to understand the complete path of an operation.</p><p>This is where observability comes in.</p><p>Observability is not just having logs. It is not just having metrics either. And it is not simply installing a nice-looking tool with modern dashboards. Observability is the ability to understand the internal behavior of a system based on the signals it emits in production.</p><p>This definition matters because it changes the focus of the discussion.</p><p>The goal is not only to know that something went wrong. The goal is to understand what is happening, where it is happening, why it is happening, and what impact it has on the rest of the system.</p><p>This difference is fundamental for companies that are scaling.</p><p>Imagine an order system. The user completes a purchase, the backend creates the order, validates stock, processes payment, publishes an event to a queue, sends an email, updates reports, and notifies an external integration. This flow may involve an API, database, cache, queue, worker, payment service, email service, and third-party integration.</p><p>If something fails, where is the problem?</p><p>It could be in the main endpoint. It could be in the database. It could be in the queue. It could be in the worker. It could be in the external API. It could be intermittent slowness. It could be a business rule that started consuming more time after a change. It could be a service that works well for most of the day but fails under load.</p><p>Without observability, the team tries to figure this out manually.</p><p>They open logs from one service. Then another. They compare timestamps. They search for error messages. They try to reproduce the behavior. They add new logs. They deploy again. They wait for it to happen again. And while all of this is happening, the system continues impacting users or accumulating failures in the background.</p><p>This is one of the invisible costs of lacking observability: the time lost trying to understand the problem.</p><p>And time in production is expensive.</p><p>Not only because of the technical team’s cost, but because of the impact on the customer, the operation, and the business’s trust. A poorly understood incident tends to take longer to resolve. An intermittent problem without traceability tends to reappear. A failure without context tends to generate superficial fixes.</p><p>Knowing that there was an error is very different from understanding the behavior that led to that error.</p><p>Consider a simple example in Node.js:</p><pre>app.get(&#39;/checkout/:id&#39;, async (req, res) =&gt; {<br>  const order = await getOrder(req.params.id);<br>  const customer = await getCustomer(order.customerId);<br>  const payment = await getPaymentStatus(order.paymentId);<br>  const shipping = await calculateShipping(order.address);<br>  res.json({<br>    order,<br>    customer,<br>    payment,<br>    shipping<br>  });<br>});</pre><p>This code looks simple. It fetches an order, loads customer data, checks the payment status, calculates shipping, and returns a response. In a small application, this might work without major issues.</p><p>But in production, each one of these functions may hide a different dependency. getOrder may query a database under heavy load. getCustomer may call another internal service. getPaymentStatus may depend on an external API. calculateShipping may use a logistics service that becomes slow at certain times of the day.</p><p>If this endpoint starts taking five seconds to respond, what is the cause?</p><p>A traditional log may only say that the request took too long. In some cases, it may register a timeout. But that does not answer the main question. Is the problem in the database? In the customer service? In the payment gateway? In the shipping calculation? In all of them at the same time? Or in none of them individually, but in the sum of their latencies?</p><p>Without context, the team guesses.</p><p>With observability, the team investigates.</p><p>That difference completely changes the operation.</p><p>Observability usually relies on three major signals: metrics, logs, and traces. Each one has a different role, and the real value appears when they are analyzed together.</p><p>Metrics help understand the general state of the system over time. They show quantitative behavior: average response time, error rate, CPU usage, memory usage, number of messages in a queue, throughput, requests per minute, latency by endpoint, among other indicators. Metrics are useful for identifying trends and noticing when something has moved away from the expected pattern.</p><p>Logs record specific events. They show messages, errors, exceptions, contextual data, and useful information for punctual analysis. A good log helps explain what happened at a given moment, as long as it is well structured and contains relevant information.</p><p>Traces show the path traveled by a request or operation across different parts of the system. In distributed architectures, this is one of the most important elements because it allows the team to follow the complete journey of a call.</p><p>A trace may look like this:</p><pre>Client<br>  → API Gateway: 45ms<br>    → Auth Service: 30ms<br>    → Order Service: 120ms<br>      → Database: 80ms<br>      → Payment Service: 2,400ms<br>        → External Provider: 2,200ms<br>    → Response: 2,610ms</pre><p>In this example, it becomes clear that the bottleneck is not in the gateway, not in the order service, and not in the database. Most of the time is being spent in the payment service and, more specifically, in the external provider. This visibility completely changes how the team reacts to the problem.</p><p>Without a trace, someone might try to optimize the main endpoint. Someone might increase infrastructure. Someone might add cache where it is not needed. Someone might investigate the database unnecessarily. With a trace, the discussion stops being based on opinion and starts being based on evidence.</p><p>That is operational maturity.</p><p>Tools such as Datadog, Grafana, and New Relic offer this type of visibility in an integrated way, combining APM, logs, metrics, traces, alerts, and dashboards in a single platform. They are often good options for companies that need to accelerate observability adoption without building the entire infrastructure from scratch.</p><p>Grafana, Prometheus, Loki, Tempo, and Jaeger follow a more flexible and composable approach. Prometheus is widely used for metrics collection. Grafana is strong in visualization. Loki helps with logs. Jaeger and Tempo are used for distributed tracing. This approach usually offers more control, but also requires more technical maturity to configure, maintain, and evolve.</p><p>OpenTelemetry is becoming increasingly important in this scenario. It is not exactly a final visualization tool, but an open standard for instrumenting applications and collecting signals such as traces, metrics, and logs. The main advantage is reducing vendor lock-in and allowing the company to send data to different platforms.</p><p>In practical terms, instrumenting an application means making it emit useful signals about its own behavior.</p><p>A simplified example with OpenTelemetry in Node.js could look like this:</p><pre>const { trace } = require(&#39;@opentelemetry/api&#39;);<br>const tracer = trace.getTracer(&#39;checkout-service&#39;);<br>async function processCheckout(orderId) {<br>  return await tracer.startActiveSpan(&#39;processCheckout&#39;, async (span) =&gt; {<br>    try {<br>      span.setAttribute(&#39;order.id&#39;, orderId);<br>      const order = await getOrder(orderId);<br>      const payment = await processPayment(order);<br>      const shipping = await calculateShipping(order);<br>      span.setAttribute(&#39;payment.status&#39;, payment.status);<br>      return {<br>        order,<br>        payment,<br>        shipping<br>      };<br>    } catch (error) {<br>      span.recordException(error);<br>      span.setStatus({ code: 2, message: error.message });<br>      throw error;<br>    } finally {<br>      span.end();<br>    }<br>  });<br>}</pre><p>This type of instrumentation helps transform a common operation into something observable. The system starts emitting information about what it is doing, how long it is taking, which attributes matter, and which errors occurred.</p><p>This does not replace good logs or good metrics. But it creates a layer of context that isolated logs cannot provide.</p><p>And context is what separates observability from simple monitoring.</p><p>Traditional monitoring usually answers known questions. Did CPU go above 80%? Is memory usage high? Did the endpoint return a 500 error? Is the server down? These questions are important, but limited. They assume that you already know what you need to observe.</p><p>Observability allows you to investigate questions you did not yet know you would need to ask.</p><p>Why is checkout slow only for some users? Why does the queue grow only at a specific time? Why does the integration fail only when order volume is high? Why did response time increase after an apparently simple change? Why is an asynchronous routine delayed without generating an explicit error?</p><p>These questions are common in real systems.</p><p>And they are hard to answer with scattered logs alone.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*t22HZf_IJLsJd2bnxhbw2Q.png" /><figcaption>Observability transforms scattered signals into context, allowing the team to understand the real behavior of the system in production.</figcaption></figure><p>Observability transforms scattered signals into context so the team can understand the real behavior of the system in production.</p><p>The lack of observability also creates a cultural problem. When the team does not have clear data, technical discussions start depending on perceptions. One developer thinks the problem is in the database. Another believes it is in the external API. Someone suggests increasing server capacity. Someone else argues for refactoring the service. Without evidence, the conversation becomes a dispute of opinions.</p><p>In mature environments, data reduces noise.</p><p>Observability does not eliminate the need for technical experience, but it directs that experience. A good engineer remains essential to interpret signals, raise hypotheses, and make decisions. The difference is that they are no longer working in the dark.</p><p>This is especially true for asynchronous processes.</p><p>When an operation is sent to a queue, it leaves the main flow. This improves resilience, but also increases the need for visibility. If a message gets stuck, fails, is retried, or takes longer than expected, someone needs to know.</p><p>Without observability, the system may look healthy at the interface while accumulating problems in the background.</p><p>The user receives a positive response, but the email was not sent. The order was created, but the external integration was not notified. The transaction was recorded, but the report was not updated. The queue is growing, but no one notices until the delay becomes an incident.</p><p>This is one of the most dangerous risks of poorly monitored modern architectures: silent failure.</p><p>It does not explode immediately. It accumulates.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*8QBcMnCTr_I5culaZyUdlA.jpeg" /><figcaption>Not every failure appears to the user immediately. Some accumulate silently until they become an incident.</figcaption></figure><p>Not every failure appears to the user immediately. Some accumulate silently until they become an incident.</p><p>Observability should also not be confused with excessive information. There is a big difference between collecting a lot of data and collecting useful data. A system can generate thousands of logs per minute and still be difficult to understand. It can have dozens of dashboards and none of them answer the important questions. It can have alerts for everything and, precisely because of that, make the team ignore alerts.</p><p>This is known as alert fatigue.</p><p>When everything alerts, nothing alerts.</p><p>A good observability system needs to be designed with intention. Metrics must reflect relevant behavior. Logs need structure and context. Traces must follow critical flows. Alerts must indicate situations that require real action.</p><p>Alerting because CPU went up for a few seconds may not make sense. Alerting because the checkout error rate increased, because the payment queue is accumulating, or because login latency passed an acceptable threshold can be much more relevant.</p><p>The question should not be only “what can we measure?”. The question should be “what do we need to understand in order to operate the system better?”.</p><p>This change in question is essential.</p><p>In many systems, four signals are a good starting point: latency, traffic, errors, and saturation. Latency shows how long operations take. Traffic shows how much the system is being used. Errors show explicit failures. Saturation shows how close resources are to their limits.</p><p>These signals do not solve everything, but they offer an important base for understanding system health.</p><p>In backend applications, this may mean tracking response time by endpoint, error rate by service, slow database queries, connection usage, CPU and memory consumption, queue size, and average worker processing time.</p><p>In frontend applications, observability also matters. Many companies forget that the user experience starts in the browser or in the application. Metrics such as loading time, JavaScript errors, API failures, device-specific performance, Core Web Vitals, and real user behavior help understand problems that the backend alone may never see.</p><p>Tools such as Sentry, LogRocket, Datadog RUM, New Relic Browser, and OpenTelemetry for frontend can help at this point. They allow teams to identify interface errors, slowness in specific screens, and real impacts on the user experience.</p><p>This matters because not every production problem lives on the server.</p><p>Sometimes the API is healthy, but the frontend broke a critical journey. Sometimes the backend responds quickly, but the user suffers because of a heavy screen. Sometimes the error happens only in a specific browser, operating system, or app version.</p><p>Without observability in the frontend, the company sees only part of reality.</p><p>And half a reality can lead to wrong decisions.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*CWElXt7f6gn7eIrHnSpWmQ.png" /><figcaption>The user experience depends on both ends: what happens in the interface and what happens in the infrastructure behind it.</figcaption></figure><p>The user experience depends on both ends: what happens in the interface and what happens in the infrastructure behind it.</p><p>Another important point is the relationship between observability and business. Systems do not exist just to return status 200. They exist to support real processes. Sales, payments, registrations, customer service, proposals, queries, integrations, reports, internal operations.</p><p>For that reason, good observability should not look only at infrastructure. It should also look at critical journeys.</p><p>How many checkouts failed? How many orders remained pending? How many proposals were not processed? How many integrations were delayed? How many users could not complete registration? Which step of the funnel has the highest error rate?</p><p>These questions connect engineering to business.</p><p>And when engineering can demonstrate impact clearly, the conversation changes level.</p><p>The team stops talking only about CPU, memory, and 500 errors. It starts talking about conversion, operational stability, avoided loss, recovery time, reliability, and user experience.</p><p>This is one of the great strengths of observability: it brings technology and business closer together.</p><p>In companies that are scaling, this is decisive. The larger the system, the harder it becomes to make decisions based only on perception. It is necessary to understand where the bottlenecks are, which failures have the greatest impact, which services need priority, and which problems truly affect the user.</p><p>Without observability, everything seems urgent.</p><p>With observability, it becomes easier to prioritize.</p><p>This also affects how incidents are handled. In immature companies, incidents are usually chaotic. Many people join a call, each one looks at a different part, hypotheses appear one after another, and decisions are made under pressure.</p><p>In more mature companies, observability does not eliminate pressure, but it reduces disorder. The team can quickly identify the scope of the problem, understand which users were impacted, locate the responsible service, and follow the recovery.</p><p>After the incident, the data also helps with the postmortem analysis. Not to look for blame, but to understand behavior, identify process failures, and improve the system.</p><p>This is another important difference.</p><p>Observability should not be used as a tool to monitor people. It should be used as a tool to learn about systems.</p><p>The focus is not “who caused the problem?”. The focus is “why did the system allow this problem to have this impact?”.</p><p>This mindset changes engineering culture.</p><p>It takes the team out of defensive mode and puts the company in a posture of continuous improvement.</p><p>It is also important to recognize that observability has a cost. Collecting, storing, and processing data can become expensive, especially in high-volume systems. Managed tools offer a lot of convenience, but they need to be configured carefully to avoid unnecessary costs. Self-managed stacks offer control, but require operation.</p><p>That is why observability also needs strategy.</p><p>It is not about recording everything forever. It is about defining what matters, how long to keep it, what level of detail makes sense, and which signals truly help decision-making.</p><p>Debug logs in production without control can generate cost and noise. Tracing 100% of requests may be unnecessary in some scenarios. Metrics without standardization can make analysis difficult. The balance is collecting enough information to understand the system without turning observability into a problem of its own.</p><p>This maturity comes with time.</p><p>The important thing is to start with what sustains the operation.</p><p>Critical flows first. Most important services first. Journeys that impact users and revenue first. Then coverage evolves.</p><p>In a company that depends on checkout, observe checkout. In a company that depends on onboarding, observe onboarding. In a company that depends on integrations, observe integrations. In a company that depends on asynchronous processing, observe queues and workers.</p><p>It sounds obvious, but many companies start with what is easy to measure, not with what is important to understand.</p><p>This inversion reduces the value of observability.</p><p>In the end, observability is a form of responsibility. It means recognizing that putting a system into production without the ability to understand its behavior is accepting an unnecessary risk. It means accepting that, in real environments, failures will happen, integrations will fluctuate, users will find unexpected paths, and changes will generate side effects.</p><p>The question is not whether something will fail.</p><p>The question is how long your company will take to notice, understand, and fix it.</p><p>Serious systems do not depend only on hope. They emit signals. They are monitored. They allow investigation. They provide context for decisions.</p><p>Without that, every stage of growth increases the distance between what the company thinks is happening and what is actually happening.</p><p>And that distance is dangerous.</p><p>Because a system can look healthy from the outside while accumulating problems inside. It can respond to part of the requests and fail in critical flows. It can have active servers and still deliver a poor experience. It can record logs and still offer no understanding.</p><p>Observability exists to reduce that distance.</p><p>It does not solve every problem, but it allows teams to see problems clearly. It does not prevent every failure, but it reduces reaction time. It does not replace architecture, tests, or good deployment processes, but it strengthens all of these pillars.</p><p>Without observability, tests say whether something should work. CI/CD delivers changes consistently. Asynchronous processes distribute responsibilities. But when all of this reaches production, one essential question remains: is the system actually behaving as expected?</p><p>If you cannot answer that, you are not controlling the system.</p><p>You are only hoping it is working.</p><p>And hope is not a scaling strategy.</p><p>The question that remains is simple: does your company understand the real behavior of the system in production, or does it still depend on isolated logs to discover what happened? 🔭</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4968a296fe8f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Se você não sabe o que acontece no seu sistema, você não controla nada]]></title>
            <link>https://medium.com/@patrickotto.dev/se-voc%C3%AA-n%C3%A3o-sabe-o-que-acontece-no-seu-sistema-voc%C3%AA-n%C3%A3o-controla-nada-becd2ec1ccc6?source=rss-daa0cab73ef2------2</link>
            <guid isPermaLink="false">https://medium.com/p/becd2ec1ccc6</guid>
            <category><![CDATA[observability]]></category>
            <dc:creator><![CDATA[Patrick Otto]]></dc:creator>
            <pubDate>Mon, 11 May 2026 18:45:02 GMT</pubDate>
            <atom:updated>2026-05-11T19:14:09.248Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7zQyCMPLYjaIYAGRqKMKEg.png" /></figure><blockquote>Logs isolados podem mostrar que algo falhou, mas somente observabilidade permite entender o comportamento real do sistema em produção.</blockquote><p>Existe uma diferença importante entre saber que um sistema falhou e entender por que ele falhou. Essa diferença parece pequena quando a aplicação é simples, mas se torna enorme conforme o sistema cresce, ganha novas integrações, passa a processar mais dados e começa a depender de múltiplos serviços funcionando ao mesmo tempo.</p><p>Em um sistema pequeno, a relação entre causa e efeito costuma ser direta. Um erro aparece em uma tela, o desenvolvedor abre o log, encontra a exceção, entende o problema e corrige. Esse fluxo é simples porque o caminho percorrido pela requisição também é simples. O usuário faz uma ação, o backend processa, o banco responde e o resultado volta para a interface.</p><p>Durante um tempo, isso funciona.</p><p>O problema é que sistemas reais raramente permanecem simples quando o negócio começa a crescer. Novas funcionalidades são adicionadas, integrações externas entram no fluxo, rotinas assíncronas passam a ser executadas em segundo plano, filas começam a processar eventos, bancos diferentes passam a ser consultados e múltiplos serviços começam a conversar entre si.</p><p>Nesse momento, o sistema deixa de ser uma linha reta.</p><p>Ele passa a ser uma rede.</p><p>E quando uma rede começa a apresentar problemas, olhar apenas para um ponto isolado não é suficiente para entender o todo.</p><p>Essa é uma das razões pelas quais muitas empresas acreditam que têm controle sobre seus sistemas, quando na verdade têm apenas registros soltos de eventos. Existe log. Existe algum painel de CPU e memória. Existe talvez um alerta de erro. Mas falta contexto. Falta correlação. Falta uma forma clara de entender o caminho completo de uma operação.</p><p>É aqui que entra a observabilidade.</p><p>Observabilidade não é apenas ter logs. Também não é apenas ter métricas. E não é simplesmente instalar uma ferramenta bonita com dashboards modernos. Observabilidade é a capacidade de compreender o comportamento interno de um sistema a partir dos sinais que ele emite em produção.</p><p>Essa definição é importante porque muda o foco da discussão.</p><p>O objetivo não é apenas saber que algo deu errado. O objetivo é entender o que está acontecendo, onde está acontecendo, por que está acontecendo e qual impacto isso gera no restante do sistema.</p><p>Essa diferença é fundamental para empresas que estão escalando.</p><p>Imagine um sistema de pedidos. O usuário finaliza uma compra, o backend cria o pedido, valida estoque, processa pagamento, publica um evento em uma fila, envia e-mail, atualiza relatórios e notifica uma integração externa. Esse fluxo pode envolver API, banco de dados, cache, fila, worker, serviço de pagamento, serviço de e-mail e integração de terceiros.</p><p>Se algo falhar, onde está o problema?</p><p>Pode estar no endpoint principal. Pode estar no banco. Pode estar na fila. Pode estar no worker. Pode estar na API externa. Pode estar em uma lentidão intermitente. Pode estar em uma regra de negócio que passou a consumir mais tempo depois de uma alteração. Pode estar em um serviço que funciona bem na maior parte do dia, mas falha sob carga.</p><p>Sem observabilidade, o time tenta descobrir isso manualmente.</p><p>Abre logs de um serviço. Depois de outro. Compara horários. Procura mensagens de erro. Tenta reproduzir o comportamento. Adiciona novos logs. Faz um novo deploy. Espera acontecer de novo. E, enquanto isso, o sistema continua impactando usuários ou acumulando falhas em segundo plano.</p><p>Esse é um dos custos invisíveis da falta de observabilidade: o tempo perdido para entender o problema.</p><p>E tempo, em produção, é caro.</p><p>Não apenas pelo custo da equipe técnica, mas pelo impacto no cliente, na operação e na confiança do negócio. Um incidente mal compreendido tende a demorar mais para ser resolvido. Um problema intermitente sem rastreabilidade tende a reaparecer. Uma falha sem contexto tende a gerar correções superficiais.</p><p>Saber que houve erro é muito diferente de entender o comportamento que levou até o erro.</p><p>Considere um exemplo simples em Node.js:</p><pre>app.get(&#39;/checkout/:id&#39;, async (req, res) =&gt; {<br>  const order = await getOrder(req.params.id);<br>  const customer = await getCustomer(order.customerId);<br>  const payment = await getPaymentStatus(order.paymentId);<br>  const shipping = await calculateShipping(order.address);<br>  res.json({<br>    order,<br>    customer,<br>    payment,<br>    shipping<br>  });<br>});</pre><p>Esse código parece simples. Ele busca um pedido, carrega dados do cliente, consulta o status do pagamento, calcula o frete e retorna uma resposta. Em uma aplicação pequena, talvez isso funcione sem grandes problemas.</p><p>Mas, em produção, cada uma dessas funções pode esconder uma dependência diferente. getOrder pode consultar um banco de dados com alta carga. getCustomer pode chamar outro serviço interno. getPaymentStatus pode depender de uma API externa. calculateShipping pode usar um serviço de logística que apresenta lentidão em determinados horários.</p><p>Se esse endpoint começar a demorar cinco segundos para responder, qual é a causa?</p><p>O log tradicional talvez diga apenas que a requisição demorou. Em alguns casos, pode registrar um timeout. Mas isso não responde à pergunta principal. O problema está no banco? No serviço de cliente? No gateway de pagamento? No cálculo de frete? Em todos ao mesmo tempo? Em nenhum deles isoladamente, mas na soma das latências?</p><p>Sem contexto, o time adivinha.</p><p>Com observabilidade, o time investiga.</p><p>Essa diferença muda completamente a operação.</p><p>A observabilidade normalmente se apoia em três grandes sinais: métricas, logs e traces. Cada um tem uma função diferente, e o valor real aparece quando eles são analisados em conjunto.</p><p>Métricas ajudam a entender o estado geral do sistema ao longo do tempo. Elas mostram comportamento quantitativo: tempo médio de resposta, taxa de erro, consumo de CPU, uso de memória, quantidade de mensagens em fila, throughput, número de requisições por minuto, latência por endpoint, entre outros indicadores. Métricas são úteis para identificar tendências e perceber quando algo saiu do padrão.</p><p>Logs registram eventos específicos. Eles mostram mensagens, erros, exceções, dados de contexto e informações úteis para análise pontual. Um bom log ajuda a explicar o que aconteceu em determinado momento, desde que esteja bem estruturado e contenha informações relevantes.</p><p>Traces mostram o caminho percorrido por uma requisição ou operação entre diferentes partes do sistema. Em arquiteturas distribuídas, esse é um dos elementos mais importantes, porque permite acompanhar a jornada completa de uma chamada.</p><p>Um trace pode mostrar algo como:</p><pre>Client<br>  → API Gateway: 45ms<br>    → Auth Service: 30ms<br>    → Order Service: 120ms<br>      → Database: 80ms<br>      → Payment Service: 2.400ms<br>        → External Provider: 2.200ms<br>    → Response: 2.610ms</pre><p>Nesse exemplo, fica claro que o gargalo não está no gateway, nem no serviço de pedidos, nem no banco. A maior parte do tempo está no serviço de pagamento e, mais especificamente, no provedor externo. Essa visibilidade muda completamente a forma como o time reage ao problema.</p><p>Sem trace, talvez alguém tentasse otimizar o endpoint principal. Talvez aumentasse infraestrutura. Talvez criasse cache onde não precisava. Talvez investigasse o banco sem necessidade. Com trace, a discussão deixa de ser baseada em opinião e passa a ser baseada em evidência.</p><p>Isso é maturidade operacional.</p><p>Ferramentas como Datadog, Grafana e New Relic oferecem essa visão de forma integrada, combinando APM, logs, métricas, traces, alertas e dashboards em uma única plataforma. Elas costumam ser boas opções para empresas que precisam acelerar a adoção de observabilidade sem montar toda a infraestrutura do zero.</p><p>Grafana, Prometheus, Loki, Tempo e Jaeger seguem uma linha mais flexível e componível. Prometheus é muito usado para coleta de métricas. Grafana é forte em visualização. Loki ajuda com logs. Jaeger e Tempo são usados para tracing distribuído. Essa abordagem costuma oferecer mais controle, mas também exige mais maturidade técnica para configurar, manter e evoluir.</p><p>OpenTelemetry entra como um ponto cada vez mais importante nesse cenário. Ele não é exatamente uma ferramenta final de visualização, mas um padrão aberto para instrumentar aplicações e coletar sinais como traces, métricas e logs. A grande vantagem é reduzir dependência de fornecedor e permitir que a empresa envie dados para diferentes plataformas.</p><p>Em termos práticos, instrumentar uma aplicação significa fazer com que ela emita sinais úteis sobre o próprio comportamento.</p><p>Um exemplo simplificado com OpenTelemetry em Node.js poderia seguir uma linha como esta:</p><pre>const { trace } = require(&#39;@opentelemetry/api&#39;);<br><br>const tracer = trace.getTracer(&#39;checkout-service&#39;);<br>async function processCheckout(orderId) {<br>  return await tracer.startActiveSpan(&#39;processCheckout&#39;, async (span) =&gt; {<br>    try {<br>      span.setAttribute(&#39;order.id&#39;, orderId);<br>      const order = await getOrder(orderId);<br>      const payment = await processPayment(order);<br>      const shipping = await calculateShipping(order);<br>      span.setAttribute(&#39;payment.status&#39;, payment.status);<br>      return {<br>        order,<br>        payment,<br>        shipping<br>      };<br>    } catch (error) {<br>      span.recordException(error);<br>      span.setStatus({ code: 2, message: error.message });<br>      throw error;<br>    } finally {<br>      span.end();<br>    }<br>  });<br>}</pre><p>Esse tipo de instrumentação ajuda a transformar uma operação comum em algo observável. O sistema passa a emitir informações sobre o que está fazendo, quanto tempo está levando, quais atributos são importantes e quais erros ocorreram.</p><p>Isso não substitui bons logs, nem boas métricas. Mas cria uma camada de contexto que logs isolados não conseguem oferecer.</p><p>E contexto é o que separa observabilidade de simples monitoramento.</p><p>Monitoramento tradicional geralmente responde a perguntas conhecidas. A CPU passou de 80%? A memória está alta? O endpoint retornou erro 500? O servidor está fora do ar? Essas perguntas são importantes, mas são limitadas. Elas partem do princípio de que você já sabe o que precisa observar.</p><p>Observabilidade permite investigar perguntas que você ainda não sabia que precisaria fazer.</p><p>Por que o checkout está lento apenas para alguns usuários? Por que a fila cresce somente em determinado horário? Por que a integração falha apenas quando há alto volume de pedidos? Por que o tempo de resposta aumentou depois de uma alteração aparentemente simples? Por que uma rotina assíncrona está atrasando sem gerar erro explícito?</p><p>Essas perguntas são comuns em sistemas reais.</p><p>E são difíceis de responder apenas com logs soltos.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RGe2mmyj2lk_6k1VxMBWcw.png" /><figcaption>Observabilidade transforma sinais dispersos em contexto para que o time entenda o comportamento real do sistema em produção.</figcaption></figure><p>A falta de observabilidade também cria um problema cultural. Quando o time não possui dados claros, as discussões técnicas passam a depender de percepções. Um desenvolvedor acha que o problema está no banco. Outro acredita que está na API externa. Alguém sugere aumentar servidor. Outro defende refatorar o serviço. Sem evidência, a conversa vira disputa de opinião.</p><p>Em ambientes maduros, dados reduzem ruído.</p><p>A observabilidade não elimina a necessidade de experiência técnica, mas direciona essa experiência. Um bom engenheiro continua sendo fundamental para interpretar os sinais, levantar hipóteses e tomar decisões. A diferença é que ele deixa de trabalhar no escuro.</p><p>Isso vale especialmente para processos assíncronos.</p><p>Quando uma operação é enviada para uma fila, ela sai do fluxo principal. Isso melhora a resiliência, mas também aumenta a necessidade de visibilidade. Se uma mensagem fica parada, falha, é reprocessada ou demora mais do que deveria, alguém precisa saber.</p><p>Sem observabilidade, o sistema pode parecer saudável na interface enquanto acumula problemas em segundo plano.</p><p>O usuário recebe uma resposta positiva, mas o e-mail não foi enviado. O pedido foi criado, mas a integração externa não foi notificada. A transação foi registrada, mas o relatório não foi atualizado. A fila está crescendo, mas ninguém percebe até que o atraso vire incidente.</p><p>Esse é um dos riscos mais perigosos de arquiteturas modernas mal acompanhadas: a falha silenciosa.</p><p>Ela não explode imediatamente. Ela se acumula.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9Cwd_Ym0fhBOmfRLzQCSUw.jpeg" /><figcaption>Nem toda falha aparece para o usuário imediatamente. Algumas se acumulam em silêncio até virarem incidente.</figcaption></figure><p>Observabilidade também não deve ser confundida com excesso de informação. Existe uma diferença grande entre coletar muitos dados e coletar dados úteis. Um sistema pode gerar milhares de logs por minuto e ainda assim ser difícil de entender. Pode ter dezenas de dashboards e nenhum deles responder às perguntas importantes. Pode ter alertas para tudo e, justamente por isso, fazer com que o time ignore alertas.</p><p>Esse é o fenômeno conhecido como fadiga de alerta.</p><p>Quando tudo alerta, nada alerta.</p><p>Um bom sistema de observabilidade precisa ser desenhado com intenção. Métricas precisam refletir comportamento relevante. Logs precisam ter estrutura e contexto. Traces precisam acompanhar fluxos críticos. Alertas precisam indicar situações que exigem ação real.</p><p>Alertar porque a CPU subiu por alguns segundos talvez não faça sentido. Alertar porque a taxa de erro do checkout aumentou, porque a fila de pagamentos está acumulando ou porque a latência do login passou de um limite aceitável pode ser muito mais relevante.</p><p>A pergunta não deve ser apenas “o que podemos medir?”. A pergunta deve ser “o que precisamos entender para operar melhor o sistema?”.</p><p>Essa mudança de pergunta é essencial.</p><p>Em muitos sistemas, existem quatro sinais que ajudam bastante a começar: latência, tráfego, erros e saturação. Latência mostra quanto tempo as operações levam. Tráfego mostra quanto o sistema está sendo utilizado. Erros mostram falhas explícitas. Saturação mostra o quanto os recursos estão próximos do limite.</p><p>Esses sinais não resolvem tudo, mas oferecem uma base importante para entender a saúde do sistema.</p><p>Em aplicações backend, isso pode significar acompanhar tempo de resposta por endpoint, taxa de erro por serviço, consultas lentas no banco, uso de conexões, consumo de CPU e memória, tamanho de filas e tempo médio de processamento de workers.</p><p>Em aplicações frontend, observabilidade também importa. Muitas empresas esquecem que a experiência do usuário começa no navegador ou no aplicativo. Métricas como tempo de carregamento, erros JavaScript, falhas de API, performance por dispositivo, Core Web Vitals e comportamento real do usuário ajudam a entender problemas que o backend talvez nunca enxergue sozinho.</p><p>Ferramentas como Sentry, LogRocket, Datadog RUM, New Relic Browser e OpenTelemetry para frontend podem ajudar nesse ponto. Elas permitem identificar erros na interface, lentidão em telas específicas e impactos reais na experiência do usuário.</p><p>Isso é importante porque nem todo problema de produção está no servidor.</p><p>Às vezes, a API está saudável, mas o frontend quebrou uma jornada crítica. Às vezes, o backend responde rápido, mas o usuário sofre com uma tela pesada. Às vezes, o erro acontece apenas em um navegador, sistema operacional ou versão específica do aplicativo.</p><p>Sem observabilidade no frontend, a empresa enxerga apenas parte da realidade.</p><p>E meia realidade pode levar a decisões erradas.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hOvrlxLsaeldowN7qisFBQ.png" /><figcaption>A experiência do usuário depende das duas pontas: o que acontece na interface e o que acontece na infraestrutura por trás dela.</figcaption></figure><p>Outro ponto importante é a relação entre observabilidade e negócio. Sistemas não existem apenas para retornar status 200. Eles existem para suportar processos reais. Vendas, pagamentos, cadastros, atendimentos, propostas, consultas, integrações, relatórios, operações internas.</p><p>Por isso, uma boa observabilidade não deve olhar apenas para infraestrutura. Deve olhar também para jornadas críticas.</p><p>Quantos checkouts falharam? Quantos pedidos ficaram pendentes? Quantas propostas não foram processadas? Quantas integrações atrasaram? Quantos usuários não conseguiram concluir o cadastro? Qual etapa do funil apresenta mais erro?</p><p>Essas perguntas conectam engenharia ao negócio.</p><p>E quando engenharia consegue demonstrar impacto com clareza, a conversa muda de nível.</p><p>O time deixa de falar apenas em CPU, memória e erro 500. Passa a falar em conversão, estabilidade operacional, perda evitada, tempo de recuperação, confiabilidade e experiência do usuário.</p><p>Essa é uma das grandes forças da observabilidade: ela aproxima tecnologia e negócio.</p><p>Em empresas que estão escalando, isso é decisivo. Quanto maior o sistema, mais difícil fica tomar decisões apenas com base em percepção. É preciso entender onde estão os gargalos, quais falhas têm maior impacto, quais serviços precisam de prioridade e quais problemas realmente afetam o usuário.</p><p>Sem observabilidade, tudo parece urgente.</p><p>Com observabilidade, fica mais fácil priorizar.</p><p>Isso também afeta a forma como incidentes são tratados. Em empresas pouco maduras, incidentes geralmente são caóticos. Muitas pessoas entram em uma chamada, cada uma olha uma parte, hipóteses surgem em sequência e decisões são tomadas sob pressão.</p><p>Em empresas mais maduras, a observabilidade não elimina a pressão, mas reduz a desordem. O time consegue identificar rapidamente o escopo do problema, entender quais usuários foram impactados, localizar o serviço responsável e acompanhar a recuperação.</p><p>Além disso, depois do incidente, os dados ajudam na análise pós-mortem. Não para buscar culpados, mas para entender comportamento, identificar falhas de processo e melhorar o sistema.</p><p>Essa é outra diferença importante.</p><p>Observabilidade não deve ser usada como ferramenta de vigilância sobre pessoas. Ela deve ser usada como ferramenta de aprendizado sobre sistemas.</p><p>O foco não é “quem causou o problema?”. O foco é “por que o sistema permitiu que esse problema tivesse esse impacto?”.</p><p>Essa mentalidade muda a cultura de engenharia.</p><p>Ela tira o time do modo defensivo e coloca a empresa em uma postura de melhoria contínua.</p><p>Também é importante reconhecer que observabilidade tem custo. Coletar, armazenar e processar dados pode ficar caro, especialmente em sistemas com grande volume. Ferramentas gerenciadas oferecem muita facilidade, mas precisam ser configuradas com critério para evitar custos desnecessários. Stacks próprias oferecem controle, mas exigem operação.</p><p>Por isso, observabilidade também precisa de estratégia.</p><p>Não se trata de registrar tudo para sempre. Trata-se de definir o que é importante, por quanto tempo manter, qual nível de detalhe faz sentido e quais sinais realmente ajudam na tomada de decisão.</p><p>Logs de debug em produção sem controle podem gerar custo e ruído. Traces em 100% das requisições podem ser desnecessários em alguns cenários. Métricas sem padronização podem dificultar análise. O equilíbrio está em coletar informação suficiente para entender o sistema sem transformar observabilidade em um problema próprio.</p><p>Essa maturidade vem com o tempo.</p><p>O importante é começar pelo que sustenta a operação.</p><p>Fluxos críticos primeiro. Serviços mais importantes primeiro. Jornadas que impactam usuário e receita primeiro. Depois, a cobertura evolui.</p><p>Em uma empresa que depende de checkout, observe checkout. Em uma empresa que depende de onboarding, observe onboarding. Em uma empresa que depende de integrações, observe integrações. Em uma empresa que depende de processamento assíncrono, observe filas e workers.</p><p>Parece óbvio, mas muitas empresas começam pelo que é fácil medir, não pelo que é importante entender.</p><p>Essa inversão reduz o valor da observabilidade.</p><p>No fim, observabilidade é uma forma de responsabilidade. É reconhecer que colocar um sistema em produção sem capacidade de entender seu comportamento é assumir um risco desnecessário. É aceitar que, em ambientes reais, falhas vão acontecer, integrações vão oscilar, usuários vão encontrar caminhos inesperados e mudanças vão gerar efeitos colaterais.</p><p>A questão não é se algo vai falhar.</p><p>A questão é quanto tempo sua empresa vai levar para perceber, entender e corrigir.</p><p>Sistemas sérios não dependem apenas de esperança. Eles emitem sinais. Eles são acompanhados. Eles permitem investigação. Eles oferecem contexto para decisão.</p><p>Sem isso, qualquer crescimento aumenta a distância entre o que a empresa acha que está acontecendo e o que realmente está acontecendo.</p><p>E essa distância é perigosa.</p><p>Porque um sistema pode parecer saudável por fora e estar acumulando problemas por dentro. Pode responder parte das requisições e falhar em fluxos críticos. Pode ter servidores ativos e ainda assim entregar uma experiência ruim. Pode registrar logs e, mesmo assim, não oferecer entendimento.</p><p>Observabilidade existe para reduzir essa distância.</p><p>Ela não resolve todos os problemas, mas permite enxergá-los com clareza. Não impede todas as falhas, mas reduz o tempo de reação. Não substitui arquitetura, testes ou bons processos de deploy, mas fortalece todos esses pilares.</p><p>Sem observabilidade, testes dizem se algo deveria funcionar. CI/CD entrega mudanças com consistência. Processos assíncronos distribuem responsabilidades. Mas, quando tudo isso chega em produção, ainda resta uma pergunta essencial: o sistema está realmente se comportando como deveria?</p><p>Se você não consegue responder, você não controla o sistema.</p><p>Você apenas espera que ele esteja funcionando.</p><p>E esperança não é estratégia de escala.</p><p>A pergunta que fica é simples: sua empresa entende o comportamento real do sistema em produção ou ainda depende de logs soltos para descobrir o que aconteceu? 🔭</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=becd2ec1ccc6" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[If Everything in Your System Needs to Happen at the Same Time, It Is Already at Risk]]></title>
            <link>https://medium.com/@patrickotto.dev/if-everything-in-your-system-needs-to-happen-at-the-same-time-it-is-already-at-risk-73a49fb4a1b1?source=rss-daa0cab73ef2------2</link>
            <guid isPermaLink="false">https://medium.com/p/73a49fb4a1b1</guid>
            <category><![CDATA[rabbitmq]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[message-queue]]></category>
            <category><![CDATA[kafka]]></category>
            <dc:creator><![CDATA[Patrick Otto]]></dc:creator>
            <pubDate>Fri, 08 May 2026 19:35:49 GMT</pubDate>
            <atom:updated>2026-05-08T19:35:49.201Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FvVjyJSczgSs3diqTFNktg.png" /></figure><blockquote>When every operation depends on another one finishing immediately, the system may still work, but it starts growing with a silent fragility.</blockquote><p>There is a very common idea in software development that growing systems fail because they are not fast enough. At first glance, this explanation seems to make sense. If more users are accessing the application, the system needs to respond faster. If more data is being processed, the infrastructure needs to support more load. If more integrations are added, the backend needs to handle more operations.</p><p>This interpretation is not entirely wrong, but it is incomplete.</p><p>In many cases, the real problem is not speed. It is dependency.</p><p>A system can be fast and still be fragile. It can respond well under normal conditions and still collapse when one part of the flow becomes slow. It can have good infrastructure and still suffer because too many things need to happen at the same time, inside the same request, within the same chain of responsibility.</p><p>This is one of the most important differences between systems that simply grow and systems that are truly prepared to scale.</p><p>At the beginning of a project, synchronous communication is usually the natural choice. A request reaches the backend, the application processes it, talks to the database, perhaps calls an external service, and returns a response. This model is easy to understand, easy to debug, and easy to implement.</p><p>Frameworks such as Express in Node.js, ASP.NET in .NET, Django in Python, Laravel in PHP, Spring Boot in Java, or Ruby on Rails make this flow extremely productive. A developer can create endpoints, implement rules, connect to the database, and deliver value quickly.</p><p>In a small application, this works very well. The flow is clear. The number of dependencies is limited. The response time is predictable. And if something fails, it is usually possible to identify the problem with relative ease.</p><p>The problem is that systems do not remain simple when the business starts to grow.</p><p>New rules appear. New screens are created. New integrations enter the flow. Processes that were once small begin to involve different parts of the application. A registration is no longer just a registration. It starts validating data, consulting external services, generating notifications, updating indicators, sending emails, recording logs, creating history records, recalculating balances, or triggering other routines.</p><p>At first, all of this is usually placed inside the same flow.</p><p>The user clicks a button. The backend receives the request. The system executes all the steps. Only then does it return a response.</p><p>It seems organized. It seems direct. It seems efficient.</p><p>Until the moment it no longer is.</p><p>Imagine, for example, an order system. When a customer completes a purchase, the backend needs to create the order, validate stock, calculate shipping, process payment, issue an invoice, send an email, update the admin dashboard, and notify external integrations.</p><p>In a fully synchronous model, all of these steps may end up happening inside the same operation. The flow might look something like this:</p><pre>app.post(&#39;/orders&#39;, async (req, res) =&gt; {<br>  const order = await createOrder(req.body);<br>  await reserveStock(order);<br>  await processPayment(order);<br>  await issueInvoice(order);<br>  await sendConfirmationEmail(order);<br>  await notifyExternalMarketplace(order);<br>  res.json({<br>    success: true,<br>    orderId: order.id<br>  });<br>});</pre><p>This code is easy to understand. It looks clean, linear, and predictable. But there is a hidden problem in this simplicity: the success of the entire operation depends on every step working within the expected time.</p><p>If stock reservation takes too long, the user waits. If payment is slow, the user waits. If invoice generation fails, the entire order may be compromised. If the email service is down, an operation that should only depend on order creation may fail because of a secondary step. If the integration with an external marketplace is unstable, the checkout may be affected by something that should not block the main user experience.</p><p>This is the point where the system starts showing fragility.</p><p>Not because the code is necessarily wrong, but because different responsibilities were placed inside the same critical flow.</p><p>The question that needs to be asked is simple: does all of this really need to happen before responding to the user?</p><p>In many cases, the answer is no.</p><p>Creating the order may need to happen immediately. Payment validation may also be part of the critical flow. But sending emails, notifying external systems, updating reports, triggering integrations, or processing auxiliary routines can usually happen later.</p><p>This separation completely changes how the system behaves.</p><p>When everything happens synchronously, each step adds time and risk to the main operation. The more dependencies exist inside the flow, the greater the chance that something will fail. And when one step fails, the impact can spread across the entire process.</p><p>It is the domino effect applied to software architecture.</p><p>An external API becomes slow and the whole system starts responding poorly. A third-party service goes down and internal operations are blocked. A heavy routine consumes too many resources and affects users who had nothing to do with that process.</p><p>In moments like these, many companies try to solve the problem by increasing infrastructure. They add more servers, more memory, better databases, replicas, cache, or scale containers. All of this can help, but only up to a point.</p><p>If the main problem is coupling, more infrastructure only buys time.</p><p>It does not fix the structure.</p><p>An overly coupled system remains fragile even when running on larger machines. It may support more volume for a while, but it remains vulnerable to the same type of failure. When dependency is built into the design of the flow, the solution needs to go through architecture.</p><p>This is where asynchronous processes come in.</p><p>Asynchronous processing is the practice of separating what needs to happen immediately from what can happen at another moment, without blocking the system’s main response. Instead of executing every step inside the same request, the application records an event, sends a message to a queue, or publishes a notification so another process can handle that task later.</p><p>The concept is not new, but it has become essential in modern systems.</p><p>Tools such as RabbitMQ, Apache Kafka, AWS SQS, Google Pub/Sub, and Azure Service Bus exist precisely to support this kind of separation. Each one has different characteristics, but they all start from the same central idea: allowing parts of the system to communicate without depending on an immediate response.</p><p>RabbitMQ is widely used for traditional task queues. It works very well when you need to distribute work among consumers, process messages reliably, and manage specific queues. It is common in scenarios such as email sending, image processing, report generation, notifications, integrations, and internal routines.</p><p>Kafka has a different nature. It is stronger in scenarios involving high-volume events, data streaming, and distributed processing. Instead of thinking only in terms of a task queue, Kafka works very well when different consumers need to read events, process information in parallel, and keep event history for a period of time.</p><p>AWS SQS, Google Pub/Sub, and Azure Service Bus bring this logic into managed cloud environments. The advantage is reducing operational effort. Instead of directly managing servers, clusters, or tool maintenance, the company uses a service managed by the cloud provider.</p><p>The choice of tool depends on the context. The mistake is believing that the tool is the main point.</p><p>It is not.</p><p>The main point is the change in mindset.</p><p>Instead of asking “how do I make all of this faster?”, the question becomes “what really needs to happen now and what can happen later?”.</p><p>When asked properly, this question improves the system’s architecture.</p><p>Returning to the order example, the flow could be reorganized like this:</p><pre>app.post(&#39;/orders&#39;, async (req, res) =&gt; {<br>  const order = await createOrder(req.body);<br>  await reserveStock(order);<br>  await processPayment(order);<br>  await queue.publish(&#39;order.created&#39;, {<br>    orderId: order.id,<br>    customerId: order.customerId<br>  });<br>  res.json({<br>    success: true,<br>    orderId: order.id<br>  });<br>});</pre><p>In this model, the main flow handles what is essential to respond to the user. After that, it publishes an event informing that an order was created. From this event, other processes can take responsibility for complementary tasks.</p><p>One worker can issue the invoice. Another can send the email. Another can update reports. Another can notify the marketplace. Each process now has a clearer and more independent responsibility.</p><p>A simple consumer example could look like this:</p><pre>queue.consume(&#39;order.created&#39;, async (message) =&gt; {<br>  const { orderId } = message;<br>  const order = await getOrder(orderId);<br>  await sendConfirmationEmail(order);<br>  await notifyExternalMarketplace(order);<br>});</pre><p>This code represents an important change. Email sending and external notification no longer block order creation. If the email service is unavailable, the order can still be created. If the marketplace is unstable, the message can be retried later. The system no longer depends on everything working at the same time.</p><p>This is the essence of resilience.</p><p>It does not mean ignoring failures. It means preventing a secondary failure from bringing down the main flow.</p><p>This separation also improves the user experience. Instead of waiting for every step to finish, the user receives a faster response about what truly matters at that moment. The rest of the processing happens in the background.</p><p>For the user, the feeling is speed.</p><p>For the system, the gain is stability.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JRV6_7ZJwJGS-SKv7fb6fQ.png" /><figcaption>Asynchronous architectures work like a well-organized production line: each step has its own responsibility, reducing bottlenecks and preventing a localized failure from compromising the entire system.</figcaption></figure><p>There is a simple analogy that helps explain this to non-technical people. Imagine a restaurant. If the waiter had to take the order, cook the food, prepare the drink, wash the dishes, close the bill, and only then return to the table, the service would be slow and fragile. Any problem in one step would block the entire process.</p><p>A restaurant works better because responsibilities are separated. The waiter registers the order, the kitchen prepares it, the cashier handles payment, someone organizes delivery, and other people take care of cleaning. The customer does not need to wait for all internal restaurant routines to finish in order to know that the order was received.</p><p>Asynchronous systems follow a similar logic.</p><p>They do not eliminate work. They organize the work.</p><p>This organization is what allows scale.</p><p>When a system separates responsibilities, it gains the ability to absorb variation. If the email queue grows, you can increase consumers only for that queue. If invoice generation is slow, the rest of the system can continue working. If an external integration becomes unavailable, you can reprocess messages later without blocking the user.</p><p>This creates a more fault-tolerant architecture.</p><p>But it is important to say that asynchronous processes also bring new challenges.</p><p>The first one is eventual consistency. In a synchronous system, the tendency is to expect everything to be updated at the same time. In an asynchronous system, some information may take a few seconds or minutes to be processed. This needs to be understood by the business and reflected in the user experience.</p><p>For example, an order may be created immediately, but the invoice may be issued a few seconds later. A report may not reflect a transaction at the exact moment it occurred. An integration may be processed in the background.</p><p>This is not necessarily a problem. Many times, it is the correct architectural choice. But it needs to be communicated and handled properly.</p><p>The second challenge is idempotency.</p><p>In systems with queues, the same message may be processed more than once in certain scenarios. For that reason, consumers need to be prepared to handle repetition without generating unwanted side effects.</p><p>If a payment worker processes the same message twice, it cannot charge the customer twice. If an email worker receives the same message again, it may need to verify whether that email was already sent. If an external integration is retried, the system must ensure that duplicates will not be created.</p><p>This requires care.</p><p>A simple protection example would be:</p><pre>queue.consume(&#39;invoice.issue&#39;, async (message) =&gt; {<br>  const { orderId } = message;const existingInvoice = await findInvoiceByOrderId(orderId);<br>  if (existingInvoice) {<br>    return;<br>  }<br>  await issueInvoice(orderId);<br>});</pre><p>Here, before issuing an invoice, the system checks whether it already exists. This kind of validation prevents message reprocessing from creating business problems.</p><p>The third challenge is observability.</p><p>When the system stops being linear, understanding what is happening becomes harder. In a synchronous flow, you follow a request from beginning to end. In an asynchronous flow, the process can pass through queues, workers, retries, and different services.</p><p>Without observability, queues become black boxes.</p><p>You need to know how many messages are pending, how many failed, which ones are being retried, how long each task takes, and where the bottlenecks are. Otherwise, the system may look healthy at the interface while silently accumulating problems in the background.</p><p>This is why asynchronous processing and observability need to walk together.</p><p>RabbitMQ, Kafka, SQS, or Pub/Sub solve communication. But the company still needs to monitor behavior, failures, delays, and retries. Tools such as Datadog, New Relic, Grafana, Prometheus, and OpenTelemetry come in at this point to provide visibility into the flow.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hx4WkzXwuNv0ufMno1ZeHw.jpeg" /><figcaption>The interface may look healthy while problems quietly accumulate in the background.</figcaption></figure><p>Another important point is that asynchronous processing should not be used as an excuse for disorder. Putting everything into queues without criteria can create a system that is harder to understand than the original one. The decision needs to be guided by responsibility, criticality, and user impact.</p><p>Not everything needs to be asynchronous.</p><p>Operations that require an immediate response, critical validations, or decisions that must be confirmed at the moment of the request may remain synchronous. The mistake is placing inside the main flow what does not need to be there.</p><p>Good architecture is not architecture that uses queues everywhere. It is architecture that knows where queues make sense.</p><p>This discernment is what separates technical usage from architectural maturity.</p><p>In financial systems, for example, certain validations need to happen immediately. You cannot simply tell the user that a transaction was approved if the critical validation has not yet occurred. On the other hand, notifications, reconciliations, complementary audits, and report updates can be processed asynchronously.</p><p>In e-commerce systems, the order and payment may be part of the main flow, while emails, recommendations, CRM updates, and secondary integrations can go to queues.</p><p>In healthcare systems, the recording of critical information needs to be reliable at the moment of the operation, while notification routines, synchronization, or analytical processing can happen later.</p><p>In real estate, automotive, or marketplace platforms, integrations with external portals, data enrichment, report generation, and notifications often benefit greatly from an asynchronous approach.</p><p>The practical application changes according to the industry, but the principle remains the same.</p><p>Separate the essential from the complementary.</p><p>This separation improves performance, but that is not the only benefit. It may not even be the main one.</p><p>The greatest benefit is reducing the impact of failures.</p><p>When everything is coupled, a small failure can compromise an entire operation. When responsibilities are separated, the failure remains isolated. It can be monitored, retried, and corrected without bringing down the main flow.</p><p>This changes the system’s behavior under pressure.</p><p>And every growing system goes through moments of pressure.</p><p>Traffic spikes, external instability, increased processing, commercial campaigns, integrations going down, rule changes, user base growth. At some point, the system will be tested outside ideal conditions.</p><p>The question is whether it was built to absorb this or whether it depends on everything working perfectly all the time.</p><p>Systems that depend on perfection do not scale well.</p><p>They only work while the environment is favorable.</p><p>When a business begins to grow, the number of unpredictable situations increases. This is the moment when architecture needs to stop being merely functional and start being resilient.</p><p>Asynchronous processes are part of this transition.</p><p>They allow the system to continue operating even when some parts are slow, unavailable, or overloaded. They allow work to be distributed. They allow failures to be retried. They protect the user from steps that do not need to block the experience.</p><p>This kind of architecture shows that the company has understood something important: not everything has the same urgency.</p><p>And when everything is treated as urgent, the system loses its ability to prioritize.</p><p>Maturity is knowing the difference.</p><p>There is also a direct impact on the development team. When responsibilities are separated, the code tends to become more organized. Services start having clearer functions. Workers can be developed, scaled, and monitored independently. The team can evolve parts of the system without touching the entire main flow.</p><p>This makes maintenance easier.</p><p>It also makes team scaling easier.</p><p>When everything is inside the same flow, every change requires extra caution because the impact may be broad. When responsibilities are well separated, the scope of change becomes clearer. This reduces fear, improves predictability, and allows safer evolution.</p><p>There is also a direct connection with automated tests here. The better separated the responsibilities are, the easier it is to test each part. A worker that processes email sending can be tested independently. A queue consumer for payments can have its rules validated with specific tests. An event producer can be verified without requiring the entire system to run together.</p><p>In other words, asynchronous processing is not just infrastructure. It influences architecture, testing, observability, deployment, and operations.</p><p>This is why companies that want to scale cannot treat messaging as a technical detail. It changes how the system thinks, reacts, and evolves.</p><p>But there is an important caution: unnecessary complexity is also a problem.</p><p>Implementing Kafka in a system that only needed a simple queue can create more cost than value. Creating dozens of events without governance can turn the system into a maze. Using messaging without clear standards can generate duplicate messages, inconsistent consumers, and flows that are hard to trace.</p><p>The tool must serve the problem, not technical ego.</p><p>RabbitMQ can be excellent for many queue scenarios. SQS may be enough for AWS-based systems that need operational simplicity. Kafka may be the right choice when there is high event volume, retention needs, and multiple consumers processing data in different ways. Azure Service Bus may make sense in Microsoft-heavy corporate environments. Google Pub/Sub may be natural for architectures running on GCP.</p><p>There is no universal tool.</p><p>There is context.</p><p>And context is what defines good architecture.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*L2O8jDoyJSOmQxJxZdvcjQ.png" /><figcaption>On one side, chained dependency. On the other, independent flows designed to scale.</figcaption></figure><p>In the end, asynchronous processes are not just a way to make the system faster. They are a way to make the system more prepared for reality.</p><p>And the reality is that APIs fail, databases become slow, external services go down, users access systems in spikes, rules change, and integrations behave unpredictably.</p><p>A serious system cannot depend on everything working perfectly at the same time.</p><p>It needs to be designed to keep operating even when parts of the environment fail.</p><p>That is the central point.</p><p>When every operation depends on another one finishing immediately, the system may look stable at a small scale. But that stability is fragile because it depends on a perfect sequence of events.</p><p>As the business grows, that perfect sequence becomes less and less likely.</p><p>That is why the question is not only whether your application is fast today. The question is whether it will remain reliable when volume increases, when an integration fails, when a queue grows, when a service becomes slow, or when an operation needs to be retried.</p><p>Systems that scale are not systems that never face failures. They are systems designed to limit the impact of those failures.</p><p>Asynchronous processes help exactly with that.</p><p>They do not eliminate complexity, but they organize complexity. They do not remove failures, but they reduce propagation. They do not make everything happen immediately, but they allow the system to prioritize what really matters.</p><p>And this prioritization is one of the foundations of scale.</p><p>Without it, the company continues stacking responsibilities into the same flow, until the system becomes slow, fragile, and difficult to evolve.</p><p>At that point, the problem is no longer just technical. It starts affecting operations, user experience, team predictability, and the company’s ability to grow without turning every new demand into a risk.</p><p>That is why asynchronous processes are not an architectural luxury. They are not reserved only for large companies. They are not merely a technical choice for people who enjoy messaging systems.</p><p>They are a practical response to a real problem: growing systems cannot depend on everything happening at the same time.</p><p>If your company still treats every process as synchronous, every integration as blocking, and every operation as immediate, maybe the system is still working.</p><p>But that does not mean it is ready to scale.</p><p>The question that remains is simple: does your system separate what needs to happen now from what can happen later, or is it still allowing growth to depend on everything working at the same time? 🔥</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=73a49fb4a1b1" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Se tudo no seu sistema precisa acontecer ao mesmo tempo, ele já está em risco]]></title>
            <link>https://medium.com/@patrickotto.dev/se-tudo-no-seu-sistema-precisa-acontecer-ao-mesmo-tempo-ele-j%C3%A1-est%C3%A1-em-risco-8eed2225c545?source=rss-daa0cab73ef2------2</link>
            <guid isPermaLink="false">https://medium.com/p/8eed2225c545</guid>
            <category><![CDATA[kafka]]></category>
            <category><![CDATA[arquitetura-de-software]]></category>
            <category><![CDATA[rabbitmq]]></category>
            <category><![CDATA[mensageria]]></category>
            <category><![CDATA[assíncrono]]></category>
            <dc:creator><![CDATA[Patrick Otto]]></dc:creator>
            <pubDate>Fri, 08 May 2026 19:13:28 GMT</pubDate>
            <atom:updated>2026-05-08T19:13:28.120Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FvVjyJSczgSs3diqTFNktg.png" /></figure><blockquote>Quando cada operação depende de outra para terminar imediatamente, o sistema pode até continuar funcionando, mas começa a crescer com uma fragilidade silenciosa.</blockquote><p>Existe uma ideia bastante comum no desenvolvimento de software de que sistemas em crescimento falham porque não são rápidos o suficiente. À primeira vista, essa explicação parece fazer sentido. Se mais usuários estão acessando a aplicação, o sistema precisa responder mais rápido. Se mais dados estão sendo processados, a infraestrutura precisa suportar mais carga. Se mais integrações são adicionadas, o backend precisa lidar com mais operações.</p><p>Essa leitura não está totalmente errada, mas é incompleta.</p><p>Em muitos casos, o problema real não é velocidade. É dependência.</p><p>Um sistema pode ser rápido e ainda assim ser frágil. Pode responder bem em condições normais e mesmo assim entrar em colapso quando uma parte do fluxo fica lenta. Pode ter boa infraestrutura e continuar sofrendo porque coisas demais precisam acontecer ao mesmo tempo, dentro da mesma requisição, na mesma cadeia de responsabilidade.</p><p>Essa é uma das diferenças mais importantes entre sistemas que apenas crescem e sistemas realmente preparados para escalar.</p><p>No início de um projeto, a comunicação síncrona costuma ser a escolha natural. Uma requisição chega ao backend, a aplicação processa, conversa com o banco de dados, talvez chame um serviço externo e retorna uma resposta. Esse modelo é simples de entender, simples de depurar e simples de implementar.</p><p>Frameworks como Express no Node.js, ASP.NET no .NET, Django no Python, Laravel no PHP, Spring Boot no Java ou Ruby on Rails tornam esse fluxo extremamente produtivo. Um desenvolvedor consegue criar endpoints, implementar regras, conectar ao banco de dados e entregar valor rapidamente.</p><p>Em uma aplicação pequena, isso funciona muito bem. O fluxo é claro. A quantidade de dependências é limitada. O tempo de resposta é previsível. E, se algo falha, geralmente é possível identificar o problema com relativa facilidade.</p><p>O problema é que sistemas não permanecem simples quando o negócio começa a crescer.</p><p>Novas regras surgem. Novas telas são criadas. Novas integrações entram no fluxo. Processos que antes eram pequenos passam a envolver diferentes partes da aplicação. Um cadastro deixa de ser apenas um cadastro. Ele passa a validar dados, consultar serviços externos, gerar notificações, atualizar indicadores, disparar e-mails, registrar logs, criar históricos, recalcular saldos ou acionar outras rotinas.</p><p>No começo, tudo isso costuma ser colocado dentro do mesmo fluxo.</p><p>O usuário clica em um botão. O backend recebe a requisição. O sistema executa todas as etapas. Só depois retorna uma resposta.</p><p>Parece organizado. Parece direto. Parece eficiente.</p><p>Até o momento em que deixa de ser.</p><p>Imagine, por exemplo, um sistema de pedidos. Quando o cliente finaliza uma compra, o backend precisa criar o pedido, validar estoque, calcular frete, consultar pagamento, emitir nota, enviar e-mail, atualizar painel administrativo e notificar integrações externas.</p><p>Em um modelo totalmente síncrono, todas essas etapas podem acabar acontecendo dentro da mesma operação. O fluxo talvez seja parecido com isso:</p><pre>app.pojast(&#39;/orders&#39;, async (req, res) =&gt; {<br>  const order = await createOrder(req.body);<br>  await reserveStock(order);<br>  await processPayment(order);<br>  await issueInvoice(order);<br>  await sendConfirmationEmail(order);<br>  await notifyExternalMarketplace(order);<br>  res.json({<br>    success: true,<br>    orderId: order.id<br>  });<br>});</pre><p>Esse código é fácil de entender. Ele parece limpo, linear e previsível. Mas existe um problema escondido nessa simplicidade: o sucesso da operação inteira depende de todas as etapas funcionarem no tempo esperado.</p><p>Se a reserva de estoque demorar, o usuário espera. Se o pagamento estiver lento, o usuário espera. Se a emissão de nota falhar, talvez o pedido inteiro seja comprometido. Se o serviço de e-mail estiver fora do ar, uma operação que deveria depender apenas da criação do pedido pode falhar por causa de uma etapa secundária. Se a integração com um marketplace externo estiver instável, o checkout pode ser impactado por algo que nem deveria bloquear a experiência principal.</p><p>Esse é o ponto em que o sistema começa a mostrar fragilidade.</p><p>Não porque o código está necessariamente errado, mas porque responsabilidades diferentes foram colocadas dentro do mesmo fluxo crítico.</p><p>A pergunta que precisa ser feita é simples: tudo isso realmente precisa acontecer antes de responder ao usuário?</p><p>Em muitos casos, a resposta é não.</p><p>Criar o pedido talvez precise acontecer imediatamente. Validar pagamento talvez também faça parte do fluxo crítico. Mas enviar e-mail, notificar sistemas externos, atualizar relatórios, disparar integrações ou processar rotinas auxiliares normalmente podem acontecer depois.</p><p>Essa separação muda completamente a forma como o sistema se comporta.</p><p>Quando tudo acontece de forma síncrona, cada etapa adiciona tempo e risco à operação principal. Quanto mais dependências existem no fluxo, maior a chance de alguma coisa falhar. E quando uma etapa falha, o impacto pode se espalhar para todo o processo.</p><p>É o famoso efeito dominó aplicado à arquitetura de software.</p><p>Uma API externa fica lenta e o sistema inteiro começa a responder mal. Um serviço de terceiros fica fora do ar e operações internas são bloqueadas. Uma rotina pesada consome recursos demais e prejudica usuários que nem estavam relacionados àquele processo.</p><p>Nesses momentos, muitas empresas tentam resolver o problema aumentando infraestrutura. Colocam mais servidores, aumentam memória, melhoram banco de dados, criam réplicas, adicionam cache ou escalam containers. Tudo isso pode ajudar, mas apenas até certo ponto.</p><p>Se o problema principal é acoplamento, mais infraestrutura só compra tempo.</p><p>Ela não corrige a estrutura.</p><p>Um sistema acoplado demais continua frágil mesmo rodando em máquinas maiores. Ele pode até suportar mais volume por algum tempo, mas permanece vulnerável ao mesmo tipo de falha. Quando a dependência está no desenho do fluxo, a solução precisa passar pela arquitetura.</p><p>É aqui que entram os processos assíncronos.</p><p>Processamento assíncrono é a prática de separar o que precisa acontecer imediatamente daquilo que pode acontecer em outro momento, sem bloquear a resposta principal do sistema. Em vez de executar todas as etapas dentro da mesma requisição, a aplicação registra um evento, envia uma mensagem para uma fila ou publica uma notificação para que outro processo cuide daquela tarefa posteriormente.</p><p>O conceito não é novo, mas se tornou essencial em sistemas modernos.</p><p>Ferramentas como RabbitMQ, Apache Kafka, AWS SQS, Google Pub/Sub e Azure Service Bus existem justamente para ajudar nesse tipo de separação. Cada uma tem características diferentes, mas todas partem de uma ideia central: permitir que partes do sistema se comuniquem sem depender de uma resposta imediata.</p><p>RabbitMQ é muito utilizado para filas tradicionais de tarefas. Ele funciona muito bem quando você precisa distribuir trabalho entre consumidores, processar mensagens de forma confiável e controlar filas específicas. É comum em cenários como envio de e-mails, processamento de imagens, geração de relatórios, notificações, integrações e rotinas internas.</p><p>Kafka tem uma natureza diferente. Ele é mais forte em cenários de alto volume de eventos, streaming de dados e processamento distribuído. Em vez de pensar apenas em uma fila de tarefas, Kafka trabalha muito bem quando diferentes consumidores precisam ler eventos, processar informações em paralelo e manter histórico de eventos por um período.</p><p>AWS SQS, Google Pub/Sub e Azure Service Bus trazem essa lógica para ambientes cloud gerenciados. A vantagem é reduzir esforço operacional. Em vez de cuidar diretamente de servidores, clusters ou manutenção da ferramenta, a empresa usa um serviço gerenciado pelo provedor de nuvem.</p><p>A escolha da ferramenta depende do contexto. O erro está em achar que a ferramenta é o ponto principal.</p><p>Não é.</p><p>O ponto principal é a mudança de mentalidade.</p><p>Em vez de perguntar “como faço tudo isso mais rápido?”, a pergunta passa a ser “o que realmente precisa acontecer agora e o que pode acontecer depois?”.</p><p>Essa pergunta, quando bem feita, melhora a arquitetura do sistema.</p><p>Voltando ao exemplo do pedido, o fluxo poderia ser reorganizado assim:</p><pre>app.post(&#39;/orders&#39;, async (req, res) =&gt; {<br>  const order = await createOrder(req.body);<br>  await reserveStock(order);<br>  await processPayment(order);<br>  await queue.publish(&#39;order.created&#39;, {<br>    orderId: order.id,<br>    customerId: order.customerId<br>  });<br>  res.json({<br>    success: true,<br>    orderId: order.id<br>  });<br>});</pre><p>Nesse modelo, o fluxo principal cuida do que é essencial para responder ao usuário. Depois disso, publica um evento informando que um pedido foi criado. A partir desse evento, outros processos podem assumir tarefas complementares.</p><p>Um worker pode emitir a nota. Outro pode enviar e-mail. Outro pode atualizar relatórios. Outro pode notificar o marketplace. Cada processo passa a ter uma responsabilidade mais clara e independente.</p><p>Um exemplo simples de consumidor poderia ser:</p><pre>queue.consume(&#39;order.created&#39;, async (message) =&gt; {<br>  const { orderId } = message;const order = await getOrder(orderId);<br>  await sendConfirmationEmail(order);<br>  await notifyExternalMarketplace(order);<br>});</pre><p>Esse código representa uma mudança importante. O envio de e-mail e a notificação externa deixam de bloquear a criação do pedido. Se o serviço de e-mail estiver fora do ar, o pedido ainda pode ser criado. Se o marketplace estiver instável, a mensagem pode ser reprocessada depois. O sistema deixa de depender de tudo funcionando ao mesmo tempo.</p><p>Essa é a essência da resiliência.</p><p>Não significa ignorar falhas. Significa impedir que uma falha secundária derrube o fluxo principal.</p><p>Essa separação também melhora a experiência do usuário. Em vez de esperar todas as etapas concluírem, ele recebe uma resposta mais rápida sobre aquilo que realmente importa naquele momento. O restante do processamento acontece em segundo plano.</p><p>Para o usuário, a sensação é de velocidade.</p><p>Para o sistema, o ganho é de estabilidade.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JRV6_7ZJwJGS-SKv7fb6fQ.png" /><figcaption>Arquiteturas assíncronas organizam o fluxo em etapas independentes, reduzindo gargalos e evitando que uma falha comprometa todo o sistema.</figcaption></figure><p>Existe uma analogia simples que ajuda a explicar isso para quem não é técnico. Imagine um restaurante. Se o garçom precisasse anotar o pedido, cozinhar, preparar a bebida, lavar os pratos, fechar a conta e só então voltar à mesa, o atendimento seria lento e frágil. Qualquer problema em uma etapa bloquearia todo o processo.</p><p>Um restaurante funciona melhor porque as responsabilidades são separadas. O garçom registra o pedido, a cozinha prepara, o caixa cobra, alguém organiza a entrega e outras pessoas cuidam da limpeza. O cliente não precisa esperar todas as rotinas internas do restaurante terminarem para saber que seu pedido foi recebido.</p><p>Sistemas assíncronos seguem uma lógica parecida.</p><p>Eles não eliminam trabalho. Eles organizam o trabalho.</p><p>Essa organização é o que permite escalar.</p><p>Quando um sistema separa responsabilidades, ele ganha capacidade de absorver variações. Se a fila de e-mails cresce, você pode aumentar consumidores apenas para essa fila. Se a emissão de nota está lenta, o restante do sistema pode continuar funcionando. Se uma integração externa fica indisponível, você pode reprocessar mensagens depois sem bloquear o usuário.</p><p>Isso cria uma arquitetura mais tolerante a falhas.</p><p>Mas é importante dizer: processos assíncronos também trazem novos desafios.</p><p>O primeiro deles é a consistência eventual. Em um sistema síncrono, a tendência é esperar que tudo esteja atualizado ao mesmo tempo. Em um sistema assíncrono, algumas informações podem levar alguns segundos ou minutos para serem processadas. Isso precisa ser entendido pelo negócio e refletido na experiência do usuário.</p><p>Por exemplo, um pedido pode ser criado imediatamente, mas a nota fiscal pode ser emitida alguns segundos depois. Um relatório pode não refletir uma transação no mesmo instante em que ela ocorreu. Uma integração pode ser processada em segundo plano.</p><p>Isso não é necessariamente um problema. Muitas vezes é uma escolha arquitetural correta. Mas precisa ser comunicada e tratada.</p><p>O segundo desafio é a idempotência.</p><p>Em sistemas com filas, uma mesma mensagem pode ser processada mais de uma vez em determinados cenários. Por isso, os consumidores precisam estar preparados para lidar com repetição sem gerar efeitos colaterais indevidos.</p><p>Se um worker de pagamento processa a mesma mensagem duas vezes, ele não pode cobrar o cliente duas vezes. Se um worker de e-mail recebe a mesma mensagem novamente, talvez precise verificar se aquele e-mail já foi enviado. Se uma integração externa é reprocessada, o sistema precisa garantir que não haverá duplicidade.</p><p>Isso exige cuidado.</p><p>Um exemplo simples de proteção seria:</p><pre>queue.consume(&#39;invoice.issue&#39;, async (message) =&gt; {<br>  const { orderId } = message;const existingInvoice = await findInvoiceByOrderId(orderId);<br>  if (existingInvoice) {<br>    return;<br>  }<br>  await issueInvoice(orderId);<br>});</pre><p>Aqui, antes de emitir uma nota, o sistema verifica se ela já existe. Esse tipo de validação evita que o reprocessamento de mensagens gere problemas no negócio.</p><p>O terceiro desafio é observabilidade.</p><p>Quando o sistema deixa de ser linear, entender o que está acontecendo se torna mais difícil. Em um fluxo síncrono, você acompanha uma requisição do início ao fim. Em um fluxo assíncrono, o processo pode passar por filas, workers, reprocessamentos e diferentes serviços.</p><p>Sem observabilidade, filas viram caixas pretas.</p><p>Você precisa saber quantas mensagens estão pendentes, quantas falharam, quais estão sendo reprocessadas, quanto tempo cada tarefa demora e onde estão os gargalos. Caso contrário, o sistema pode parecer saudável na interface enquanto acumula problemas em segundo plano.</p><p>Esse é o motivo pelo qual assíncrono e observabilidade precisam andar juntos.</p><p>RabbitMQ, Kafka, SQS ou Pub/Sub resolvem a comunicação. Mas a empresa ainda precisa monitorar comportamento, falhas, atrasos e reprocessamentos. Ferramentas como Datadog, New Relic, Grafana, Prometheus e OpenTelemetry entram nesse ponto para dar visibilidade ao fluxo.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hx4WkzXwuNv0ufMno1ZeHw.jpeg" /><figcaption>A interface parece saudável, enquanto os problemas se acumulam em silêncio no fundo do sistema.</figcaption></figure><p>Outro ponto importante é que processos assíncronos não devem ser usados como desculpa para bagunça. Colocar tudo em fila sem critério pode criar um sistema mais difícil de entender do que o original. A decisão precisa ser orientada por responsabilidade, criticidade e impacto no usuário.</p><p>Nem tudo precisa ser assíncrono.</p><p>Operações que exigem resposta imediata, validações críticas ou decisões que precisam ser confirmadas no momento da requisição podem continuar síncronas. O erro está em colocar dentro do fluxo principal aquilo que não precisa estar ali.</p><p>Uma boa arquitetura não é aquela que usa fila em tudo. É aquela que sabe onde a fila faz sentido.</p><p>Esse discernimento é o que separa uso técnico de maturidade arquitetural.</p><p>Em sistemas financeiros, por exemplo, certas validações precisam acontecer imediatamente. Você não pode simplesmente dizer ao usuário que uma transação foi aprovada se a validação crítica ainda não ocorreu. Por outro lado, notificações, conciliações, auditorias complementares e atualizações de relatórios podem ser processadas de forma assíncrona.</p><p>Em e-commerces, o pedido e o pagamento podem fazer parte do fluxo principal, mas e-mails, recomendações, atualização de CRM e integrações secundárias podem ir para filas.</p><p>Em sistemas de saúde, o registro de uma informação crítica precisa ser confiável no momento da operação, mas rotinas de notificação, sincronização ou processamento analítico podem acontecer depois.</p><p>Em plataformas imobiliárias, automotivas ou marketplaces, integrações com portais externos, enriquecimento de dados, geração de relatórios e notificações normalmente se beneficiam muito de uma abordagem assíncrona.</p><p>A aplicação prática muda conforme o setor, mas o princípio permanece.</p><p>Separar o essencial do complementar.</p><p>Essa separação melhora a performance, mas esse não é o único benefício. Talvez nem seja o principal.</p><p>O maior benefício é reduzir o impacto de falhas.</p><p>Quando tudo está acoplado, uma falha pequena pode comprometer uma operação inteira. Quando responsabilidades estão separadas, a falha fica isolada. Ela pode ser monitorada, reprocessada e corrigida sem derrubar o fluxo principal.</p><p>Isso muda o comportamento do sistema em momentos de pressão.</p><p>E todo sistema que cresce passa por momentos de pressão.</p><p>Picos de acesso, instabilidade externa, aumento de processamento, campanhas comerciais, integrações fora do ar, mudanças de regra, crescimento de base de usuários. Em algum momento, o sistema será testado fora das condições ideais.</p><p>A pergunta é se ele foi construído para absorver isso ou se depende de tudo funcionando perfeitamente o tempo todo.</p><p>Sistemas que dependem de perfeição não escalam bem.</p><p>Eles apenas funcionam enquanto o ambiente é favorável.</p><p>Quando um negócio começa a crescer, a quantidade de situações imprevisíveis aumenta. É nesse momento que a arquitetura precisa deixar de ser apenas funcional e passar a ser resiliente.</p><p>Processos assíncronos fazem parte dessa transição.</p><p>Eles permitem que o sistema continue operando mesmo quando algumas partes estão lentas, indisponíveis ou sobrecarregadas. Permitem distribuir trabalho. Permitem reprocessar falhas. Permitem proteger o usuário de etapas que não precisam bloquear sua experiência.</p><p>Esse tipo de arquitetura mostra que a empresa entendeu uma coisa importante: nem tudo tem a mesma urgência.</p><p>E quando tudo é tratado como urgente, o sistema perde capacidade de priorização.</p><p>A maturidade está em saber diferenciar.</p><p>Existe também um impacto direto no time de desenvolvimento. Quando responsabilidades são separadas, o código tende a ficar mais organizado. Serviços passam a ter funções mais claras. Workers podem ser desenvolvidos, escalados e monitorados de forma independente. O time consegue evoluir partes do sistema sem mexer em todo o fluxo principal.</p><p>Isso facilita manutenção.</p><p>Também facilita escala de equipe.</p><p>Quando tudo está dentro do mesmo fluxo, qualquer alteração exige cuidado redobrado porque o impacto pode ser amplo. Quando responsabilidades são bem separadas, o escopo de mudança fica mais claro. Isso reduz medo, melhora previsibilidade e permite evolução mais segura.</p><p>Aqui existe uma ligação direta com testes automatizados. Quanto mais bem separadas estão as responsabilidades, mais fácil é testar cada parte. Um worker que processa envio de e-mail pode ser testado isoladamente. Um consumidor de fila de pagamento pode ter suas regras validadas com testes específicos. Um produtor de eventos pode ser verificado sem depender de todo o sistema rodando junto.</p><p>Ou seja, assíncrono não é apenas infraestrutura. Ele influencia arquitetura, testes, observabilidade, deploy e operação.</p><p>Essa é a razão pela qual empresas que querem escalar não podem tratar mensageria como detalhe técnico. Ela muda a forma como o sistema pensa, reage e evolui.</p><p>Mas existe um cuidado importante: complexidade desnecessária também é um problema.</p><p>Implementar Kafka em um sistema que só precisava de uma fila simples pode gerar mais custo do que benefício. Criar dezenas de eventos sem governança pode transformar o sistema em um labirinto. Usar mensageria sem padrões claros pode criar mensagens duplicadas, consumidores inconsistentes e fluxos difíceis de rastrear.</p><p>A ferramenta precisa servir ao problema, não ao ego técnico.</p><p>RabbitMQ pode ser excelente para uma grande parte dos cenários de fila. SQS pode ser suficiente para sistemas em AWS que precisam de simplicidade operacional. Kafka pode ser a escolha certa quando há alto volume de eventos, necessidade de retenção e múltiplos consumidores processando dados de formas diferentes. Azure Service Bus pode fazer sentido em ambientes corporativos Microsoft. Google Pub/Sub pode ser natural para arquiteturas em GCP.</p><p>Não existe ferramenta universal.</p><p>Existe contexto.</p><p>E contexto é o que define boa arquitetura.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*L2O8jDoyJSOmQxJxZdvcjQ.png" /><figcaption>De um lado, dependência em cadeia. Do outro, fluxos independentes preparados para escalar.</figcaption></figure><p>No fim, processos assíncronos não são apenas uma forma de deixar o sistema mais rápido. São uma forma de tornar o sistema mais preparado para a realidade.</p><p>E a realidade é que APIs falham, bancos ficam lentos, serviços externos saem do ar, usuários acessam em picos, regras mudam e integrações se comportam de forma imprevisível.</p><p>Um sistema sério não pode depender de tudo funcionando perfeitamente ao mesmo tempo.</p><p>Ele precisa ser desenhado para continuar operando mesmo quando partes do ambiente falham.</p><p>Esse é o ponto central.</p><p>Quando cada operação depende de outra para terminar imediatamente, o sistema pode até parecer estável em pequena escala. Mas essa estabilidade é frágil, porque depende de uma sequência perfeita de acontecimentos.</p><p>À medida que o negócio cresce, essa sequência perfeita se torna cada vez menos provável.</p><p>Por isso, a pergunta não é apenas se sua aplicação está rápida hoje. A pergunta é se ela continuará confiável quando o volume aumentar, quando uma integração falhar, quando uma fila crescer, quando um serviço ficar lento ou quando uma operação precisar ser reprocessada.</p><p>Sistemas que escalam não são aqueles que nunca enfrentam falhas. São aqueles que foram desenhados para limitar o impacto dessas falhas.</p><p>Processos assíncronos ajudam exatamente nisso.</p><p>Eles não eliminam complexidade, mas organizam a complexidade. Não removem falhas, mas reduzem propagação. Não fazem tudo acontecer imediatamente, mas permitem que o sistema priorize o que realmente importa.</p><p>E essa priorização é uma das bases da escala.</p><p>Sem ela, a empresa continua empilhando responsabilidades no mesmo fluxo, até que o sistema se torne lento, frágil e difícil de evoluir.</p><p>Nesse ponto, o problema deixa de ser apenas técnico. Ele começa a afetar a operação, a experiência do usuário, a previsibilidade do time e a capacidade da empresa de crescer sem transformar cada nova demanda em risco.</p><p>Por isso, processos assíncronos não são luxo de arquitetura. Não são algo reservado apenas para grandes empresas. Não são apenas uma escolha técnica para quem gosta de mensageria.</p><p>Eles são uma resposta prática a um problema real: sistemas que crescem não podem depender de tudo acontecendo ao mesmo tempo.</p><p>Se sua empresa ainda trata todo processo como síncrono, toda integração como bloqueante e toda operação como imediata, talvez o sistema ainda esteja funcionando.</p><p>Mas isso não significa que ele esteja pronto para escalar.</p><p>A pergunta que fica é simples: seu sistema separa o que precisa acontecer agora do que pode acontecer depois, ou ainda está deixando o crescimento depender de tudo funcionar ao mesmo tempo? 🔥</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8eed2225c545" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How I made nri-mysql work with MariaDB 10.5]]></title>
            <link>https://medium.com/@patrickotto.dev/how-i-made-nri-mysql-work-with-mariadb-10-5-b95f55515609?source=rss-daa0cab73ef2------2</link>
            <guid isPermaLink="false">https://medium.com/p/b95f55515609</guid>
            <dc:creator><![CDATA[Patrick Otto]]></dc:creator>
            <pubDate>Mon, 04 May 2026 16:09:59 GMT</pubDate>
            <atom:updated>2026-05-04T17:39:55.684Z</atom:updated>
            <content:encoded><![CDATA[<h3>How I made nri-mysql work with MariaDB 10.5 when the official integration simply did not deliver what it promised (New Relic)</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SzfauPJasVof-0qmAyfFbg.png" /></figure><blockquote>It took me 3 days to reach the final solution. Not because I was missing a flag, but because the real problem was in how telemetry was born, connected, and consumed by New Relic’s UI.</blockquote><p>If you have ever spent hours trying to make database <strong>observability</strong> work outside the officially supported path, this article may save you a lot of blind trial and error. What I want to show here is not just a working configuration. It is the reasoning that led me there, the path that failed, what I had to validate in NRQL, what New Relic’s interface was actually expecting to receive, and, in the end, the solution that finally made Query details and Execution plan work with MariaDB 10.5.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/0*n_ehQBeiLyP_KK5e.png" /><figcaption>This was the first sign that the problem wasn’t just data collection. Some of the telemetry existed, but the final experience remained broken.</figcaption></figure><p>There is a very common assumption in observability that if the integration is installed, everything else should simply work. In practice, that is rarely true. Especially when you step outside the most comfortable path in the documentation and try to use an integration designed for a specific scenario on a database that is close, but not exactly within what the vendor officially supports. That is exactly what happened when I tried to make nri-mysql deliver the full monitoring experience for a MariaDB 10.5 environment.</p><p>The goal looked simple. I wanted New Relic to show, in a coherent way, slow queries, individual query details, and execution plan. What actually happened was very different. One part worked. Another part worked partially. And the most interesting part, which was Query details, remained empty. When that happens, the problem stops looking like ingestion and starts looking like internal telemetry coupling. That was the moment I stopped treating the situation as just another YAML configuration issue and started looking at it as I should have from the beginning, as a data model problem.</p><p>At first, the most obvious hypothesis was to blame MariaDB. After all, New Relic’s query performance flow is much more centered around MySQL 8 than MariaDB 10.5. So the idea of incompatibility felt natural. But there was one important detail. Wait time analysis was working. That meant New Relic was indeed receiving part of the database telemetry. The environment was not blind. The agent was not broken. Database access was not wrong. Collection existed, but the final view remained incomplete. And when collection exists but the interface still stays empty, the problem is usually no longer about being able to read the database. It is about how the events relate to each other.</p><p>This is the most common trap in this kind of troubleshooting. When an integration partially fails, the tendency is to keep pushing in the same direction. Change a flag, tweak a threshold, enable more metrics, restart the agent, test again. All of that solves configuration problems. None of that solves semantic problems. In my case, New Relic was showing an almost didactic situation. I had events arriving for some views. I had data in NRQL. I even had execution plans being generated in a custom format. But the native page remained empty. That was a strong indication that the interface did not merely want existing events. It wanted events that were coherent with each other.</p><p>That was when the investigation changed level. Instead of continuing to click around the interface waiting for something to unlock, I moved into NRQL and started checking what actually existed in New Relic’s event store. First, I confirmed that the individual events existed. Then I confirmed that the execution plan events also existed. Then I noticed a decisive detail. The native page was filtering by entityGuid. In other words, sending entityName was not enough. The page’s own internal query was using a different identifier. Once I understood that, an important piece finally fell into place.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/0*twIwpzJddMy2IRbu.png" /><figcaption>NRQL showing MySQL IndividualQueriesSample returning data and the difference between querying with and without entityGuid.</figcaption></figure><p>Even after aligning the entity, the solution was still not complete. The top of the page depended on one thing. The execution plan depended on another. And both needed to talk to each other. At first, I had partial solutions coming from different places. One event coming from slow_log, another coming from performance_schema, and another one being artificially generated with <strong>EXPLAIN FORMAT=JSON</strong>. Separately, all of that looked reasonable. Together, it did not. Because New Relic’s UI does not want only three events with the correct names. It wants coherence between them. It wants the same query_id, the same event_id, the same thread_id, the same entity, and the same time window. Without that, the interface may receive data, but it still cannot assemble the full story.</p><p>That is why I abandoned the idea of mixing sources. The solution only started to become stable when I made all three event types be born from the same logical origin. What worked best was using recent statements from performance_schema.events_statements_current, performance_schema.events_statements_history, and performance_schema.events_statements_history_long. From there, I was able to generate <strong>MysqlIndividualQueriesSample</strong>, <strong>MysqlQueryExecutionSample</strong>, and <strong>MysqlSlowQueriesSample</strong>, all from the same recent set of queries. That was important because it allowed me to align the identifiers the UI uses to build the final view. Instead of three telemetry streams that merely looked similar, I ended up with three perspectives of the same telemetry.</p><p>When that part finally began to work, another issue appeared. Execution plan still looked strange. Some fields were coming back as zero. Some tables were only partially visible. Some steps simply disappeared. Once again, the database was not failing to respond. The problem was the format. EXPLAIN FORMAT=JSON in MariaDB 10.5 returns structures with repeated keys such as table, inside blocks that a more naive parser silently overwrites. The data was there. The parser was the thing throwing part of it away without noticing. Once I fixed that, the plan began to appear with much more coherence. Not perfect in every case, but functional. And functional was already much better than empty.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/0*ZNjapFnfNa0bil8T.png" /><figcaption>Print of the Execution plan appearing with step_id, table_name and access_type</figcaption></figure><p>After that, another side effect showed up. The interface started to display queries that were useless for application analysis, such as SELECT version(), SELECT sleep(…), queries against information_schema, queries against performance_schema, EXPLAIN FORMAT=JSON, and SET commands. In other words, I had solved the pipeline, but I had still not solved panel quality. The answer was to filter that noise inside the script itself. It made no sense to send to the final view queries that only existed because of the inspection process itself or because of the monitoring process itself. From that point on, the screen started to show what actually mattered, which were real application queries.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/0*5OyBfVKBNftPWrsb.png" /><figcaption>Printout of nri-flex — pretty — verbose or of the Python script returning JSON with the events already normalized</figcaption></figure><p>In the end, the solution became much smaller than the problem initially suggested. I kept only two files, mariadb-config.yml and mariadb_query_execution.py. The first one defines nri-mysql-legacy and nri-flex. The second one uses performance_schema, filters noise, and generates the events compatible with New Relic’s interface. What unlocked Query details was not magic. It was the combination of origin coherence, entity identification coherence, and coherence between event identifiers.</p><p>Below are the two final files, already rewritten with flags and placeholders so that anyone can adapt them to their own environment without exposing usernames, passwords, real hostnames, or their own account identifiers.</p><h4>mariadb-config.yml</h4><pre>integrations:<br>  - name: nri-mysql-legacy<br>    executable: /var/db/newrelic-infra/newrelic-integrations/bin/nri-mysql-legacy<br>    env:<br>      HOSTNAME: &quot;${NR_MYSQL_HOST}&quot;<br>      PORT: ${NR_MYSQL_PORT}<br>      USERNAME: &quot;${NR_MYSQL_USER}&quot;<br>      PASSWORD: &quot;${NR_MYSQL_PASSWORD}&quot;<br>      REMOTE_MONITORING: true<br>      EXTENDED_METRICS: true<br>      EXTENDED_INNODB_METRICS: true<br>      ENABLE_QUERY_MONITORING: true<br>      QUERY_MONITORING_RESPONSE_TIME_THRESHOLD: 1<br>      QUERY_MONITORING_COUNT_THRESHOLD: 20<br>    interval: 30s<br>    labels:<br>      env: &quot;${NR_ENVIRONMENT}&quot;<br>      role: &quot;${NR_ROLE}&quot;<br>    inventory_source: config/mysql<br><br>  - name: nri-flex<br>    config:<br>      name: mariadbQueryTelemetry<br>      apis:<br>        - name: mariadbFakeIndividualQueries<br>          commands:<br>            - run: &gt;-<br>                sh -c &quot;MYSQL_HOST=${NR_MYSQL_HOST}<br>                MYSQL_PORT=${NR_MYSQL_PORT}<br>                MYSQL_USER=${NR_MYSQL_USER}<br>                MYSQL_PASSWORD=&#39;${NR_MYSQL_PASSWORD}&#39;<br>                MYSQL_DATABASE_FILTER=${NR_MYSQL_DATABASE}<br>                MYSQL_QUERY_PLAN_LIMIT=${NR_QUERY_PLAN_LIMIT:-20}<br>                MYSQL_QUERY_PLAN_THRESHOLD_MS=${NR_QUERY_PLAN_THRESHOLD_MS:-0}<br>                MYSQL_SLOW_QUERY_FETCH_INTERVAL_SECONDS=${NR_SLOW_QUERY_FETCH_INTERVAL_SECONDS:-300}<br>                MYSQL_QUERY_MONITORING_COUNT_THRESHOLD=${NR_QUERY_MONITORING_COUNT_THRESHOLD:-20}<br>                MYSQL_QUERY_PLAN_MODE=individual_queries<br>                python3 /usr/local/bin/mariadb_query_execution.py 2&gt;/dev/null || echo &#39;[]&#39;&quot;<br>          custom_attributes:<br>            application: &quot;${NR_APPLICATION_NAME}&quot;<br>            entityGuid: &quot;${NR_ENTITY_GUID}&quot;<br>            entityName: &quot;${NR_ENTITY_NAME}&quot;<br>            displayName: &quot;${NR_DISPLAY_NAME}&quot;<br>            hostname: &quot;${NR_MYSQL_HOST}&quot;<br>            port: &quot;${NR_MYSQL_PORT}&quot;<br>            db.instance: &quot;${NR_MYSQL_DATABASE}&quot;<br>            database.name: &quot;${NR_MYSQL_DATABASE}&quot;<br>          event_type: MysqlIndividualQueriesSample<br><br>        - name: mariadbSlowQueries<br>          commands:<br>            - run: &gt;-<br>                sh -c &quot;MYSQL_HOST=${NR_MYSQL_HOST}<br>                MYSQL_PORT=${NR_MYSQL_PORT}<br>                MYSQL_USER=${NR_MYSQL_USER}<br>                MYSQL_PASSWORD=&#39;${NR_MYSQL_PASSWORD}&#39;<br>                MYSQL_DATABASE_FILTER=${NR_MYSQL_DATABASE}<br>                MYSQL_QUERY_PLAN_LIMIT=${NR_QUERY_PLAN_LIMIT:-20}<br>                MYSQL_QUERY_PLAN_THRESHOLD_MS=${NR_QUERY_PLAN_THRESHOLD_MS:-0}<br>                MYSQL_SLOW_QUERY_FETCH_INTERVAL_SECONDS=${NR_SLOW_QUERY_FETCH_INTERVAL_SECONDS:-300}<br>                MYSQL_QUERY_MONITORING_COUNT_THRESHOLD=${NR_QUERY_MONITORING_COUNT_THRESHOLD:-20}<br>                MYSQL_QUERY_PLAN_MODE=slow_queries<br>                python3 /usr/local/bin/mariadb_query_execution.py 2&gt;/dev/null || echo &#39;[]&#39;&quot;<br>          custom_attributes:<br>            application: &quot;${NR_APPLICATION_NAME}&quot;<br>            entityGuid: &quot;${NR_ENTITY_GUID}&quot;<br>            entityName: &quot;${NR_ENTITY_NAME}&quot;<br>            displayName: &quot;${NR_DISPLAY_NAME}&quot;<br>            hostname: &quot;${NR_MYSQL_HOST}&quot;<br>            port: &quot;${NR_MYSQL_PORT}&quot;<br>            db.instance: &quot;${NR_MYSQL_DATABASE}&quot;<br>            database.name: &quot;${NR_MYSQL_DATABASE}&quot;<br>          event_type: MysqlSlowQueriesSample<br><br>        - name: mariadbFakeQueryExecutionPlans<br>          commands:<br>            - run: &gt;-<br>                sh -c &quot;MYSQL_HOST=${NR_MYSQL_HOST}<br>                MYSQL_PORT=${NR_MYSQL_PORT}<br>                MYSQL_USER=${NR_MYSQL_USER}<br>                MYSQL_PASSWORD=&#39;${NR_MYSQL_PASSWORD}&#39;<br>                MYSQL_DATABASE_FILTER=${NR_MYSQL_DATABASE}<br>                MYSQL_QUERY_PLAN_LIMIT=${NR_QUERY_PLAN_LIMIT:-20}<br>                MYSQL_QUERY_PLAN_THRESHOLD_MS=${NR_QUERY_PLAN_THRESHOLD_MS:-0}<br>                MYSQL_SLOW_QUERY_FETCH_INTERVAL_SECONDS=${NR_SLOW_QUERY_FETCH_INTERVAL_SECONDS:-300}<br>                MYSQL_QUERY_MONITORING_COUNT_THRESHOLD=${NR_QUERY_MONITORING_COUNT_THRESHOLD:-20}<br>                MYSQL_QUERY_PLAN_MODE=query_execution<br>                MYSQL_QUERY_PLAN_COMMAND_MODE=analyze<br>                python3 /usr/local/bin/mariadb_query_execution.py 2&gt;/dev/null || echo &#39;[]&#39;&quot;<br>          custom_attributes:<br>            application: &quot;${NR_APPLICATION_NAME}&quot;<br>            entityGuid: &quot;${NR_ENTITY_GUID}&quot;<br>            entityName: &quot;${NR_ENTITY_NAME}&quot;<br>            displayName: &quot;${NR_DISPLAY_NAME}&quot;<br>            hostname: &quot;${NR_MYSQL_HOST}&quot;<br>            port: &quot;${NR_MYSQL_PORT}&quot;<br>            db.instance: &quot;${NR_MYSQL_DATABASE}&quot;<br>            database.name: &quot;${NR_MYSQL_DATABASE}&quot;<br>          event_type: MysqlQueryExecutionSample</pre><h4>mariadb_query_execution.py</h4><pre>#!/usr/bin/env python3<br>&quot;&quot;&quot;<br>Generate fake MysqlQueryExecutionSample events for MariaDB by:<br>1. Reading recent SELECT/WITH statements from performance_schema<br>2. Running EXPLAIN FORMAT=JSON for each unique SQL text<br>3. Flattening MariaDB&#39;s JSON plan into step-based events that resemble<br>   New Relic&#39;s MysqlQueryExecutionSample shape<br><br>This is a compatibility workaround for environments where<br>MysqlIndividualQueriesSample and MysqlWaitEventsSample exist, but<br>MysqlQueryExecutionSample does not.<br>&quot;&quot;&quot;<br><br>import argparse<br>import binascii<br>from datetime import datetime<br>import json<br>import os<br>import re<br>import subprocess<br>import sys<br>import traceback<br>from typing import Any, Dict, List, Optional, Set, Tuple<br><br><br>DEBUG_ENABLED = os.getenv(&quot;MYSQL_QUERY_PLAN_DEBUG&quot;, &quot;&quot;).lower() in (&quot;1&quot;, &quot;true&quot;, &quot;yes&quot;, &quot;on&quot;)<br><br><br>def debug(*parts: Any) -&gt; None:<br>    if not DEBUG_ENABLED:<br>        return<br>    print(&quot;[mariadb_query_execution]&quot;, *parts, file=sys.stderr)<br><br><br>def mysql_command(<br>    *,<br>    host: str,<br>    port: str,<br>    user: str,<br>    password: str,<br>    database: Optional[str],<br>    sql: str,<br>) -&gt; str:<br>    cmd = [&quot;mysql&quot;, &quot;--raw&quot;, &quot;-N&quot;, &quot;-B&quot;, &quot;-h&quot;, host, &quot;-P&quot;, str(port), &quot;-u&quot;, user]<br>    if database:<br>        cmd.extend([&quot;-D&quot;, database])<br>    cmd.extend([&quot;-e&quot;, sql])<br><br>    env = os.environ.copy()<br>    env[&quot;MYSQL_PWD&quot;] = password<br><br>    proc = subprocess.run(<br>        cmd,<br>        stdout=subprocess.PIPE,<br>        stderr=subprocess.PIPE,<br>        universal_newlines=True,<br>        env=env,<br>        check=False,<br>    )<br>    if proc.returncode != 0:<br>        raise RuntimeError(proc.stderr.strip() or proc.stdout.strip() or &quot;mysql command failed&quot;)<br>    debug(&quot;mysql ok&quot;, &quot;db=&quot; + (database or &quot;&quot;), &quot;sql=&quot;, &quot; &quot;.join(sql.split())[:240])<br>    return proc.stdout<br><br><br>def hexdecode_text(value: str) -&gt; str:<br>    if not value:<br>        return &quot;&quot;<br>    return binascii.unhexlify(value.encode(&quot;ascii&quot;)).decode(&quot;utf-8&quot;, &quot;replace&quot;)<br><br><br>def normalize_string(value: Any) -&gt; str:<br>    if value is None:<br>        return &quot;&quot;<br>    if isinstance(value, bool):<br>        return &quot;true&quot; if value else &quot;false&quot;<br>    return str(value)<br><br><br>def normalize_csv(value: Any) -&gt; str:<br>    if value is None:<br>        return &quot;&quot;<br>    if isinstance(value, list):<br>        return &quot;,&quot;.join(str(item) for item in value)<br>    return str(value)<br><br><br>def normalize_int(value: Any) -&gt; int:<br>    if value in (None, &quot;&quot;):<br>        return 0<br>    try:<br>        return int(value)<br>    except Exception:<br>        try:<br>            return int(float(value))<br>        except Exception:<br>            return 0<br><br><br>def normalize_float_string(value: Any) -&gt; str:<br>    if value in (None, &quot;&quot;):<br>        return &quot;&quot;<br>    try:<br>        number = float(value)<br>    except Exception:<br>        return normalize_string(value)<br><br>    formatted = &quot;{0:.3f}&quot;.format(number).rstrip(&quot;0&quot;).rstrip(&quot;.&quot;)<br>    return formatted or &quot;0&quot;<br><br><br>def format_bytes_human(value: Any) -&gt; str:<br>    try:<br>        size = float(value)<br>    except Exception:<br>        return &quot;&quot;<br><br>    if size &lt;= 0:<br>        return &quot;&quot;<br><br>    units = (<br>        (1024 ** 3, &quot;G&quot;),<br>        (1024 ** 2, &quot;M&quot;),<br>        (1024, &quot;K&quot;),<br>    )<br>    for threshold, suffix in units:<br>        if size &gt;= threshold:<br>            scaled = size / threshold<br>            if abs(scaled - round(scaled)) &lt; 0.05:<br>                return &quot;{0}{1}&quot;.format(int(round(scaled)), suffix)<br>            return &quot;{0:.1f}{1}&quot;.format(scaled, suffix)<br>    return str(int(round(size)))<br><br><br>def utc_now_iso() -&gt; str:<br>    return datetime.utcnow().strftime(&quot;%Y-%m-%dT%H:%M:%SZ&quot;)<br><br><br>def clean_identifier(value: str) -&gt; str:<br>    text = normalize_string(value).strip()<br>    if not text:<br>        return &quot;&quot;<br>    text = text.replace(&quot;`&quot;, &quot;&quot;)<br>    if &quot;.&quot; in text:<br>        text = text.split(&quot;.&quot;)[-1]<br>    return text.strip()<br><br><br>def build_table_alias_map(query_text: str) -&gt; Dict[str, str]:<br>    text = &quot; &quot;.join((query_text or &quot;&quot;).replace(&quot;\n&quot;, &quot; &quot;).replace(&quot;\r&quot;, &quot; &quot;).split())<br>    if not text:<br>        return {}<br><br>    alias_map: Dict[str, str] = {}<br>    pattern = re.compile(<br>        r&quot;(?i)\b(?:from|join)\s+&quot;<br>        r&quot;((?:`[^`]+`|\w+)(?:\s*\.\s*(?:`[^`]+`|\w+))?)&quot;<br>        r&quot;(?:\s+(?:as\s+)?(`[^`]+`|\w+))?&quot;<br>    )<br><br>    for match in pattern.finditer(text):<br>        raw_table_name = match.group(1) or &quot;&quot;<br>        raw_alias = match.group(2) or &quot;&quot;<br><br>        table_name = clean_identifier(raw_table_name)<br>        alias = clean_identifier(raw_alias)<br>        if not table_name:<br>            continue<br><br>        alias_map[table_name.lower()] = table_name<br>        if alias:<br>            alias_map[alias.lower()] = table_name<br><br>    return alias_map<br><br><br>def is_noise_query(query_text: str) -&gt; bool:<br>    text = &quot; &quot;.join((query_text or &quot;&quot;).strip().lower().split())<br>    if not text:<br>        return True<br><br>    prefixes = (<br>        &quot;set &quot;,<br>        &quot;show &quot;,<br>        &quot;explain &quot;,<br>    )<br>    if text.startswith(prefixes):<br>        return True<br><br>    contains_any = (<br>        &quot;select version(&quot;,<br>        &quot;select `version`&quot;,<br>        &quot;@@version_comment&quot;,<br>        &quot;select sleep(&quot;,<br>        &quot;information_schema.&quot;,<br>        &quot;information_schema`&quot;,<br>        &quot;performance_schema.&quot;,<br>        &quot;performance_schema`&quot;,<br>        &quot;events_statements_summary_by_digest&quot;,<br>        &quot;innodb_trx&quot;,<br>    )<br>    return any(fragment in text for fragment in contains_any)<br><br><br>def preserve_duplicate_keys(pairs: List[Tuple[str, Any]]) -&gt; Dict[str, Any]:<br>    data: Dict[str, Any] = {}<br>    for key, value in pairs:<br>        if key in data:<br>            existing = data[key]<br>            if isinstance(existing, list):<br>                existing.append(value)<br>            else:<br>                data[key] = [existing, value]<br>        else:<br>            data[key] = value<br>    return data<br><br><br>def parse_explain_json(raw: str) -&gt; Dict[str, Any]:<br>    raw = raw.strip()<br>    if not raw:<br>        raise ValueError(&quot;empty EXPLAIN output&quot;)<br><br>    try:<br>        return json.loads(raw, object_pairs_hook=preserve_duplicate_keys)<br>    except json.JSONDecodeError:<br>        if &quot;\\n&quot; in raw or &quot;\\t&quot; in raw:<br>            try:<br>                return json.loads(<br>                    raw.encode(&quot;utf-8&quot;).decode(&quot;unicode_escape&quot;),<br>                    object_pairs_hook=preserve_duplicate_keys,<br>                )<br>            except Exception:<br>                pass<br>        start = raw.find(&quot;{&quot;)<br>        end = raw.rfind(&quot;}&quot;)<br>        if start == -1 or end == -1 or start &gt;= end:<br>            raise<br>        return json.loads(raw[start : end + 1], object_pairs_hook=preserve_duplicate_keys)<br><br><br>def extract_plan_steps(plan: Dict[str, Any], event_id: int, thread_id: int) -&gt; List[Dict[str, Any]]:<br>    steps: List[Dict[str, Any]] = []<br>    step_id = 0<br>    root_query_cost = &quot;&quot;<br><br>    root_query_block = plan.get(&quot;query_block&quot;) if isinstance(plan.get(&quot;query_block&quot;), dict) else {}<br>    if root_query_block:<br>        root_cost_info = root_query_block.get(&quot;cost_info&quot;) if isinstance(root_query_block.get(&quot;cost_info&quot;), dict) else {}<br>        root_query_cost = normalize_string(root_cost_info.get(&quot;query_cost&quot;))<br>        if not root_query_cost:<br>            root_query_cost = normalize_float_string(root_query_block.get(&quot;r_total_time_ms&quot;))<br><br>    def visit(node: Any, inherited_query_cost: str = &quot;&quot;, inherited_prefix_cost: str = &quot;&quot;) -&gt; None:<br>        nonlocal step_id<br><br>        if isinstance(node, dict):<br>            cost_info = node.get(&quot;cost_info&quot;) if isinstance(node.get(&quot;cost_info&quot;), dict) else {}<br>            runtime_engine_stats = node.get(&quot;r_engine_stats&quot;) if isinstance(node.get(&quot;r_engine_stats&quot;), dict) else {}<br><br>            query_cost = normalize_string(cost_info.get(&quot;query_cost&quot;)) or inherited_query_cost or root_query_cost<br>            if not query_cost:<br>                query_cost = normalize_float_string(node.get(&quot;r_total_time_ms&quot;))<br><br>            table_name = normalize_string(node.get(&quot;table_name&quot;))<br>            access_type = normalize_string(node.get(&quot;access_type&quot;))<br><br>            if table_name:<br>                rows_value = node.get(&quot;rows_examined_per_scan&quot;, node.get(&quot;rows&quot;))<br>                rows_join_value = node.get(&quot;rows_produced_per_join&quot;, node.get(&quot;rows&quot;))<br>                filtered_value = node.get(&quot;r_filtered&quot;, node.get(&quot;filtered&quot;))<br>                read_cost = normalize_string(cost_info.get(&quot;read_cost&quot;))<br>                eval_cost = normalize_string(cost_info.get(&quot;eval_cost&quot;))<br>                prefix_cost = normalize_string(cost_info.get(&quot;prefix_cost&quot;)) or inherited_prefix_cost<br>                data_read_per_join = normalize_string(cost_info.get(&quot;data_read_per_join&quot;))<br><br>                if not read_cost:<br>                    read_cost = normalize_float_string(node.get(&quot;r_table_time_ms&quot;))<br>                if not eval_cost:<br>                    eval_cost = normalize_float_string(node.get(&quot;r_other_time_ms&quot;))<br>                if not prefix_cost:<br>                    prefix_cost = normalize_float_string(node.get(&quot;r_total_time_ms&quot;)) or query_cost<br>                if not data_read_per_join:<br>                    pages_accessed = runtime_engine_stats.get(&quot;pages_accessed&quot;)<br>                    pages_read_count = runtime_engine_stats.get(&quot;pages_read_count&quot;)<br>                    if pages_accessed not in (None, &quot;&quot;):<br>                        data_read_per_join = format_bytes_human(float(pages_accessed) * 16384)<br>                    elif pages_read_count not in (None, &quot;&quot;):<br>                        data_read_per_join = format_bytes_human(float(pages_read_count) * 16384)<br><br>                steps.append(<br>                    {<br>                        &quot;event_id&quot;: int(event_id),<br>                        &quot;thread_id&quot;: int(thread_id),<br>                        &quot;step_id&quot;: step_id,<br>                        &quot;query_cost&quot;: query_cost,<br>                        &quot;table_name&quot;: table_name,<br>                        &quot;access_type&quot;: access_type or &quot;UNKNOWN&quot;,<br>                        &quot;rows_examined_per_scan&quot;: normalize_int(rows_value),<br>                        &quot;rows_produced_per_join&quot;: normalize_int(rows_join_value),<br>                        &quot;filtered&quot;: normalize_float_string(filtered_value) or normalize_string(filtered_value),<br>                        &quot;read_cost&quot;: read_cost,<br>                        &quot;eval_cost&quot;: eval_cost,<br>                        &quot;possible_keys&quot;: normalize_csv(node.get(&quot;possible_keys&quot;)),<br>                        &quot;key&quot;: normalize_string(node.get(&quot;key&quot;)),<br>                        &quot;used_key_parts&quot;: normalize_csv(node.get(&quot;used_key_parts&quot;)),<br>                        &quot;ref&quot;: normalize_csv(node.get(&quot;ref&quot;)),<br>                        &quot;prefix_cost&quot;: prefix_cost,<br>                        &quot;data_read_per_join&quot;: data_read_per_join,<br>                        &quot;using_index&quot;: normalize_string(node.get(&quot;using_index&quot;, False)),<br>                        &quot;key_length&quot;: normalize_string(node.get(&quot;key_length&quot;)),<br>                    }<br>                )<br>                step_id += 1<br><br>            for value in node.values():<br>                visit(value, query_cost, prefix_cost if table_name else inherited_prefix_cost)<br><br>        elif isinstance(node, list):<br>            for item in node:<br>                visit(item, inherited_query_cost, inherited_prefix_cost)<br><br>    visit(plan)<br>    return steps<br><br><br>def is_useful_execution_step(step: Dict[str, Any]) -&gt; bool:<br>    if normalize_int(step.get(&quot;event_id&quot;)) &lt;= 0:<br>        return False<br>    if normalize_int(step.get(&quot;thread_id&quot;)) &lt;= 0:<br>        return False<br>    if normalize_int(step.get(&quot;step_id&quot;)) &lt; 0:<br>        return False<br><br>    table_name = normalize_string(step.get(&quot;table_name&quot;)).strip()<br>    access_type = normalize_string(step.get(&quot;access_type&quot;)).strip()<br><br>    if not table_name:<br>        return False<br>    if table_name.lower() == &quot;other&quot;:<br>        return False<br>    if not access_type or access_type.upper() == &quot;UNKNOWN&quot;:<br>        return False<br><br>    has_rows = (<br>        normalize_int(step.get(&quot;rows_examined_per_scan&quot;)) &gt; 0<br>        or normalize_int(step.get(&quot;rows_produced_per_join&quot;)) &gt; 0<br>    )<br>    has_index_details = any(<br>        normalize_string(step.get(field)).strip()<br>        for field in (&quot;possible_keys&quot;, &quot;key&quot;, &quot;used_key_parts&quot;, &quot;ref&quot;)<br>    )<br>    has_cost_details = any(<br>        normalize_string(step.get(field)).strip()<br>        for field in (&quot;query_cost&quot;, &quot;read_cost&quot;, &quot;eval_cost&quot;, &quot;prefix_cost&quot;, &quot;data_read_per_join&quot;)<br>    )<br>    has_filter = normalize_string(step.get(&quot;filtered&quot;)).strip() not in (&quot;&quot;, &quot;0&quot;, &quot;0.0&quot;)<br><br>    return has_rows or has_index_details or has_cost_details or has_filter<br><br><br>def candidate_query_sql(<br>    table_name: str, limit: int, threshold_ms: float, database_filter: Optional[str]<br>) -&gt; str:<br>    where_parts = [<br>        &quot;CURRENT_SCHEMA IS NOT NULL&quot;,<br>        &quot;SQL_TEXT IS NOT NULL&quot;,<br>        &quot;SQL_TEXT &lt;&gt; &#39;&#39;&quot;,<br>        &quot;SQL_TEXT NOT LIKE &#39;%?%&#39;&quot;,<br>        &quot;(SQL_TEXT LIKE &#39;SELECT %&#39; OR SQL_TEXT LIKE &#39;WITH %&#39;)&quot;,<br>        f&quot;TIMER_WAIT / 1000000000 &gt; {threshold_ms}&quot;,<br>        &quot;COALESCE(MYSQL_ERRNO, 0) = 0&quot;,<br>        &quot;CURRENT_SCHEMA NOT IN (&#39;mysql&#39;, &#39;information_schema&#39;, &#39;performance_schema&#39;, &#39;sys&#39;)&quot;,<br>    ]<br>    if database_filter:<br>        escaped_db = database_filter.replace(&quot;&#39;&quot;, &quot;&#39;&#39;&quot;)<br>        where_parts.append(f&quot;CURRENT_SCHEMA = &#39;{escaped_db}&#39;&quot;)<br><br>    return f&quot;&quot;&quot;<br>SELECT<br>  COALESCE(DIGEST, &#39;&#39;) AS query_id,<br>  HEX(<br>    COALESCE(<br>      CASE<br>        WHEN CHAR_LENGTH(DIGEST_TEXT) &gt; 4000 THEN CONCAT(LEFT(DIGEST_TEXT, 3997), &#39;...&#39;)<br>        ELSE DIGEST_TEXT<br>      END,<br>      &#39;&#39;<br>    )<br>  ) AS query_text_hex,<br>  HEX(COALESCE(SQL_TEXT, &#39;&#39;)) AS query_sample_text_hex,<br>  EVENT_ID,<br>  THREAD_ID,<br>  ROUND(TIMER_WAIT / 1000000000, 3) AS execution_time_ms,<br>  COALESCE(ROWS_SENT, 0) AS rows_sent,<br>  COALESCE(ROWS_EXAMINED, 0) AS rows_examined,<br>  COALESCE(CURRENT_SCHEMA, &#39;&#39;) AS database_name<br>FROM performance_schema.{table_name}<br>WHERE {&quot; AND &quot;.join(where_parts)}<br>ORDER BY EVENT_ID DESC<br>LIMIT {int(limit)};<br>&quot;&quot;&quot;.strip()<br><br><br>def slow_queries_sql(fetch_interval_seconds: int, query_count_threshold: int, database_filter: Optional[str]) -&gt; str:<br>    where_parts = [<br>        &quot;CONVERT_TZ(LAST_SEEN, @@session.time_zone, &#39;+00:00&#39;) &gt;= UTC_TIMESTAMP() - INTERVAL {0} SECOND&quot;.format(<br>            int(fetch_interval_seconds)<br>        ),<br>        &quot;SCHEMA_NAME IS NOT NULL&quot;,<br>        &quot;SCHEMA_NAME NOT IN (&#39;mysql&#39;, &#39;information_schema&#39;, &#39;performance_schema&#39;, &#39;sys&#39;)&quot;,<br>    ]<br>    if database_filter:<br>        escaped_db = database_filter.replace(&quot;&#39;&quot;, &quot;&#39;&#39;&quot;)<br>        where_parts.append(&quot;SCHEMA_NAME = &#39;{0}&#39;&quot;.format(escaped_db))<br><br>    return &quot;&quot;&quot;<br>SELECT<br>  COALESCE(DIGEST, &#39;&#39;) AS query_id,<br>  HEX(<br>    COALESCE(<br>      CASE<br>        WHEN CHAR_LENGTH(DIGEST_TEXT) &gt; 4000 THEN CONCAT(LEFT(DIGEST_TEXT, 3997), &#39;...&#39;)<br>        ELSE DIGEST_TEXT<br>      END,<br>      &#39;&#39;<br>    )<br>  ) AS query_text_hex,<br>  COALESCE(SCHEMA_NAME, &#39;&#39;) AS database_name,<br>  COALESCE(COUNT_STAR, 0) AS execution_count,<br>  0 AS avg_cpu_time_ms,<br>  ROUND((SUM_TIMER_WAIT / NULLIF(COUNT_STAR, 0)) / 1000000000, 3) AS avg_elapsed_time_ms,<br>  COALESCE(SUM_ROWS_EXAMINED / NULLIF(COUNT_STAR, 0), 0) AS avg_disk_reads,<br>  COALESCE(SUM_ROWS_AFFECTED / NULLIF(COUNT_STAR, 0), 0) AS avg_disk_writes,<br>  CASE<br>    WHEN SUM_NO_INDEX_USED &gt; 0 THEN &#39;Yes&#39;<br>    ELSE &#39;No&#39;<br>  END AS has_full_table_scan,<br>  CASE<br>    WHEN DIGEST_TEXT LIKE &#39;SELECT%&#39; THEN &#39;SELECT&#39;<br>    WHEN DIGEST_TEXT LIKE &#39;INSERT%&#39; THEN &#39;INSERT&#39;<br>    WHEN DIGEST_TEXT LIKE &#39;UPDATE%&#39; THEN &#39;UPDATE&#39;<br>    WHEN DIGEST_TEXT LIKE &#39;DELETE%&#39; THEN &#39;DELETE&#39;<br>    ELSE &#39;OTHER&#39;<br>  END AS statement_type,<br>  DATE_FORMAT(CONVERT_TZ(LAST_SEEN, @@session.time_zone, &#39;+00:00&#39;), &#39;%Y-%m-%dT%H:%i:%sZ&#39;) AS last_execution_timestamp,<br>  DATE_FORMAT(UTC_TIMESTAMP(), &#39;%Y-%m-%dT%H:%i:%sZ&#39;) AS collection_timestamp<br>FROM performance_schema.events_statements_summary_by_digest<br>WHERE {0}<br>ORDER BY avg_elapsed_time_ms DESC<br>LIMIT {1};<br>&quot;&quot;&quot;.format(&quot; AND &quot;.join(where_parts), int(query_count_threshold)).strip()<br><br><br>def fetch_slow_query_summaries(args: argparse.Namespace) -&gt; List[Dict[str, Any]]:<br>    sql = slow_queries_sql(args.fetch_interval_seconds, args.query_count_threshold, args.database_filter)<br>    raw = mysql_command(<br>        host=args.host,<br>        port=args.port,<br>        user=args.user,<br>        password=args.password,<br>        database=None,<br>        sql=sql,<br>    )<br><br>    summaries: List[Dict[str, Any]] = []<br>    for line in raw.splitlines():<br>        if not line.strip():<br>            continue<br>        parts = line.split(&quot;\t&quot;)<br>        if len(parts) != 12:<br>            continue<br><br>        (<br>            query_id,<br>            query_text_hex,<br>            database_name,<br>            execution_count,<br>            avg_cpu_time_ms,<br>            avg_elapsed_time_ms,<br>            avg_disk_reads,<br>            avg_disk_writes,<br>            has_full_table_scan,<br>            statement_type,<br>            last_execution_timestamp,<br>            collection_timestamp,<br>        ) = parts<br><br>        summaries.append(<br>            {<br>                &quot;query_id&quot;: query_id,<br>                &quot;query_text&quot;: hexdecode_text(query_text_hex),<br>                &quot;database_name&quot;: database_name,<br>                &quot;schema_name&quot;: database_name,<br>                &quot;execution_count&quot;: normalize_int(execution_count),<br>                &quot;avg_cpu_time_ms&quot;: float(avg_cpu_time_ms or 0),<br>                &quot;avg_elapsed_time_ms&quot;: float(avg_elapsed_time_ms or 0),<br>                &quot;avg_disk_reads&quot;: float(avg_disk_reads or 0),<br>                &quot;avg_disk_writes&quot;: float(avg_disk_writes or 0),<br>                &quot;has_full_table_scan&quot;: has_full_table_scan,<br>                &quot;statement_type&quot;: statement_type,<br>                &quot;last_execution_timestamp&quot;: last_execution_timestamp,<br>                &quot;collection_timestamp&quot;: collection_timestamp,<br>            }<br>        )<br>    return summaries<br><br><br>def fetch_candidates(args: argparse.Namespace, allowed_query_ids: Optional[Set[str]] = None) -&gt; List[Dict[str, Any]]:<br>    candidates: List[Dict[str, Any]] = []<br>    seen: Set[Tuple[int, int]] = set()<br><br>    for table_name in (<br>        &quot;events_statements_current&quot;,<br>        &quot;events_statements_history&quot;,<br>        &quot;events_statements_history_long&quot;,<br>    ):<br>        sql = candidate_query_sql(table_name, args.limit, args.threshold_ms, args.database_filter)<br>        raw = mysql_command(<br>            host=args.host,<br>            port=args.port,<br>            user=args.user,<br>            password=args.password,<br>            database=None,<br>            sql=sql,<br>        )<br>        debug(&quot;table&quot;, table_name, &quot;rows&quot;, len([line for line in raw.splitlines() if line.strip()]))<br><br>        for line in raw.splitlines():<br>            if not line.strip():<br>                continue<br>            parts = line.split(&quot;\t&quot;)<br>            if len(parts) != 9:<br>                continue<br><br>            (<br>                query_id,<br>                query_text_hex,<br>                query_sample_text_hex,<br>                event_id,<br>                thread_id,<br>                execution_time_ms,<br>                rows_sent,<br>                rows_examined,<br>                database_name,<br>            ) = parts<br>            event_key = (normalize_int(event_id), normalize_int(thread_id))<br>            if event_key[0] &lt;= 0 or event_key[1] &lt;= 0:<br>                continue<br>            if event_key in seen:<br>                continue<br><br>            digest_text = hexdecode_text(query_text_hex).strip()<br>            query_sample_text = hexdecode_text(query_sample_text_hex).strip()<br>            if not query_sample_text:<br>                continue<br>            if is_noise_query(query_sample_text) or is_noise_query(digest_text):<br>                continue<br>            if allowed_query_ids is not None and query_id not in allowed_query_ids:<br>                continue<br><br>            seen.add(event_key)<br>            candidates.append(<br>                {<br>                    &quot;query_id&quot;: query_id,<br>                    &quot;query_text&quot;: digest_text,<br>                    &quot;query_sample_text&quot;: query_sample_text,<br>                    &quot;event_id&quot;: event_key[0],<br>                    &quot;thread_id&quot;: event_key[1],<br>                    &quot;execution_time_ms&quot;: float(execution_time_ms or 0),<br>                    &quot;rows_sent&quot;: normalize_int(rows_sent),<br>                    &quot;rows_examined&quot;: normalize_int(rows_examined),<br>                    &quot;database_name&quot;: database_name,<br>                }<br>            )<br>            debug(&quot;candidate&quot;, table_name, event_key, query_sample_text[:180].replace(&quot;\n&quot;, &quot;\\n&quot;))<br><br>            if len(candidates) &gt;= args.limit:<br>                return candidates[: args.limit]<br><br>    return candidates<br><br><br>def explain_query(args: argparse.Namespace, database_name: str, query_text: str) -&gt; Dict[str, Any]:<br>    commands: List[str]<br>    if args.plan_command_mode == &quot;auto&quot;:<br>        commands = [&quot;ANALYZE FORMAT=JSON&quot;, &quot;EXPLAIN FORMAT=JSON&quot;]<br>    elif args.plan_command_mode == &quot;analyze&quot;:<br>        commands = [&quot;ANALYZE FORMAT=JSON&quot;]<br>    else:<br>        commands = [&quot;EXPLAIN FORMAT=JSON&quot;]<br><br>    last_error: Optional[Exception] = None<br>    for command_prefix in commands:<br>        try:<br>            raw = mysql_command(<br>                host=args.host,<br>                port=args.port,<br>                user=args.user,<br>                password=args.password,<br>                database=database_name or None,<br>                sql=f&quot;{command_prefix} {query_text}&quot;,<br>            )<br>            return parse_explain_json(raw)<br>        except Exception as exc:<br>            last_error = exc<br>            debug(&quot;plan command failed&quot;, command_prefix, str(exc))<br><br>    if last_error:<br>        raise last_error<br>    raise RuntimeError(&quot;unable to produce query plan&quot;)<br><br><br>def build_events(args: argparse.Namespace) -&gt; List[Dict[str, Any]]:<br>    candidates = fetch_candidates(args)<br>    if not candidates:<br>        return []<br><br>    explain_cache: Dict[Tuple[str, str], List[Dict[str, Any]]] = {}<br>    events: List[Dict[str, Any]] = []<br><br>    for candidate in candidates:<br>        cache_key = (candidate[&quot;database_name&quot;], candidate[&quot;query_sample_text&quot;])<br>        alias_map = build_table_alias_map(candidate[&quot;query_sample_text&quot;])<br><br>        if cache_key not in explain_cache:<br>            try:<br>                plan = explain_query(args, candidate[&quot;database_name&quot;], candidate[&quot;query_sample_text&quot;])<br>                explain_cache[cache_key] = extract_plan_steps(plan, 0, 0)<br>                debug(<br>                    &quot;explain ok&quot;,<br>                    candidate[&quot;event_id&quot;],<br>                    candidate[&quot;thread_id&quot;],<br>                    &quot;steps&quot;,<br>                    len(explain_cache[cache_key]),<br>                )<br>            except Exception:<br>                explain_cache[cache_key] = []<br>                debug(<br>                    &quot;explain failed&quot;,<br>                    candidate[&quot;event_id&quot;],<br>                    candidate[&quot;thread_id&quot;],<br>                    candidate[&quot;query_sample_text&quot;][:200].replace(&quot;\n&quot;, &quot;\\n&quot;),<br>                )<br>                debug(traceback.format_exc().strip())<br><br>        plan_steps = explain_cache[cache_key]<br>        if not plan_steps:<br>            continue<br><br>        for step in plan_steps:<br>            cloned = dict(step)<br>            cloned[&quot;event_id&quot;] = candidate[&quot;event_id&quot;]<br>            cloned[&quot;thread_id&quot;] = candidate[&quot;thread_id&quot;]<br>            cloned[&quot;query_id&quot;] = candidate[&quot;query_id&quot;]<br>            cloned[&quot;query_text&quot;] = candidate[&quot;query_text&quot;] or candidate[&quot;query_sample_text&quot;]<br>            cloned[&quot;query_sample_text&quot;] = candidate[&quot;query_sample_text&quot;]<br>            cloned[&quot;database_name&quot;] = candidate[&quot;database_name&quot;]<br>            cloned[&quot;schema_name&quot;] = candidate[&quot;database_name&quot;]<br>            cloned[&quot;statement_type&quot;] = (<br>                candidate[&quot;query_sample_text&quot;].split(None, 1)[0].upper()<br>                if candidate[&quot;query_sample_text&quot;].split(None, 1)<br>                else &quot;UNKNOWN&quot;<br>            )<br>            original_table_name = clean_identifier(cloned.get(&quot;table_name&quot;))<br>            resolved_table_name = alias_map.get(original_table_name.lower(), &quot;&quot;)<br>            if resolved_table_name and resolved_table_name != original_table_name:<br>                cloned[&quot;table_alias&quot;] = original_table_name<br>                cloned[&quot;table_name&quot;] = resolved_table_name<br>            if not is_useful_execution_step(cloned):<br>                debug(<br>                    &quot;drop execution step&quot;,<br>                    cloned.get(&quot;event_id&quot;),<br>                    cloned.get(&quot;thread_id&quot;),<br>                    cloned.get(&quot;step_id&quot;),<br>                    cloned.get(&quot;table_name&quot;),<br>                    cloned.get(&quot;access_type&quot;),<br>                )<br>                continue<br>            events.append(cloned)<br><br>    return events<br><br><br>def build_individual_query_events(args: argparse.Namespace) -&gt; List[Dict[str, Any]]:<br>    candidates = fetch_candidates(args)<br>    if not candidates:<br>        return []<br><br>    events: List[Dict[str, Any]] = []<br>    for candidate in candidates:<br>        events.append(<br>            {<br>                &quot;query_id&quot;: candidate[&quot;query_id&quot;],<br>                &quot;query_text&quot;: candidate[&quot;query_text&quot;] or candidate[&quot;query_sample_text&quot;],<br>                &quot;event_id&quot;: candidate[&quot;event_id&quot;],<br>                &quot;thread_id&quot;: candidate[&quot;thread_id&quot;],<br>                &quot;execution_time_ms&quot;: candidate[&quot;execution_time_ms&quot;],<br>                &quot;rows_sent&quot;: candidate[&quot;rows_sent&quot;],<br>                &quot;rows_examined&quot;: candidate[&quot;rows_examined&quot;],<br>                &quot;database_name&quot;: candidate[&quot;database_name&quot;],<br>            }<br>        )<br>    return events<br><br><br>def build_slow_query_events(args: argparse.Namespace) -&gt; List[Dict[str, Any]]:<br>    candidates = fetch_candidates(args)<br>    if not candidates:<br>        return []<br><br>    grouped: Dict[str, Dict[str, Any]] = {}<br>    for candidate in candidates:<br>        query_id = candidate.get(&quot;query_id&quot;) or &quot;&quot;<br>        query_text = candidate.get(&quot;query_text&quot;) or candidate.get(&quot;query_sample_text&quot;) or &quot;&quot;<br>        group_key = query_id or query_text<br>        if not group_key:<br>            continue<br><br>        if group_key not in grouped:<br>            statement_type = (<br>                candidate[&quot;query_sample_text&quot;].split(None, 1)[0].upper()<br>                if candidate.get(&quot;query_sample_text&quot;, &quot;&quot;).split(None, 1)<br>                else &quot;UNKNOWN&quot;<br>            )<br>            grouped[group_key] = {<br>                &quot;query_id&quot;: query_id,<br>                &quot;query_text&quot;: query_text,<br>                &quot;database_name&quot;: candidate[&quot;database_name&quot;],<br>                &quot;schema_name&quot;: candidate[&quot;database_name&quot;],<br>                &quot;execution_count&quot;: 0,<br>                &quot;avg_cpu_time_ms&quot;: 0.0,<br>                &quot;avg_elapsed_time_total_ms&quot;: 0.0,<br>                &quot;avg_disk_reads_total&quot;: 0.0,<br>                &quot;avg_disk_writes_total&quot;: 0.0,<br>                &quot;has_full_table_scan&quot;: &quot;Unknown&quot;,<br>                &quot;statement_type&quot;: statement_type,<br>                &quot;last_execution_timestamp&quot;: utc_now_iso(),<br>                &quot;collection_timestamp&quot;: utc_now_iso(),<br>            }<br><br>        grouped[group_key][&quot;execution_count&quot;] += 1<br>        grouped[group_key][&quot;avg_elapsed_time_total_ms&quot;] += float(candidate.get(&quot;execution_time_ms&quot;, 0) or 0)<br>        grouped[group_key][&quot;avg_disk_reads_total&quot;] += float(candidate.get(&quot;rows_examined&quot;, 0) or 0)<br><br>    events: List[Dict[str, Any]] = []<br>    for summary in grouped.values():<br>        count = max(int(summary[&quot;execution_count&quot;]), 1)<br>        events.append(<br>            {<br>                &quot;query_id&quot;: summary[&quot;query_id&quot;],<br>                &quot;query_text&quot;: summary[&quot;query_text&quot;],<br>                &quot;database_name&quot;: summary[&quot;database_name&quot;],<br>                &quot;schema_name&quot;: summary[&quot;schema_name&quot;],<br>                &quot;execution_count&quot;: summary[&quot;execution_count&quot;],<br>                &quot;avg_cpu_time_ms&quot;: 0.0,<br>                &quot;avg_elapsed_time_ms&quot;: round(summary[&quot;avg_elapsed_time_total_ms&quot;] / count, 3),<br>                &quot;avg_disk_reads&quot;: round(summary[&quot;avg_disk_reads_total&quot;] / count, 3),<br>                &quot;avg_disk_writes&quot;: 0.0,<br>                &quot;has_full_table_scan&quot;: summary[&quot;has_full_table_scan&quot;],<br>                &quot;statement_type&quot;: summary[&quot;statement_type&quot;],<br>                &quot;last_execution_timestamp&quot;: summary[&quot;last_execution_timestamp&quot;],<br>                &quot;collection_timestamp&quot;: summary[&quot;collection_timestamp&quot;],<br>            }<br>        )<br><br>    events.sort(key=lambda item: item.get(&quot;avg_elapsed_time_ms&quot;, 0), reverse=True)<br>    return events[: max(args.query_count_threshold, args.limit)]<br><br><br>def self_test() -&gt; int:<br>    sample = {<br>        &quot;query_block&quot;: {<br>            &quot;select_id&quot;: 1,<br>            &quot;nested_loop&quot;: [<br>                {<br>                    &quot;table&quot;: {<br>                        &quot;table_name&quot;: &quot;tb_contratos&quot;,<br>                        &quot;access_type&quot;: &quot;ALL&quot;,<br>                        &quot;possible_keys&quot;: [&quot;PRIMARY&quot;],<br>                        &quot;key&quot;: &quot;PRIMARY&quot;,<br>                        &quot;key_length&quot;: &quot;4&quot;,<br>                        &quot;rows&quot;: 42,<br>                        &quot;filtered&quot;: 100,<br>                        &quot;using_index&quot;: True,<br>                        &quot;cost_info&quot;: {<br>                            &quot;query_cost&quot;: &quot;12.40&quot;,<br>                            &quot;read_cost&quot;: &quot;11.10&quot;,<br>                            &quot;eval_cost&quot;: &quot;1.30&quot;,<br>                            &quot;prefix_cost&quot;: &quot;12.40&quot;,<br>                            &quot;data_read_per_join&quot;: &quot;16K&quot;<br>                        }<br>                    }<br>                }<br>            ]<br>        }<br>    }<br>    events = extract_plan_steps(sample, 12345, 67890)<br>    print(json.dumps(events, indent=2))<br>    return 0 if events else 1<br><br><br>def parse_args(argv: List[str]) -&gt; argparse.Namespace:<br>    parser = argparse.ArgumentParser()<br>    parser.add_argument(&quot;--host&quot;, default=os.getenv(&quot;MYSQL_HOST&quot;, &quot;127.0.0.1&quot;))<br>    parser.add_argument(&quot;--port&quot;, default=os.getenv(&quot;MYSQL_PORT&quot;, &quot;3306&quot;))<br>    parser.add_argument(&quot;--user&quot;, default=os.getenv(&quot;MYSQL_USER&quot;, &quot;newrelic&quot;))<br>    parser.add_argument(&quot;--password&quot;, default=os.getenv(&quot;MYSQL_PASSWORD&quot;, &quot;&quot;))<br>    parser.add_argument(&quot;--database-filter&quot;, default=os.getenv(&quot;MYSQL_DATABASE_FILTER&quot;, &quot;&quot;))<br>    parser.add_argument(&quot;--limit&quot;, type=int, default=int(os.getenv(&quot;MYSQL_QUERY_PLAN_LIMIT&quot;, &quot;20&quot;)))<br>    parser.add_argument(&quot;--threshold-ms&quot;, type=float, default=float(os.getenv(&quot;MYSQL_QUERY_PLAN_THRESHOLD_MS&quot;, &quot;1&quot;)))<br>    parser.add_argument(<br>        &quot;--fetch-interval-seconds&quot;,<br>        type=int,<br>        default=int(os.getenv(&quot;MYSQL_SLOW_QUERY_FETCH_INTERVAL_SECONDS&quot;, &quot;300&quot;)),<br>    )<br>    parser.add_argument(<br>        &quot;--query-count-threshold&quot;,<br>        type=int,<br>        default=int(os.getenv(&quot;MYSQL_QUERY_MONITORING_COUNT_THRESHOLD&quot;, &quot;20&quot;)),<br>    )<br>    parser.add_argument(<br>        &quot;--mode&quot;,<br>        default=os.getenv(&quot;MYSQL_QUERY_PLAN_MODE&quot;, &quot;query_execution&quot;),<br>        choices=(&quot;query_execution&quot;, &quot;individual_queries&quot;, &quot;slow_queries&quot;),<br>    )<br>    parser.add_argument(<br>        &quot;--plan-command-mode&quot;,<br>        default=os.getenv(&quot;MYSQL_QUERY_PLAN_COMMAND_MODE&quot;, &quot;auto&quot;),<br>        choices=(&quot;auto&quot;, &quot;analyze&quot;, &quot;explain&quot;),<br>    )<br>    parser.add_argument(&quot;--self-test&quot;, action=&quot;store_true&quot;)<br>    return parser.parse_args(argv)<br><br><br>def main(argv: List[str]) -&gt; int:<br>    args = parse_args(argv)<br>    if args.self_test:<br>        return self_test()<br><br>    if not args.password:<br>        print(&quot;[]&quot;)<br>        return 0<br><br>    try:<br>        if args.mode == &quot;individual_queries&quot;:<br>            events = build_individual_query_events(args)<br>        elif args.mode == &quot;slow_queries&quot;:<br>            events = build_slow_query_events(args)<br>        else:<br>            events = build_events(args)<br>    except Exception:<br>        debug(&quot;build_events failed&quot;)<br>        debug(traceback.format_exc().strip())<br>        print(&quot;[]&quot;)<br>        return 0<br><br>    print(json.dumps(events))<br>    return 0<br><br><br>if __name__ == &quot;__main__&quot;:<br>    raise SystemExit(main(sys.argv[1:]))</pre><p>At this point, the mechanical part is solved. What remains is showing how this is deployed and validated without exposing any sensitive data. First, I suggest defining the environment variables explicitly.</p><pre>export NR_MYSQL_HOST=&quot;127.0.0.1&quot;<br>export NR_MYSQL_PORT=&quot;3307&quot;<br>export NR_MYSQL_USER=&quot;your_user&quot;<br>export NR_MYSQL_PASSWORD=&quot;your_password&quot;<br>export NR_MYSQL_DATABASE=&quot;your_database&quot;<br>export NR_ENVIRONMENT=&quot;production&quot;<br>export NR_ROLE=&quot;mariadb-3307&quot;<br>export NR_APPLICATION_NAME=&quot;Your Application&quot;<br>export NR_ENTITY_GUID=&quot;YOUR_ENTITY_GUID&quot;<br>export NR_ENTITY_NAME=&quot;node:your-host:3307&quot;<br>export NR_DISPLAY_NAME=&quot;your-host&quot;<br>export NR_QUERY_PLAN_LIMIT=&quot;20&quot;<br>export NR_QUERY_PLAN_THRESHOLD_MS=&quot;0&quot;<br>export NR_SLOW_QUERY_FETCH_INTERVAL_SECONDS=&quot;300&quot;<br>export NR_QUERY_MONITORING_COUNT_THRESHOLD=&quot;20&quot;</pre><p>Then copy both files to the correct paths on the server.</p><pre>sudo cp mariadb_query_execution.py /usr/local/bin/mariadb_query_execution.py<br>sudo chmod 755 /usr/local/bin/mariadb_query_execution.py<br>sudo cp mariadb-config.yml /etc/newrelic-infra/integrations.d/mariadb-config.yml</pre><p>After that, I validated the script locally before restarting the agent. That step mattered because it removed the risk of debugging the interface while there was still a basic issue in the event generator itself.</p><pre>python3 -m py_compile /usr/local/bin/mariadb_query_execution.py</pre><p>Then I manually tested all three modes.</p><pre>MYSQL_QUERY_PLAN_MODE=individual_queries python3 /usr/local/bin/mariadb_query_execution.py | python3 -m json.tool<br>MYSQL_QUERY_PLAN_MODE=slow_queries python3 /usr/local/bin/mariadb_query_execution.py | python3 -m json.tool<br>MYSQL_QUERY_PLAN_MODE=query_execution python3 /usr/local/bin/mariadb_query_execution.py | python3 -m json.tool</pre><p>Only after that did I let nri-flex load the configuration and restart the agent.</p><pre>sudo /usr/bin/nri-flex --config_path /etc/newrelic-infra/integrations.d/mariadb-config.yml --pretty --verbose<br>sudo systemctl restart newrelic-infra</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ldbYW2d6mlhPZk1XtXlPBA.png" /><figcaption>“Hack” configuration flow for MariaDB</figcaption></figure><p>The final validation in New Relic also stopped being purely visual. I wanted to confirm in NRQL whether the three event types existed, whether they were tied to the same entity, and whether what the interface was consuming was actually present in the event store</p><pre>FROM MysqlIndividualQueriesSample<br>SELECT count(*)<br>WHERE entityGuid = &#39;YOUR_ENTITY_GUID&#39;<br>AND error IS NULL<br>SINCE 5 minutes ago<br><br>FROM MysqlQueryExecutionSample<br>SELECT count(*)<br>WHERE entityGuid = &#39;YOUR_ENTITY_GUID&#39;<br>AND error IS NULL<br>SINCE 5 minutes ago<br><br>FROM MysqlSlowQueriesSample<br>SELECT count(*)<br>WHERE entityGuid = &#39;YOUR_ENTITY_GUID&#39;<br>AND error IS NULL<br>SINCE 5 minutes ago</pre><p>In the end, the interface’s own query became my strongest truth criterion. When it started returning the expected data, Query details stopped being empty. When Execution plan started building real steps with event_id, thread_id, and step_id, I knew the solution had stopped being an experiment and had become functional.</p><pre>SELECT latest(execution_time_ms), latest(query_id), latest(query_text), latest(rows_examined), latest(rows_sent)<br>FROM MysqlIndividualQueriesSample<br>WHERE entityGuid = &#39;YOUR_ENTITY_GUID&#39;<br>FACET event_id, thread_id<br>SINCE 30 minutes ago UNTIL now<br><br>SELECT latest(query_cost), latest(table_name), latest(access_type), latest(rows_examined_per_scan), latest(rows_produced_per_join), latest(filtered), latest(read_cost), latest(eval_cost), latest(prefix_cost), latest(data_read_per_join), latest(possible_keys), latest(key), latest(key_length), latest(used_key_parts), latest(ref), latest(using_index)<br>FROM MysqlQueryExecutionSample<br>WHERE entityGuid = &#39;YOUR_ENTITY_GUID&#39;<br>FACET event_id, thread_id, step_id<br>SINCE 30 minutes ago UNTIL now</pre><p>The biggest lesson from this experience was simple, but important. At first, I thought I was missing a flag. Then I thought I was missing a permission. Then I thought I was missing compatibility. In the end, what I was missing was coherence between events. That kind of detail does not usually appear in official setup guides because, in the supported scenario, it is already embedded in how the integration works. But when you need to adapt the flow for a database outside the vendor’s comfort zone, understanding the relationship between data stops being a luxury. It becomes the only way out of the dark.</p><blockquote>Real observability is not about installing an agent. Real observability is about being able to trust that the story your dashboard is telling matches what the system is actually doing. Without that, you did not gain visibility. You only gained another pretty screen.</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b95f55515609" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Como eu fiz o nri-mysql funcionar com MariaDB 10.5]]></title>
            <link>https://medium.com/@patrickotto.dev/como-eu-fiz-o-nri-mysql-funcionar-com-mariadb-10-5-40efdf0f1057?source=rss-daa0cab73ef2------2</link>
            <guid isPermaLink="false">https://medium.com/p/40efdf0f1057</guid>
            <dc:creator><![CDATA[Patrick Otto]]></dc:creator>
            <pubDate>Mon, 04 May 2026 15:37:56 GMT</pubDate>
            <atom:updated>2026-05-04T17:38:47.023Z</atom:updated>
            <content:encoded><![CDATA[<h3>Como eu fiz o nri-mysql funcionar com MariaDB 10.5 quando a integração oficial simplesmente não entregava o que prometia (New Relic)</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SzfauPJasVof-0qmAyfFbg.png" /></figure><blockquote>Foram 3 dias até a solução final. Não porque faltava uma flag, mas porque o problema real estava na forma como a telemetria nascia, se relacionava e era consumida pela UI do New Relic.</blockquote><p>Se você já perdeu horas tentando fazer observabilidade de banco funcionar fora do cenário oficialmente suportado, este texto pode te poupar um bom tempo de tentativa cega. O que eu vou mostrar aqui não é apenas uma configuração pronta. É o raciocínio que me levou até ela, o caminho que falhou, o que precisei validar no NRQL, o que a interface do New Relic realmente esperava receber e, no fim, a solução que fez o Query details e o Execution plan finalmente funcionarem com <strong>MariaDB 10.5</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DzHSFduGaoA_MOm0YQwdow.png" /><figcaption>Esse foi o primeiro sinal de que o problema não era só coleta. Parte da telemetria existia, mas a experiência final continuava quebrada.</figcaption></figure><p>Existe uma ideia muito comum em observabilidade de que, se a integração está instalada, o resto deveria simplesmente funcionar. Na prática, quase nunca é assim. Principalmente quando você sai do caminho mais confortável da documentação e tenta usar uma integração desenhada para um cenário específico em um banco que está perto, mas não exatamente dentro do que o fornecedor considera oficialmente suportado. Foi exatamente isso que aconteceu comigo ao tentar fazer o nri-mysql entregar a experiência completa de monitoramento para um ambiente com MariaDB 10.5.</p><p>O objetivo parecia simples. Eu queria que o New Relic mostrasse, de forma coerente, slow queries, individual query details e execution plan. Só que o comportamento real era outro. Uma parte funcionava. Outra funcionava parcialmente. E justamente a parte mais interessante, que era o Query details, continuava vazia. Quando isso acontece, o problema deixa de parecer ingestão e começa a parecer acoplamento interno da telemetria. Foi nesse momento que eu parei de tratar a situação como mais uma configuração de YAML e comecei a olhar para ela como deveria ter olhado desde o começo, como um problema de modelo de dados.</p><p>No início, a hipótese mais óbvia era culpar o MariaDB. Afinal, a documentação do fluxo de query performance do New Relic gira muito mais em torno de MySQL 8 do que de MariaDB 10.5. Então a ideia de incompatibilidade parecia natural. Só que havia um detalhe importante. O Wait time analysis funcionava. Isso significava que o New Relic estava, sim, recebendo parte da telemetria do banco. O ambiente não estava cego. O agente não estava quebrado. O acesso ao banco não estava errado. A coleta existia, mas a visão final continuava incompleta. E quando a coleta existe, mas a interface permanece vazia, o problema normalmente não está mais em conseguir ler o banco. Ele está em como os eventos se relacionam entre si.</p><p>Essa é a armadilha mais comum nesse tipo de troubleshooting. Quando a integração falha parcialmente, a tendência é insistir sempre na mesma direção. Trocar flag, mudar threshold, ativar mais métricas, reiniciar agente, testar de novo. Tudo isso resolve problemas de configuração. Mas não resolve problemas de semântica. No meu caso, o New Relic mostrava uma situação quase didática. Eu tinha eventos chegando para algumas visões. Eu tinha dados no NRQL. Eu tinha até planos de execução sendo gerados em formato customizado. Mas a página nativa continuava vazia. Isso era um ótimo indicativo de que a interface não queria apenas eventos existentes. Ela queria eventos coerentes entre si.</p><p>Foi aí que a investigação mudou de nível. Em vez de continuar clicando na interface esperando que algo destravasse, eu fui para o NRQL e comecei a verificar o que realmente existia no banco de eventos do New Relic. Primeiro eu confirmei que os eventos individuais existiam. Depois eu confirmei que os eventos de plano de execução também existiam. Em seguida percebi um detalhe decisivo. A página nativa estava filtrando por entityGuid. Ou seja, não bastava mandar entityName. A própria consulta interna da tela estava usando um identificador diferente. Quando eu entendi isso, uma peça importante caiu no lugar.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OU1qKHbP2gj7Bm3msnvQzg.png" /><figcaption>NRQL mostrando MysqlIndividualQueriesSample retornando dados e a diferença entre consultar com e sem entityGuid.</figcaption></figure><p>Mesmo depois de alinhar a entidade, a solução ainda não estava pronta. O topo da tela dependia de uma coisa. O plano de execução dependia de outra. E os dois precisavam conversar. No começo, eu tinha soluções parciais vindas de lugares diferentes. Um evento vindo de slow_log, outro vindo de performance_schema e outro gerado artificialmente com EXPLAIN FORMAT=JSON. Separadamente, tudo parecia razoável. Juntos, não. Porque a UI do New Relic não quer apenas três eventos com nomes corretos. Ela quer coerência entre eles. Ela quer o mesmo query_id, o mesmo event_id, o mesmo thread_id, a mesma entidade e a mesma janela temporal. Sem isso, a interface até recebe dado, mas não consegue montar a história completa.</p><p>Foi por essa razão que eu abandonei a ideia de misturar fontes. A solução só começou a ficar estável quando eu fiz os três tipos de evento nascerem da mesma origem lógica. O que funcionou melhor foi usar statements recentes de performance_schema.events_statements_current, performance_schema.events_statements_history e performance_schema.events_statements_history_long. A partir deles, eu consegui gerar <strong>MysqlIndividualQueriesSample, MysqlQueryExecutionSample</strong> e <strong>MysqlSlowQueriesSample</strong>. Tudo a partir do mesmo conjunto de consultas recentes. Isso foi importante porque permitiu alinhar os identificadores que a UI usa para montar a visão. Em vez de três telemetrias parecidas, eu passei a ter três perspectivas da mesma telemetria.</p><p>Quando essa parte finalmente começou a funcionar, apareceu outro problema. O Execution plan ainda parecia estranho. Alguns campos vinham zerados. Algumas tabelas apareciam pela metade. Alguns passos simplesmente sumiam. Mais uma vez, o banco não estava deixando de responder. O problema era o formato. O<strong> EXPLAIN FORMAT=JSON</strong> do MariaDB 10.5 devolve estruturas com chaves repetidas, como table, em blocos que um parser mais ingênuo sobrescreve silenciosamente. O dado estava vindo. O parser é que estava jogando parte dele fora sem perceber. Quando eu corrigi isso, o plano começou a aparecer com muito mais coerência. Não perfeito em todos os casos, mas funcional. E funcional já era muito melhor do que vazio.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lVxaCXmBoUEGE8hN9GOM0w.png" /><figcaption>Print do Execution plan aparecendo com step_id, table_name e access_type</figcaption></figure><p>Depois disso, surgiu mais um efeito colateral. A interface começou a mostrar queries que não interessavam para a análise da aplicação, como SELECT version(), SELECT sleep(…), consultas a information_schema, consultas a performance_schema, EXPLAIN FORMAT=JSON e comandos SET. Ou seja, eu tinha resolvido o pipeline, mas ainda não tinha resolvido a qualidade do painel. A saída foi filtrar esse ruído no próprio script. Não fazia sentido mandar para a visão final queries que existiam apenas por causa do próprio processo de inspeção ou do próprio monitoramento. A partir daí, a tela começou a mostrar o que realmente importava, que eram as consultas reais da aplicação.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZlkoZfPaac41EXDDl8_D3Q.png" /><figcaption>Print do nri-flex — pretty — verbose ou do script Python retornando JSON com os eventos já normalizados</figcaption></figure><p>No fim, a solução ficou bem menor do que o problema parecia sugerir. Eu mantive apenas dois arquivos, mariadb-config.yml e mariadb_query_execution.py. O primeiro define o nri-mysql-legacy e o nri-flex. O segundo usa o performance_schema, filtra ruído e gera os eventos compatíveis para a interface do New Relic. O que destravou o Query details não foi uma mágica. Foi a combinação entre coerência de origem, coerência de identificação da entidade e coerência entre os identificadores dos eventos.</p><p>Abaixo estão os dois arquivos finais, já reescritos com flags e placeholders para que qualquer pessoa consiga adaptar ao próprio ambiente sem expor usuário, senha, hostname real ou identificadores da própria conta.</p><h4>mariadb-config.yml</h4><pre>integrations:<br>  - name: nri-mysql-legacy<br>    executable: /var/db/newrelic-infra/newrelic-integrations/bin/nri-mysql-legacy<br>    env:<br>      HOSTNAME: &quot;${NR_MYSQL_HOST}&quot;<br>      PORT: ${NR_MYSQL_PORT}<br>      USERNAME: &quot;${NR_MYSQL_USER}&quot;<br>      PASSWORD: &quot;${NR_MYSQL_PASSWORD}&quot;<br>      REMOTE_MONITORING: true<br>      EXTENDED_METRICS: true<br>      EXTENDED_INNODB_METRICS: true<br>      ENABLE_QUERY_MONITORING: true<br>      QUERY_MONITORING_RESPONSE_TIME_THRESHOLD: 1<br>      QUERY_MONITORING_COUNT_THRESHOLD: 20<br>    interval: 30s<br>    labels:<br>      env: &quot;${NR_ENVIRONMENT}&quot;<br>      role: &quot;${NR_ROLE}&quot;<br>    inventory_source: config/mysql<br><br>  - name: nri-flex<br>    config:<br>      name: mariadbQueryTelemetry<br>      apis:<br>        - name: mariadbFakeIndividualQueries<br>          commands:<br>            - run: &gt;-<br>                sh -c &quot;MYSQL_HOST=${NR_MYSQL_HOST}<br>                MYSQL_PORT=${NR_MYSQL_PORT}<br>                MYSQL_USER=${NR_MYSQL_USER}<br>                MYSQL_PASSWORD=&#39;${NR_MYSQL_PASSWORD}&#39;<br>                MYSQL_DATABASE_FILTER=${NR_MYSQL_DATABASE}<br>                MYSQL_QUERY_PLAN_LIMIT=${NR_QUERY_PLAN_LIMIT:-20}<br>                MYSQL_QUERY_PLAN_THRESHOLD_MS=${NR_QUERY_PLAN_THRESHOLD_MS:-0}<br>                MYSQL_SLOW_QUERY_FETCH_INTERVAL_SECONDS=${NR_SLOW_QUERY_FETCH_INTERVAL_SECONDS:-300}<br>                MYSQL_QUERY_MONITORING_COUNT_THRESHOLD=${NR_QUERY_MONITORING_COUNT_THRESHOLD:-20}<br>                MYSQL_QUERY_PLAN_MODE=individual_queries<br>                python3 /usr/local/bin/mariadb_query_execution.py 2&gt;/dev/null || echo &#39;[]&#39;&quot;<br>          custom_attributes:<br>            application: &quot;${NR_APPLICATION_NAME}&quot;<br>            entityGuid: &quot;${NR_ENTITY_GUID}&quot;<br>            entityName: &quot;${NR_ENTITY_NAME}&quot;<br>            displayName: &quot;${NR_DISPLAY_NAME}&quot;<br>            hostname: &quot;${NR_MYSQL_HOST}&quot;<br>            port: &quot;${NR_MYSQL_PORT}&quot;<br>            db.instance: &quot;${NR_MYSQL_DATABASE}&quot;<br>            database.name: &quot;${NR_MYSQL_DATABASE}&quot;<br>          event_type: MysqlIndividualQueriesSample<br><br>        - name: mariadbSlowQueries<br>          commands:<br>            - run: &gt;-<br>                sh -c &quot;MYSQL_HOST=${NR_MYSQL_HOST}<br>                MYSQL_PORT=${NR_MYSQL_PORT}<br>                MYSQL_USER=${NR_MYSQL_USER}<br>                MYSQL_PASSWORD=&#39;${NR_MYSQL_PASSWORD}&#39;<br>                MYSQL_DATABASE_FILTER=${NR_MYSQL_DATABASE}<br>                MYSQL_QUERY_PLAN_LIMIT=${NR_QUERY_PLAN_LIMIT:-20}<br>                MYSQL_QUERY_PLAN_THRESHOLD_MS=${NR_QUERY_PLAN_THRESHOLD_MS:-0}<br>                MYSQL_SLOW_QUERY_FETCH_INTERVAL_SECONDS=${NR_SLOW_QUERY_FETCH_INTERVAL_SECONDS:-300}<br>                MYSQL_QUERY_MONITORING_COUNT_THRESHOLD=${NR_QUERY_MONITORING_COUNT_THRESHOLD:-20}<br>                MYSQL_QUERY_PLAN_MODE=slow_queries<br>                python3 /usr/local/bin/mariadb_query_execution.py 2&gt;/dev/null || echo &#39;[]&#39;&quot;<br>          custom_attributes:<br>            application: &quot;${NR_APPLICATION_NAME}&quot;<br>            entityGuid: &quot;${NR_ENTITY_GUID}&quot;<br>            entityName: &quot;${NR_ENTITY_NAME}&quot;<br>            displayName: &quot;${NR_DISPLAY_NAME}&quot;<br>            hostname: &quot;${NR_MYSQL_HOST}&quot;<br>            port: &quot;${NR_MYSQL_PORT}&quot;<br>            db.instance: &quot;${NR_MYSQL_DATABASE}&quot;<br>            database.name: &quot;${NR_MYSQL_DATABASE}&quot;<br>          event_type: MysqlSlowQueriesSample<br><br>        - name: mariadbFakeQueryExecutionPlans<br>          commands:<br>            - run: &gt;-<br>                sh -c &quot;MYSQL_HOST=${NR_MYSQL_HOST}<br>                MYSQL_PORT=${NR_MYSQL_PORT}<br>                MYSQL_USER=${NR_MYSQL_USER}<br>                MYSQL_PASSWORD=&#39;${NR_MYSQL_PASSWORD}&#39;<br>                MYSQL_DATABASE_FILTER=${NR_MYSQL_DATABASE}<br>                MYSQL_QUERY_PLAN_LIMIT=${NR_QUERY_PLAN_LIMIT:-20}<br>                MYSQL_QUERY_PLAN_THRESHOLD_MS=${NR_QUERY_PLAN_THRESHOLD_MS:-0}<br>                MYSQL_SLOW_QUERY_FETCH_INTERVAL_SECONDS=${NR_SLOW_QUERY_FETCH_INTERVAL_SECONDS:-300}<br>                MYSQL_QUERY_MONITORING_COUNT_THRESHOLD=${NR_QUERY_MONITORING_COUNT_THRESHOLD:-20}<br>                MYSQL_QUERY_PLAN_MODE=query_execution<br>                MYSQL_QUERY_PLAN_COMMAND_MODE=analyze<br>                python3 /usr/local/bin/mariadb_query_execution.py 2&gt;/dev/null || echo &#39;[]&#39;&quot;<br>          custom_attributes:<br>            application: &quot;${NR_APPLICATION_NAME}&quot;<br>            entityGuid: &quot;${NR_ENTITY_GUID}&quot;<br>            entityName: &quot;${NR_ENTITY_NAME}&quot;<br>            displayName: &quot;${NR_DISPLAY_NAME}&quot;<br>            hostname: &quot;${NR_MYSQL_HOST}&quot;<br>            port: &quot;${NR_MYSQL_PORT}&quot;<br>            db.instance: &quot;${NR_MYSQL_DATABASE}&quot;<br>            database.name: &quot;${NR_MYSQL_DATABASE}&quot;<br>          event_type: MysqlQueryExecutionSample</pre><h4>mariadb_query_execution.py</h4><pre>#!/usr/bin/env python3<br>&quot;&quot;&quot;<br>Generate fake MysqlQueryExecutionSample events for MariaDB by:<br>1. Reading recent SELECT/WITH statements from performance_schema<br>2. Running EXPLAIN FORMAT=JSON for each unique SQL text<br>3. Flattening MariaDB&#39;s JSON plan into step-based events that resemble<br>   New Relic&#39;s MysqlQueryExecutionSample shape<br><br>This is a compatibility workaround for environments where<br>MysqlIndividualQueriesSample and MysqlWaitEventsSample exist, but<br>MysqlQueryExecutionSample does not.<br>&quot;&quot;&quot;<br><br>import argparse<br>import binascii<br>from datetime import datetime<br>import json<br>import os<br>import re<br>import subprocess<br>import sys<br>import traceback<br>from typing import Any, Dict, List, Optional, Set, Tuple<br><br><br>DEBUG_ENABLED = os.getenv(&quot;MYSQL_QUERY_PLAN_DEBUG&quot;, &quot;&quot;).lower() in (&quot;1&quot;, &quot;true&quot;, &quot;yes&quot;, &quot;on&quot;)<br><br><br>def debug(*parts: Any) -&gt; None:<br>    if not DEBUG_ENABLED:<br>        return<br>    print(&quot;[mariadb_query_execution]&quot;, *parts, file=sys.stderr)<br><br><br>def mysql_command(<br>    *,<br>    host: str,<br>    port: str,<br>    user: str,<br>    password: str,<br>    database: Optional[str],<br>    sql: str,<br>) -&gt; str:<br>    cmd = [&quot;mysql&quot;, &quot;--raw&quot;, &quot;-N&quot;, &quot;-B&quot;, &quot;-h&quot;, host, &quot;-P&quot;, str(port), &quot;-u&quot;, user]<br>    if database:<br>        cmd.extend([&quot;-D&quot;, database])<br>    cmd.extend([&quot;-e&quot;, sql])<br><br>    env = os.environ.copy()<br>    env[&quot;MYSQL_PWD&quot;] = password<br><br>    proc = subprocess.run(<br>        cmd,<br>        stdout=subprocess.PIPE,<br>        stderr=subprocess.PIPE,<br>        universal_newlines=True,<br>        env=env,<br>        check=False,<br>    )<br>    if proc.returncode != 0:<br>        raise RuntimeError(proc.stderr.strip() or proc.stdout.strip() or &quot;mysql command failed&quot;)<br>    debug(&quot;mysql ok&quot;, &quot;db=&quot; + (database or &quot;&quot;), &quot;sql=&quot;, &quot; &quot;.join(sql.split())[:240])<br>    return proc.stdout<br><br><br>def hexdecode_text(value: str) -&gt; str:<br>    if not value:<br>        return &quot;&quot;<br>    return binascii.unhexlify(value.encode(&quot;ascii&quot;)).decode(&quot;utf-8&quot;, &quot;replace&quot;)<br><br><br>def normalize_string(value: Any) -&gt; str:<br>    if value is None:<br>        return &quot;&quot;<br>    if isinstance(value, bool):<br>        return &quot;true&quot; if value else &quot;false&quot;<br>    return str(value)<br><br><br>def normalize_csv(value: Any) -&gt; str:<br>    if value is None:<br>        return &quot;&quot;<br>    if isinstance(value, list):<br>        return &quot;,&quot;.join(str(item) for item in value)<br>    return str(value)<br><br><br>def normalize_int(value: Any) -&gt; int:<br>    if value in (None, &quot;&quot;):<br>        return 0<br>    try:<br>        return int(value)<br>    except Exception:<br>        try:<br>            return int(float(value))<br>        except Exception:<br>            return 0<br><br><br>def normalize_float_string(value: Any) -&gt; str:<br>    if value in (None, &quot;&quot;):<br>        return &quot;&quot;<br>    try:<br>        number = float(value)<br>    except Exception:<br>        return normalize_string(value)<br><br>    formatted = &quot;{0:.3f}&quot;.format(number).rstrip(&quot;0&quot;).rstrip(&quot;.&quot;)<br>    return formatted or &quot;0&quot;<br><br><br>def format_bytes_human(value: Any) -&gt; str:<br>    try:<br>        size = float(value)<br>    except Exception:<br>        return &quot;&quot;<br><br>    if size &lt;= 0:<br>        return &quot;&quot;<br><br>    units = (<br>        (1024 ** 3, &quot;G&quot;),<br>        (1024 ** 2, &quot;M&quot;),<br>        (1024, &quot;K&quot;),<br>    )<br>    for threshold, suffix in units:<br>        if size &gt;= threshold:<br>            scaled = size / threshold<br>            if abs(scaled - round(scaled)) &lt; 0.05:<br>                return &quot;{0}{1}&quot;.format(int(round(scaled)), suffix)<br>            return &quot;{0:.1f}{1}&quot;.format(scaled, suffix)<br>    return str(int(round(size)))<br><br><br>def utc_now_iso() -&gt; str:<br>    return datetime.utcnow().strftime(&quot;%Y-%m-%dT%H:%M:%SZ&quot;)<br><br><br>def clean_identifier(value: str) -&gt; str:<br>    text = normalize_string(value).strip()<br>    if not text:<br>        return &quot;&quot;<br>    text = text.replace(&quot;`&quot;, &quot;&quot;)<br>    if &quot;.&quot; in text:<br>        text = text.split(&quot;.&quot;)[-1]<br>    return text.strip()<br><br><br>def build_table_alias_map(query_text: str) -&gt; Dict[str, str]:<br>    text = &quot; &quot;.join((query_text or &quot;&quot;).replace(&quot;\n&quot;, &quot; &quot;).replace(&quot;\r&quot;, &quot; &quot;).split())<br>    if not text:<br>        return {}<br><br>    alias_map: Dict[str, str] = {}<br>    pattern = re.compile(<br>        r&quot;(?i)\b(?:from|join)\s+&quot;<br>        r&quot;((?:`[^`]+`|\w+)(?:\s*\.\s*(?:`[^`]+`|\w+))?)&quot;<br>        r&quot;(?:\s+(?:as\s+)?(`[^`]+`|\w+))?&quot;<br>    )<br><br>    for match in pattern.finditer(text):<br>        raw_table_name = match.group(1) or &quot;&quot;<br>        raw_alias = match.group(2) or &quot;&quot;<br><br>        table_name = clean_identifier(raw_table_name)<br>        alias = clean_identifier(raw_alias)<br>        if not table_name:<br>            continue<br><br>        alias_map[table_name.lower()] = table_name<br>        if alias:<br>            alias_map[alias.lower()] = table_name<br><br>    return alias_map<br><br><br>def is_noise_query(query_text: str) -&gt; bool:<br>    text = &quot; &quot;.join((query_text or &quot;&quot;).strip().lower().split())<br>    if not text:<br>        return True<br><br>    prefixes = (<br>        &quot;set &quot;,<br>        &quot;show &quot;,<br>        &quot;explain &quot;,<br>    )<br>    if text.startswith(prefixes):<br>        return True<br><br>    contains_any = (<br>        &quot;select version(&quot;,<br>        &quot;select `version`&quot;,<br>        &quot;@@version_comment&quot;,<br>        &quot;select sleep(&quot;,<br>        &quot;information_schema.&quot;,<br>        &quot;information_schema`&quot;,<br>        &quot;performance_schema.&quot;,<br>        &quot;performance_schema`&quot;,<br>        &quot;events_statements_summary_by_digest&quot;,<br>        &quot;innodb_trx&quot;,<br>    )<br>    return any(fragment in text for fragment in contains_any)<br><br><br>def preserve_duplicate_keys(pairs: List[Tuple[str, Any]]) -&gt; Dict[str, Any]:<br>    data: Dict[str, Any] = {}<br>    for key, value in pairs:<br>        if key in data:<br>            existing = data[key]<br>            if isinstance(existing, list):<br>                existing.append(value)<br>            else:<br>                data[key] = [existing, value]<br>        else:<br>            data[key] = value<br>    return data<br><br><br>def parse_explain_json(raw: str) -&gt; Dict[str, Any]:<br>    raw = raw.strip()<br>    if not raw:<br>        raise ValueError(&quot;empty EXPLAIN output&quot;)<br><br>    try:<br>        return json.loads(raw, object_pairs_hook=preserve_duplicate_keys)<br>    except json.JSONDecodeError:<br>        if &quot;\\n&quot; in raw or &quot;\\t&quot; in raw:<br>            try:<br>                return json.loads(<br>                    raw.encode(&quot;utf-8&quot;).decode(&quot;unicode_escape&quot;),<br>                    object_pairs_hook=preserve_duplicate_keys,<br>                )<br>            except Exception:<br>                pass<br>        start = raw.find(&quot;{&quot;)<br>        end = raw.rfind(&quot;}&quot;)<br>        if start == -1 or end == -1 or start &gt;= end:<br>            raise<br>        return json.loads(raw[start : end + 1], object_pairs_hook=preserve_duplicate_keys)<br><br><br>def extract_plan_steps(plan: Dict[str, Any], event_id: int, thread_id: int) -&gt; List[Dict[str, Any]]:<br>    steps: List[Dict[str, Any]] = []<br>    step_id = 0<br>    root_query_cost = &quot;&quot;<br><br>    root_query_block = plan.get(&quot;query_block&quot;) if isinstance(plan.get(&quot;query_block&quot;), dict) else {}<br>    if root_query_block:<br>        root_cost_info = root_query_block.get(&quot;cost_info&quot;) if isinstance(root_query_block.get(&quot;cost_info&quot;), dict) else {}<br>        root_query_cost = normalize_string(root_cost_info.get(&quot;query_cost&quot;))<br>        if not root_query_cost:<br>            root_query_cost = normalize_float_string(root_query_block.get(&quot;r_total_time_ms&quot;))<br><br>    def visit(node: Any, inherited_query_cost: str = &quot;&quot;, inherited_prefix_cost: str = &quot;&quot;) -&gt; None:<br>        nonlocal step_id<br><br>        if isinstance(node, dict):<br>            cost_info = node.get(&quot;cost_info&quot;) if isinstance(node.get(&quot;cost_info&quot;), dict) else {}<br>            runtime_engine_stats = node.get(&quot;r_engine_stats&quot;) if isinstance(node.get(&quot;r_engine_stats&quot;), dict) else {}<br><br>            query_cost = normalize_string(cost_info.get(&quot;query_cost&quot;)) or inherited_query_cost or root_query_cost<br>            if not query_cost:<br>                query_cost = normalize_float_string(node.get(&quot;r_total_time_ms&quot;))<br><br>            table_name = normalize_string(node.get(&quot;table_name&quot;))<br>            access_type = normalize_string(node.get(&quot;access_type&quot;))<br><br>            if table_name:<br>                rows_value = node.get(&quot;rows_examined_per_scan&quot;, node.get(&quot;rows&quot;))<br>                rows_join_value = node.get(&quot;rows_produced_per_join&quot;, node.get(&quot;rows&quot;))<br>                filtered_value = node.get(&quot;r_filtered&quot;, node.get(&quot;filtered&quot;))<br>                read_cost = normalize_string(cost_info.get(&quot;read_cost&quot;))<br>                eval_cost = normalize_string(cost_info.get(&quot;eval_cost&quot;))<br>                prefix_cost = normalize_string(cost_info.get(&quot;prefix_cost&quot;)) or inherited_prefix_cost<br>                data_read_per_join = normalize_string(cost_info.get(&quot;data_read_per_join&quot;))<br><br>                if not read_cost:<br>                    read_cost = normalize_float_string(node.get(&quot;r_table_time_ms&quot;))<br>                if not eval_cost:<br>                    eval_cost = normalize_float_string(node.get(&quot;r_other_time_ms&quot;))<br>                if not prefix_cost:<br>                    prefix_cost = normalize_float_string(node.get(&quot;r_total_time_ms&quot;)) or query_cost<br>                if not data_read_per_join:<br>                    pages_accessed = runtime_engine_stats.get(&quot;pages_accessed&quot;)<br>                    pages_read_count = runtime_engine_stats.get(&quot;pages_read_count&quot;)<br>                    if pages_accessed not in (None, &quot;&quot;):<br>                        data_read_per_join = format_bytes_human(float(pages_accessed) * 16384)<br>                    elif pages_read_count not in (None, &quot;&quot;):<br>                        data_read_per_join = format_bytes_human(float(pages_read_count) * 16384)<br><br>                steps.append(<br>                    {<br>                        &quot;event_id&quot;: int(event_id),<br>                        &quot;thread_id&quot;: int(thread_id),<br>                        &quot;step_id&quot;: step_id,<br>                        &quot;query_cost&quot;: query_cost,<br>                        &quot;table_name&quot;: table_name,<br>                        &quot;access_type&quot;: access_type or &quot;UNKNOWN&quot;,<br>                        &quot;rows_examined_per_scan&quot;: normalize_int(rows_value),<br>                        &quot;rows_produced_per_join&quot;: normalize_int(rows_join_value),<br>                        &quot;filtered&quot;: normalize_float_string(filtered_value) or normalize_string(filtered_value),<br>                        &quot;read_cost&quot;: read_cost,<br>                        &quot;eval_cost&quot;: eval_cost,<br>                        &quot;possible_keys&quot;: normalize_csv(node.get(&quot;possible_keys&quot;)),<br>                        &quot;key&quot;: normalize_string(node.get(&quot;key&quot;)),<br>                        &quot;used_key_parts&quot;: normalize_csv(node.get(&quot;used_key_parts&quot;)),<br>                        &quot;ref&quot;: normalize_csv(node.get(&quot;ref&quot;)),<br>                        &quot;prefix_cost&quot;: prefix_cost,<br>                        &quot;data_read_per_join&quot;: data_read_per_join,<br>                        &quot;using_index&quot;: normalize_string(node.get(&quot;using_index&quot;, False)),<br>                        &quot;key_length&quot;: normalize_string(node.get(&quot;key_length&quot;)),<br>                    }<br>                )<br>                step_id += 1<br><br>            for value in node.values():<br>                visit(value, query_cost, prefix_cost if table_name else inherited_prefix_cost)<br><br>        elif isinstance(node, list):<br>            for item in node:<br>                visit(item, inherited_query_cost, inherited_prefix_cost)<br><br>    visit(plan)<br>    return steps<br><br><br>def is_useful_execution_step(step: Dict[str, Any]) -&gt; bool:<br>    if normalize_int(step.get(&quot;event_id&quot;)) &lt;= 0:<br>        return False<br>    if normalize_int(step.get(&quot;thread_id&quot;)) &lt;= 0:<br>        return False<br>    if normalize_int(step.get(&quot;step_id&quot;)) &lt; 0:<br>        return False<br><br>    table_name = normalize_string(step.get(&quot;table_name&quot;)).strip()<br>    access_type = normalize_string(step.get(&quot;access_type&quot;)).strip()<br><br>    if not table_name:<br>        return False<br>    if table_name.lower() == &quot;other&quot;:<br>        return False<br>    if not access_type or access_type.upper() == &quot;UNKNOWN&quot;:<br>        return False<br><br>    has_rows = (<br>        normalize_int(step.get(&quot;rows_examined_per_scan&quot;)) &gt; 0<br>        or normalize_int(step.get(&quot;rows_produced_per_join&quot;)) &gt; 0<br>    )<br>    has_index_details = any(<br>        normalize_string(step.get(field)).strip()<br>        for field in (&quot;possible_keys&quot;, &quot;key&quot;, &quot;used_key_parts&quot;, &quot;ref&quot;)<br>    )<br>    has_cost_details = any(<br>        normalize_string(step.get(field)).strip()<br>        for field in (&quot;query_cost&quot;, &quot;read_cost&quot;, &quot;eval_cost&quot;, &quot;prefix_cost&quot;, &quot;data_read_per_join&quot;)<br>    )<br>    has_filter = normalize_string(step.get(&quot;filtered&quot;)).strip() not in (&quot;&quot;, &quot;0&quot;, &quot;0.0&quot;)<br><br>    return has_rows or has_index_details or has_cost_details or has_filter<br><br><br>def candidate_query_sql(<br>    table_name: str, limit: int, threshold_ms: float, database_filter: Optional[str]<br>) -&gt; str:<br>    where_parts = [<br>        &quot;CURRENT_SCHEMA IS NOT NULL&quot;,<br>        &quot;SQL_TEXT IS NOT NULL&quot;,<br>        &quot;SQL_TEXT &lt;&gt; &#39;&#39;&quot;,<br>        &quot;SQL_TEXT NOT LIKE &#39;%?%&#39;&quot;,<br>        &quot;(SQL_TEXT LIKE &#39;SELECT %&#39; OR SQL_TEXT LIKE &#39;WITH %&#39;)&quot;,<br>        f&quot;TIMER_WAIT / 1000000000 &gt; {threshold_ms}&quot;,<br>        &quot;COALESCE(MYSQL_ERRNO, 0) = 0&quot;,<br>        &quot;CURRENT_SCHEMA NOT IN (&#39;mysql&#39;, &#39;information_schema&#39;, &#39;performance_schema&#39;, &#39;sys&#39;)&quot;,<br>    ]<br>    if database_filter:<br>        escaped_db = database_filter.replace(&quot;&#39;&quot;, &quot;&#39;&#39;&quot;)<br>        where_parts.append(f&quot;CURRENT_SCHEMA = &#39;{escaped_db}&#39;&quot;)<br><br>    return f&quot;&quot;&quot;<br>SELECT<br>  COALESCE(DIGEST, &#39;&#39;) AS query_id,<br>  HEX(<br>    COALESCE(<br>      CASE<br>        WHEN CHAR_LENGTH(DIGEST_TEXT) &gt; 4000 THEN CONCAT(LEFT(DIGEST_TEXT, 3997), &#39;...&#39;)<br>        ELSE DIGEST_TEXT<br>      END,<br>      &#39;&#39;<br>    )<br>  ) AS query_text_hex,<br>  HEX(COALESCE(SQL_TEXT, &#39;&#39;)) AS query_sample_text_hex,<br>  EVENT_ID,<br>  THREAD_ID,<br>  ROUND(TIMER_WAIT / 1000000000, 3) AS execution_time_ms,<br>  COALESCE(ROWS_SENT, 0) AS rows_sent,<br>  COALESCE(ROWS_EXAMINED, 0) AS rows_examined,<br>  COALESCE(CURRENT_SCHEMA, &#39;&#39;) AS database_name<br>FROM performance_schema.{table_name}<br>WHERE {&quot; AND &quot;.join(where_parts)}<br>ORDER BY EVENT_ID DESC<br>LIMIT {int(limit)};<br>&quot;&quot;&quot;.strip()<br><br><br>def slow_queries_sql(fetch_interval_seconds: int, query_count_threshold: int, database_filter: Optional[str]) -&gt; str:<br>    where_parts = [<br>        &quot;CONVERT_TZ(LAST_SEEN, @@session.time_zone, &#39;+00:00&#39;) &gt;= UTC_TIMESTAMP() - INTERVAL {0} SECOND&quot;.format(<br>            int(fetch_interval_seconds)<br>        ),<br>        &quot;SCHEMA_NAME IS NOT NULL&quot;,<br>        &quot;SCHEMA_NAME NOT IN (&#39;mysql&#39;, &#39;information_schema&#39;, &#39;performance_schema&#39;, &#39;sys&#39;)&quot;,<br>    ]<br>    if database_filter:<br>        escaped_db = database_filter.replace(&quot;&#39;&quot;, &quot;&#39;&#39;&quot;)<br>        where_parts.append(&quot;SCHEMA_NAME = &#39;{0}&#39;&quot;.format(escaped_db))<br><br>    return &quot;&quot;&quot;<br>SELECT<br>  COALESCE(DIGEST, &#39;&#39;) AS query_id,<br>  HEX(<br>    COALESCE(<br>      CASE<br>        WHEN CHAR_LENGTH(DIGEST_TEXT) &gt; 4000 THEN CONCAT(LEFT(DIGEST_TEXT, 3997), &#39;...&#39;)<br>        ELSE DIGEST_TEXT<br>      END,<br>      &#39;&#39;<br>    )<br>  ) AS query_text_hex,<br>  COALESCE(SCHEMA_NAME, &#39;&#39;) AS database_name,<br>  COALESCE(COUNT_STAR, 0) AS execution_count,<br>  0 AS avg_cpu_time_ms,<br>  ROUND((SUM_TIMER_WAIT / NULLIF(COUNT_STAR, 0)) / 1000000000, 3) AS avg_elapsed_time_ms,<br>  COALESCE(SUM_ROWS_EXAMINED / NULLIF(COUNT_STAR, 0), 0) AS avg_disk_reads,<br>  COALESCE(SUM_ROWS_AFFECTED / NULLIF(COUNT_STAR, 0), 0) AS avg_disk_writes,<br>  CASE<br>    WHEN SUM_NO_INDEX_USED &gt; 0 THEN &#39;Yes&#39;<br>    ELSE &#39;No&#39;<br>  END AS has_full_table_scan,<br>  CASE<br>    WHEN DIGEST_TEXT LIKE &#39;SELECT%&#39; THEN &#39;SELECT&#39;<br>    WHEN DIGEST_TEXT LIKE &#39;INSERT%&#39; THEN &#39;INSERT&#39;<br>    WHEN DIGEST_TEXT LIKE &#39;UPDATE%&#39; THEN &#39;UPDATE&#39;<br>    WHEN DIGEST_TEXT LIKE &#39;DELETE%&#39; THEN &#39;DELETE&#39;<br>    ELSE &#39;OTHER&#39;<br>  END AS statement_type,<br>  DATE_FORMAT(CONVERT_TZ(LAST_SEEN, @@session.time_zone, &#39;+00:00&#39;), &#39;%Y-%m-%dT%H:%i:%sZ&#39;) AS last_execution_timestamp,<br>  DATE_FORMAT(UTC_TIMESTAMP(), &#39;%Y-%m-%dT%H:%i:%sZ&#39;) AS collection_timestamp<br>FROM performance_schema.events_statements_summary_by_digest<br>WHERE {0}<br>ORDER BY avg_elapsed_time_ms DESC<br>LIMIT {1};<br>&quot;&quot;&quot;.format(&quot; AND &quot;.join(where_parts), int(query_count_threshold)).strip()<br><br><br>def fetch_slow_query_summaries(args: argparse.Namespace) -&gt; List[Dict[str, Any]]:<br>    sql = slow_queries_sql(args.fetch_interval_seconds, args.query_count_threshold, args.database_filter)<br>    raw = mysql_command(<br>        host=args.host,<br>        port=args.port,<br>        user=args.user,<br>        password=args.password,<br>        database=None,<br>        sql=sql,<br>    )<br><br>    summaries: List[Dict[str, Any]] = []<br>    for line in raw.splitlines():<br>        if not line.strip():<br>            continue<br>        parts = line.split(&quot;\t&quot;)<br>        if len(parts) != 12:<br>            continue<br><br>        (<br>            query_id,<br>            query_text_hex,<br>            database_name,<br>            execution_count,<br>            avg_cpu_time_ms,<br>            avg_elapsed_time_ms,<br>            avg_disk_reads,<br>            avg_disk_writes,<br>            has_full_table_scan,<br>            statement_type,<br>            last_execution_timestamp,<br>            collection_timestamp,<br>        ) = parts<br><br>        summaries.append(<br>            {<br>                &quot;query_id&quot;: query_id,<br>                &quot;query_text&quot;: hexdecode_text(query_text_hex),<br>                &quot;database_name&quot;: database_name,<br>                &quot;schema_name&quot;: database_name,<br>                &quot;execution_count&quot;: normalize_int(execution_count),<br>                &quot;avg_cpu_time_ms&quot;: float(avg_cpu_time_ms or 0),<br>                &quot;avg_elapsed_time_ms&quot;: float(avg_elapsed_time_ms or 0),<br>                &quot;avg_disk_reads&quot;: float(avg_disk_reads or 0),<br>                &quot;avg_disk_writes&quot;: float(avg_disk_writes or 0),<br>                &quot;has_full_table_scan&quot;: has_full_table_scan,<br>                &quot;statement_type&quot;: statement_type,<br>                &quot;last_execution_timestamp&quot;: last_execution_timestamp,<br>                &quot;collection_timestamp&quot;: collection_timestamp,<br>            }<br>        )<br>    return summaries<br><br><br>def fetch_candidates(args: argparse.Namespace, allowed_query_ids: Optional[Set[str]] = None) -&gt; List[Dict[str, Any]]:<br>    candidates: List[Dict[str, Any]] = []<br>    seen: Set[Tuple[int, int]] = set()<br><br>    for table_name in (<br>        &quot;events_statements_current&quot;,<br>        &quot;events_statements_history&quot;,<br>        &quot;events_statements_history_long&quot;,<br>    ):<br>        sql = candidate_query_sql(table_name, args.limit, args.threshold_ms, args.database_filter)<br>        raw = mysql_command(<br>            host=args.host,<br>            port=args.port,<br>            user=args.user,<br>            password=args.password,<br>            database=None,<br>            sql=sql,<br>        )<br>        debug(&quot;table&quot;, table_name, &quot;rows&quot;, len([line for line in raw.splitlines() if line.strip()]))<br><br>        for line in raw.splitlines():<br>            if not line.strip():<br>                continue<br>            parts = line.split(&quot;\t&quot;)<br>            if len(parts) != 9:<br>                continue<br><br>            (<br>                query_id,<br>                query_text_hex,<br>                query_sample_text_hex,<br>                event_id,<br>                thread_id,<br>                execution_time_ms,<br>                rows_sent,<br>                rows_examined,<br>                database_name,<br>            ) = parts<br>            event_key = (normalize_int(event_id), normalize_int(thread_id))<br>            if event_key[0] &lt;= 0 or event_key[1] &lt;= 0:<br>                continue<br>            if event_key in seen:<br>                continue<br><br>            digest_text = hexdecode_text(query_text_hex).strip()<br>            query_sample_text = hexdecode_text(query_sample_text_hex).strip()<br>            if not query_sample_text:<br>                continue<br>            if is_noise_query(query_sample_text) or is_noise_query(digest_text):<br>                continue<br>            if allowed_query_ids is not None and query_id not in allowed_query_ids:<br>                continue<br><br>            seen.add(event_key)<br>            candidates.append(<br>                {<br>                    &quot;query_id&quot;: query_id,<br>                    &quot;query_text&quot;: digest_text,<br>                    &quot;query_sample_text&quot;: query_sample_text,<br>                    &quot;event_id&quot;: event_key[0],<br>                    &quot;thread_id&quot;: event_key[1],<br>                    &quot;execution_time_ms&quot;: float(execution_time_ms or 0),<br>                    &quot;rows_sent&quot;: normalize_int(rows_sent),<br>                    &quot;rows_examined&quot;: normalize_int(rows_examined),<br>                    &quot;database_name&quot;: database_name,<br>                }<br>            )<br>            debug(&quot;candidate&quot;, table_name, event_key, query_sample_text[:180].replace(&quot;\n&quot;, &quot;\\n&quot;))<br><br>            if len(candidates) &gt;= args.limit:<br>                return candidates[: args.limit]<br><br>    return candidates<br><br><br>def explain_query(args: argparse.Namespace, database_name: str, query_text: str) -&gt; Dict[str, Any]:<br>    commands: List[str]<br>    if args.plan_command_mode == &quot;auto&quot;:<br>        commands = [&quot;ANALYZE FORMAT=JSON&quot;, &quot;EXPLAIN FORMAT=JSON&quot;]<br>    elif args.plan_command_mode == &quot;analyze&quot;:<br>        commands = [&quot;ANALYZE FORMAT=JSON&quot;]<br>    else:<br>        commands = [&quot;EXPLAIN FORMAT=JSON&quot;]<br><br>    last_error: Optional[Exception] = None<br>    for command_prefix in commands:<br>        try:<br>            raw = mysql_command(<br>                host=args.host,<br>                port=args.port,<br>                user=args.user,<br>                password=args.password,<br>                database=database_name or None,<br>                sql=f&quot;{command_prefix} {query_text}&quot;,<br>            )<br>            return parse_explain_json(raw)<br>        except Exception as exc:<br>            last_error = exc<br>            debug(&quot;plan command failed&quot;, command_prefix, str(exc))<br><br>    if last_error:<br>        raise last_error<br>    raise RuntimeError(&quot;unable to produce query plan&quot;)<br><br><br>def build_events(args: argparse.Namespace) -&gt; List[Dict[str, Any]]:<br>    candidates = fetch_candidates(args)<br>    if not candidates:<br>        return []<br><br>    explain_cache: Dict[Tuple[str, str], List[Dict[str, Any]]] = {}<br>    events: List[Dict[str, Any]] = []<br><br>    for candidate in candidates:<br>        cache_key = (candidate[&quot;database_name&quot;], candidate[&quot;query_sample_text&quot;])<br>        alias_map = build_table_alias_map(candidate[&quot;query_sample_text&quot;])<br><br>        if cache_key not in explain_cache:<br>            try:<br>                plan = explain_query(args, candidate[&quot;database_name&quot;], candidate[&quot;query_sample_text&quot;])<br>                explain_cache[cache_key] = extract_plan_steps(plan, 0, 0)<br>                debug(<br>                    &quot;explain ok&quot;,<br>                    candidate[&quot;event_id&quot;],<br>                    candidate[&quot;thread_id&quot;],<br>                    &quot;steps&quot;,<br>                    len(explain_cache[cache_key]),<br>                )<br>            except Exception:<br>                explain_cache[cache_key] = []<br>                debug(<br>                    &quot;explain failed&quot;,<br>                    candidate[&quot;event_id&quot;],<br>                    candidate[&quot;thread_id&quot;],<br>                    candidate[&quot;query_sample_text&quot;][:200].replace(&quot;\n&quot;, &quot;\\n&quot;),<br>                )<br>                debug(traceback.format_exc().strip())<br><br>        plan_steps = explain_cache[cache_key]<br>        if not plan_steps:<br>            continue<br><br>        for step in plan_steps:<br>            cloned = dict(step)<br>            cloned[&quot;event_id&quot;] = candidate[&quot;event_id&quot;]<br>            cloned[&quot;thread_id&quot;] = candidate[&quot;thread_id&quot;]<br>            cloned[&quot;query_id&quot;] = candidate[&quot;query_id&quot;]<br>            cloned[&quot;query_text&quot;] = candidate[&quot;query_text&quot;] or candidate[&quot;query_sample_text&quot;]<br>            cloned[&quot;query_sample_text&quot;] = candidate[&quot;query_sample_text&quot;]<br>            cloned[&quot;database_name&quot;] = candidate[&quot;database_name&quot;]<br>            cloned[&quot;schema_name&quot;] = candidate[&quot;database_name&quot;]<br>            cloned[&quot;statement_type&quot;] = (<br>                candidate[&quot;query_sample_text&quot;].split(None, 1)[0].upper()<br>                if candidate[&quot;query_sample_text&quot;].split(None, 1)<br>                else &quot;UNKNOWN&quot;<br>            )<br>            original_table_name = clean_identifier(cloned.get(&quot;table_name&quot;))<br>            resolved_table_name = alias_map.get(original_table_name.lower(), &quot;&quot;)<br>            if resolved_table_name and resolved_table_name != original_table_name:<br>                cloned[&quot;table_alias&quot;] = original_table_name<br>                cloned[&quot;table_name&quot;] = resolved_table_name<br>            if not is_useful_execution_step(cloned):<br>                debug(<br>                    &quot;drop execution step&quot;,<br>                    cloned.get(&quot;event_id&quot;),<br>                    cloned.get(&quot;thread_id&quot;),<br>                    cloned.get(&quot;step_id&quot;),<br>                    cloned.get(&quot;table_name&quot;),<br>                    cloned.get(&quot;access_type&quot;),<br>                )<br>                continue<br>            events.append(cloned)<br><br>    return events<br><br><br>def build_individual_query_events(args: argparse.Namespace) -&gt; List[Dict[str, Any]]:<br>    candidates = fetch_candidates(args)<br>    if not candidates:<br>        return []<br><br>    events: List[Dict[str, Any]] = []<br>    for candidate in candidates:<br>        events.append(<br>            {<br>                &quot;query_id&quot;: candidate[&quot;query_id&quot;],<br>                &quot;query_text&quot;: candidate[&quot;query_text&quot;] or candidate[&quot;query_sample_text&quot;],<br>                &quot;event_id&quot;: candidate[&quot;event_id&quot;],<br>                &quot;thread_id&quot;: candidate[&quot;thread_id&quot;],<br>                &quot;execution_time_ms&quot;: candidate[&quot;execution_time_ms&quot;],<br>                &quot;rows_sent&quot;: candidate[&quot;rows_sent&quot;],<br>                &quot;rows_examined&quot;: candidate[&quot;rows_examined&quot;],<br>                &quot;database_name&quot;: candidate[&quot;database_name&quot;],<br>            }<br>        )<br>    return events<br><br><br>def build_slow_query_events(args: argparse.Namespace) -&gt; List[Dict[str, Any]]:<br>    candidates = fetch_candidates(args)<br>    if not candidates:<br>        return []<br><br>    grouped: Dict[str, Dict[str, Any]] = {}<br>    for candidate in candidates:<br>        query_id = candidate.get(&quot;query_id&quot;) or &quot;&quot;<br>        query_text = candidate.get(&quot;query_text&quot;) or candidate.get(&quot;query_sample_text&quot;) or &quot;&quot;<br>        group_key = query_id or query_text<br>        if not group_key:<br>            continue<br><br>        if group_key not in grouped:<br>            statement_type = (<br>                candidate[&quot;query_sample_text&quot;].split(None, 1)[0].upper()<br>                if candidate.get(&quot;query_sample_text&quot;, &quot;&quot;).split(None, 1)<br>                else &quot;UNKNOWN&quot;<br>            )<br>            grouped[group_key] = {<br>                &quot;query_id&quot;: query_id,<br>                &quot;query_text&quot;: query_text,<br>                &quot;database_name&quot;: candidate[&quot;database_name&quot;],<br>                &quot;schema_name&quot;: candidate[&quot;database_name&quot;],<br>                &quot;execution_count&quot;: 0,<br>                &quot;avg_cpu_time_ms&quot;: 0.0,<br>                &quot;avg_elapsed_time_total_ms&quot;: 0.0,<br>                &quot;avg_disk_reads_total&quot;: 0.0,<br>                &quot;avg_disk_writes_total&quot;: 0.0,<br>                &quot;has_full_table_scan&quot;: &quot;Unknown&quot;,<br>                &quot;statement_type&quot;: statement_type,<br>                &quot;last_execution_timestamp&quot;: utc_now_iso(),<br>                &quot;collection_timestamp&quot;: utc_now_iso(),<br>            }<br><br>        grouped[group_key][&quot;execution_count&quot;] += 1<br>        grouped[group_key][&quot;avg_elapsed_time_total_ms&quot;] += float(candidate.get(&quot;execution_time_ms&quot;, 0) or 0)<br>        grouped[group_key][&quot;avg_disk_reads_total&quot;] += float(candidate.get(&quot;rows_examined&quot;, 0) or 0)<br><br>    events: List[Dict[str, Any]] = []<br>    for summary in grouped.values():<br>        count = max(int(summary[&quot;execution_count&quot;]), 1)<br>        events.append(<br>            {<br>                &quot;query_id&quot;: summary[&quot;query_id&quot;],<br>                &quot;query_text&quot;: summary[&quot;query_text&quot;],<br>                &quot;database_name&quot;: summary[&quot;database_name&quot;],<br>                &quot;schema_name&quot;: summary[&quot;schema_name&quot;],<br>                &quot;execution_count&quot;: summary[&quot;execution_count&quot;],<br>                &quot;avg_cpu_time_ms&quot;: 0.0,<br>                &quot;avg_elapsed_time_ms&quot;: round(summary[&quot;avg_elapsed_time_total_ms&quot;] / count, 3),<br>                &quot;avg_disk_reads&quot;: round(summary[&quot;avg_disk_reads_total&quot;] / count, 3),<br>                &quot;avg_disk_writes&quot;: 0.0,<br>                &quot;has_full_table_scan&quot;: summary[&quot;has_full_table_scan&quot;],<br>                &quot;statement_type&quot;: summary[&quot;statement_type&quot;],<br>                &quot;last_execution_timestamp&quot;: summary[&quot;last_execution_timestamp&quot;],<br>                &quot;collection_timestamp&quot;: summary[&quot;collection_timestamp&quot;],<br>            }<br>        )<br><br>    events.sort(key=lambda item: item.get(&quot;avg_elapsed_time_ms&quot;, 0), reverse=True)<br>    return events[: max(args.query_count_threshold, args.limit)]<br><br><br>def self_test() -&gt; int:<br>    sample = {<br>        &quot;query_block&quot;: {<br>            &quot;select_id&quot;: 1,<br>            &quot;nested_loop&quot;: [<br>                {<br>                    &quot;table&quot;: {<br>                        &quot;table_name&quot;: &quot;tb_contratos&quot;,<br>                        &quot;access_type&quot;: &quot;ALL&quot;,<br>                        &quot;possible_keys&quot;: [&quot;PRIMARY&quot;],<br>                        &quot;key&quot;: &quot;PRIMARY&quot;,<br>                        &quot;key_length&quot;: &quot;4&quot;,<br>                        &quot;rows&quot;: 42,<br>                        &quot;filtered&quot;: 100,<br>                        &quot;using_index&quot;: True,<br>                        &quot;cost_info&quot;: {<br>                            &quot;query_cost&quot;: &quot;12.40&quot;,<br>                            &quot;read_cost&quot;: &quot;11.10&quot;,<br>                            &quot;eval_cost&quot;: &quot;1.30&quot;,<br>                            &quot;prefix_cost&quot;: &quot;12.40&quot;,<br>                            &quot;data_read_per_join&quot;: &quot;16K&quot;<br>                        }<br>                    }<br>                }<br>            ]<br>        }<br>    }<br>    events = extract_plan_steps(sample, 12345, 67890)<br>    print(json.dumps(events, indent=2))<br>    return 0 if events else 1<br><br><br>def parse_args(argv: List[str]) -&gt; argparse.Namespace:<br>    parser = argparse.ArgumentParser()<br>    parser.add_argument(&quot;--host&quot;, default=os.getenv(&quot;MYSQL_HOST&quot;, &quot;127.0.0.1&quot;))<br>    parser.add_argument(&quot;--port&quot;, default=os.getenv(&quot;MYSQL_PORT&quot;, &quot;3306&quot;))<br>    parser.add_argument(&quot;--user&quot;, default=os.getenv(&quot;MYSQL_USER&quot;, &quot;newrelic&quot;))<br>    parser.add_argument(&quot;--password&quot;, default=os.getenv(&quot;MYSQL_PASSWORD&quot;, &quot;&quot;))<br>    parser.add_argument(&quot;--database-filter&quot;, default=os.getenv(&quot;MYSQL_DATABASE_FILTER&quot;, &quot;&quot;))<br>    parser.add_argument(&quot;--limit&quot;, type=int, default=int(os.getenv(&quot;MYSQL_QUERY_PLAN_LIMIT&quot;, &quot;20&quot;)))<br>    parser.add_argument(&quot;--threshold-ms&quot;, type=float, default=float(os.getenv(&quot;MYSQL_QUERY_PLAN_THRESHOLD_MS&quot;, &quot;1&quot;)))<br>    parser.add_argument(<br>        &quot;--fetch-interval-seconds&quot;,<br>        type=int,<br>        default=int(os.getenv(&quot;MYSQL_SLOW_QUERY_FETCH_INTERVAL_SECONDS&quot;, &quot;300&quot;)),<br>    )<br>    parser.add_argument(<br>        &quot;--query-count-threshold&quot;,<br>        type=int,<br>        default=int(os.getenv(&quot;MYSQL_QUERY_MONITORING_COUNT_THRESHOLD&quot;, &quot;20&quot;)),<br>    )<br>    parser.add_argument(<br>        &quot;--mode&quot;,<br>        default=os.getenv(&quot;MYSQL_QUERY_PLAN_MODE&quot;, &quot;query_execution&quot;),<br>        choices=(&quot;query_execution&quot;, &quot;individual_queries&quot;, &quot;slow_queries&quot;),<br>    )<br>    parser.add_argument(<br>        &quot;--plan-command-mode&quot;,<br>        default=os.getenv(&quot;MYSQL_QUERY_PLAN_COMMAND_MODE&quot;, &quot;auto&quot;),<br>        choices=(&quot;auto&quot;, &quot;analyze&quot;, &quot;explain&quot;),<br>    )<br>    parser.add_argument(&quot;--self-test&quot;, action=&quot;store_true&quot;)<br>    return parser.parse_args(argv)<br><br><br>def main(argv: List[str]) -&gt; int:<br>    args = parse_args(argv)<br>    if args.self_test:<br>        return self_test()<br><br>    if not args.password:<br>        print(&quot;[]&quot;)<br>        return 0<br><br>    try:<br>        if args.mode == &quot;individual_queries&quot;:<br>            events = build_individual_query_events(args)<br>        elif args.mode == &quot;slow_queries&quot;:<br>            events = build_slow_query_events(args)<br>        else:<br>            events = build_events(args)<br>    except Exception:<br>        debug(&quot;build_events failed&quot;)<br>        debug(traceback.format_exc().strip())<br>        print(&quot;[]&quot;)<br>        return 0<br><br>    print(json.dumps(events))<br>    return 0<br><br><br>if __name__ == &quot;__main__&quot;:<br>    raise SystemExit(main(sys.argv[1:]))</pre><p>Até aqui, a parte mecânica está resolvida. O que resta é mostrar como isso é implantado e validado sem expor nenhum dado sensível. Primeiro, eu sugiro definir as variáveis do ambiente de forma explícita:</p><pre>export NR_MYSQL_HOST=&quot;127.0.0.1&quot;<br>export NR_MYSQL_PORT=&quot;3307&quot;<br>export NR_MYSQL_USER=&quot;seu_usuario&quot;<br>export NR_MYSQL_PASSWORD=&quot;sua_senha&quot;<br>export NR_MYSQL_DATABASE=&quot;seu_banco&quot;<br>export NR_ENVIRONMENT=&quot;production&quot;<br>export NR_ROLE=&quot;mariadb-3307&quot;<br>export NR_APPLICATION_NAME=&quot;Sua Aplicacao&quot;<br>export NR_ENTITY_GUID=&quot;SEU_ENTITY_GUID&quot;<br>export NR_ENTITY_NAME=&quot;node:seu-host:3307&quot;<br>export NR_DISPLAY_NAME=&quot;seu-host&quot;<br>export NR_QUERY_PLAN_LIMIT=&quot;20&quot;<br>export NR_QUERY_PLAN_THRESHOLD_MS=&quot;0&quot;<br>export NR_SLOW_QUERY_FETCH_INTERVAL_SECONDS=&quot;300&quot;<br>export NR_QUERY_MONITORING_COUNT_THRESHOLD=&quot;20&quot;</pre><p>Em seguida, copiar os dois arquivos para os caminhos corretos do servidor:</p><pre>sudo cp mariadb_query_execution.py /usr/local/bin/mariadb_query_execution.py<br>sudo chmod 755 /usr/local/bin/mariadb_query_execution.py<br>sudo cp mariadb-config.yml /etc/newrelic-infra/integrations.d/mariadb-config.yml</pre><p>Depois disso, eu validei o script localmente antes de reiniciar o agente. Esse passo foi importante porque eliminou a chance de eu estar depurando a interface enquanto ainda havia um problema básico no próprio gerador de eventos.</p><pre>python3 -m py_compile /usr/local/bin/mariadb_query_execution.py</pre><p>Em seguida, eu testei os três modos manualmente:</p><pre>MYSQL_QUERY_PLAN_MODE=individual_queries python3 /usr/local/bin/mariadb_query_execution.py | python3 -m json.tool<br>MYSQL_QUERY_PLAN_MODE=slow_queries python3 /usr/local/bin/mariadb_query_execution.py | python3 -m json.tool<br>MYSQL_QUERY_PLAN_MODE=query_execution python3 /usr/local/bin/mariadb_query_execution.py | python3 -m json.tool</pre><p>Só depois disso eu mandei o nri-flex carregar a configuração e reiniciei o agente:</p><pre>sudo /usr/bin/nri-flex --config_path /etc/newrelic-infra/integrations.d/mariadb-config.yml --pretty --verbose<br>sudo systemctl restart newrelic-infra</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ldbYW2d6mlhPZk1XtXlPBA.png" /><figcaption>Fluxo de configuração do “hack” para MariaDB</figcaption></figure><p>A validação final no New Relic também deixou de ser visual apenas. Eu quis confirmar no NRQL se os três tipos de evento existiam, se estavam usando a mesma entidade e se o que a interface consumia realmente estava presente no banco de eventos.</p><pre>FROM MysqlIndividualQueriesSample<br>SELECT count(*)<br>WHERE entityGuid = &#39;SEU_ENTITY_GUID&#39;<br>AND error IS NULL<br>SINCE 5 minutes ago<br><br>FROM MysqlQueryExecutionSample<br>SELECT count(*)<br>WHERE entityGuid = &#39;SEU_ENTITY_GUID&#39;<br>AND error IS NULL<br>SINCE 5 minutes ago<br><br>FROM MysqlSlowQueriesSample<br>SELECT count(*)<br>WHERE entityGuid = &#39;SEU_ENTITY_GUID&#39;<br>AND error IS NULL<br>SINCE 5 minutes ago</pre><p>No fim, a própria query que a interface usa foi o meu melhor critério de verdade. Quando ela começou a responder do jeito certo, o Query details deixou de ficar vazio. Quando o Execution plan começou a montar steps reais com event_id, thread_id e step_id, eu soube que a solução tinha deixado de ser um experimento e passado a ser funcional.</p><pre>SELECT latest(execution_time_ms), latest(query_id), latest(query_text), latest(rows_examined), latest(rows_sent)<br>FROM MysqlIndividualQueriesSample<br>WHERE entityGuid = &#39;SEU_ENTITY_GUID&#39;<br>FACET event_id, thread_id<br>SINCE 30 minutes ago UNTIL now<br><br>SELECT latest(query_cost), latest(table_name), latest(access_type), latest(rows_examined_per_scan), latest(rows_produced_per_join), latest(filtered), latest(read_cost), latest(eval_cost), latest(prefix_cost), latest(data_read_per_join), latest(possible_keys), latest(key), latest(key_length), latest(used_key_parts), latest(ref), latest(using_index)<br>FROM MysqlQueryExecutionSample<br>WHERE entityGuid = &#39;SEU_ENTITY_GUID&#39;<br>FACET event_id, thread_id, step_id<br>SINCE 30 minutes ago UNTIL now</pre><p>A grande lição dessa experiência foi simples, mas importante. No começo, eu achei que faltava uma flag. Depois, achei que faltava uma permissão. Depois, achei que faltava compatibilidade. No fim, o que faltava era coerência entre os eventos. Esse é o tipo de detalhe que não costuma aparecer no passo a passo oficial porque, no cenário suportado, ele já vem embutido no funcionamento da integração. Mas quando você precisa adaptar o fluxo para um banco fora da zona de conforto do fornecedor, entender a relação entre os dados deixa de ser um luxo. Vira a única forma de sair do escuro.</p><blockquote>Observabilidade real não é instalar agente. Observabilidade real é conseguir confiar que a história que o painel está te contando corresponde ao que o sistema está fazendo. Sem isso, você não ganhou visibilidade. Só ganhou mais uma tela bonita.</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=40efdf0f1057" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Without Automated Tests, Your System Is Not Ready to Grow]]></title>
            <link>https://medium.com/@patrickotto.dev/without-automated-tests-your-system-is-not-ready-to-grow-1e834a088f5f?source=rss-daa0cab73ef2------2</link>
            <guid isPermaLink="false">https://medium.com/p/1e834a088f5f</guid>
            <dc:creator><![CDATA[Patrick Otto]]></dc:creator>
            <pubDate>Sat, 02 May 2026 22:37:51 GMT</pubDate>
            <atom:updated>2026-05-02T22:37:51.334Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Lb_b4KsSkrtd__wRg20IEg.png" /></figure><h3>When a company relies only on manual validation, every change becomes a risk, every deployment becomes a moment of tension, and growth starts compromising the evolution of the system itself.</h3><p>There is a common belief in software development that systems start facing serious problems when they grow. At first, the explanation seems logical. More users generate more load. More features create more complexity. More integrations introduce more points of failure. At some point, if nothing is adjusted, something will break.</p><p>That interpretation is not wrong, but it is incomplete.</p><p>In practice, many systems start failing before they ever reach a high volume of users. They do not break first because of traffic, lack of servers, or infrastructure limitations. They begin to slow down for a different reason, one that is much quieter and much more dangerous: the lack of confidence to evolve.</p><p>This is something that does not always appear in technical reports, but it becomes clear in the behavior of the team. Developers start becoming afraid of touching certain parts of the codebase. Simple changes begin to require long manual validations. Deployments stop being a natural step in the process and start being treated as a risky event. Little by little, the company loses speed, not because the team is no longer capable, but because the system no longer feels safe.</p><p>This is one of the first signs that an application is not ready to scale.</p><p>When a system is small, almost everything can be handled through proximity. The developer knows the code, understands the business rules, knows which screens need to be tested, and often manages to validate everything manually before releasing a new version. There is a direct relationship between the person building, the person testing, and the person watching the result.</p><p>That model works for a while.</p><p>The problem is that systems do not remain small when the business starts to grow. New rules are added, exceptions appear, external integrations become part of the flow, different people start contributing to the codebase, and knowledge is no longer concentrated in one person’s head. What used to be simple to validate manually starts becoming unpredictable.</p><p>A change in a form can affect a calculation rule. A backend adjustment can break a screen that apparently had nothing to do with that part of the system. An API change can impact a mobile app, an admin dashboard, or an external integration. And when there is no automated validation layer, all of this depends on someone remembering to test it.</p><p>That is where risk starts to accumulate.</p><p>Automated tests exist precisely to reduce this dependence on human memory. They do not eliminate every problem, they do not replace critical thinking, and they do not magically turn a bad system into a good one. But they create a fundamental layer of safety so that software can evolve without every change feeling like a leap in the dark.</p><p>There is a very common phrase in development teams: “But I only changed one small thing.” Almost every production incident has started with some version of that sentence. The problem is that, in real systems, very few things are truly isolated. A seemingly small rule may be connected to several behaviors across the application. Without tests, this connection is usually discovered only after something breaks.</p><p>The role of automated tests is to anticipate part of that discovery.</p><p>On the frontend, for example, tools such as Jest, Vitest, and Testing Library help validate the behavior of components. In React, Vue, or Angular applications, it is possible to verify whether a screen still renders correctly, whether a button still triggers the expected action, whether an error message appears when the user fills in an invalid field, or whether a component reacts correctly to a state change.</p><p>This type of test does not exist only to verify whether the code “works.” It exists to protect the user experience.</p><p>Imagine a registration screen where the user needs to fill in name, email, and password. Visually, everything may look correct. But if a change in the component breaks email validation or prevents the form from being submitted, the problem can easily go unnoticed during a rushed manual validation. An automated test reduces that risk because it turns the expected behavior into something that can be verified.</p><p>A simple example in React could look like this:</p><pre>import { render, screen, fireEvent } from &#39;@testing-library/react&#39;;<br>import RegisterForm from &#39;./RegisterForm&#39;;<br><br>test(&#39;exibe mensagem de erro quando o e-mail é inválido&#39;, () =&gt; {<br>  render(&lt;RegisterForm /&gt;);<br>  fireEvent.change(screen.getByLabelText(&#39;E-mail&#39;), {<br>    target: { value: &#39;email-invalido&#39; }<br>  });<br>  fireEvent.click(screen.getByText(&#39;Cadastrar&#39;));<br>  expect(screen.getByText(&#39;Informe um e-mail válido&#39;)).toBeInTheDocument();<br>});</pre><p>This test is not concerned with the internal implementation of the component. It is concerned with the behavior perceived by the user. The user entered an invalid email, clicked register, and expects to receive an appropriate message. If someone changes this form tomorrow and breaks that validation, the test exposes the problem before it reaches production.</p><p>That is the difference between testing code and testing behavior.</p><p>Tools such as Cypress and Playwright expand this view because they allow complete navigation flows to be tested. Instead of validating only an isolated component, you can simulate the real user journey inside the system. Login, order creation, payment, report generation, customer registration, form submission, or any other critical flow.</p><p>This kind of test is especially important in digital products that depend on a continuous experience. A system may have a backend working correctly, but if the user cannot complete the action in the interface, the problem is real. For the end customer, it does not matter whether the API responded correctly. What matters is whether they were able to use the product.</p><p>On the backend, the logic changes slightly. The focus moves away from visual interaction and toward the consistency of business rules. Tools such as xUnit and NUnit in the .NET ecosystem, PyTest in Python, Mocha or Jest in Node.js, PHPUnit in PHP, and JUnit in Java are used to validate internal application behavior.</p><p>Here, tests protect the heart of the system.</p><p>If a commission rule, tax calculation, credit approval, order creation, or permission validation is critical to the business, it should not depend only on manual testing. There should be an automated way to ensure that this rule continues to work after every change.</p><p>A simple backend example could be an order rule:</p><pre>describe(&#39;OrderService&#39;, () =&gt; {<br>  it(&#39;deve criar um pedido com status pendente quando os dados forem válidos&#39;, async () =&gt; {<br>    const payload = {<br>      customerId: 10,<br>      items: [<br>        { productId: 1, quantity: 2, price: 100 }<br>      ]<br>    };<br>    const order = await OrderService.create(payload);<br>      expect(order.status).toBe(&#39;pending&#39;);<br>      expect(order.total).toBe(200);<br>    });<br>});</pre><p>This test validates an essential rule: when an order is created with valid data, the system must calculate the total correctly and initialize the order with the expected status. It looks simple, but this is exactly the kind of behavior that sustains trust in a system.</p><p>When rules like this are not tested, the company starts depending on informal validation. Someone needs to remember to test the order, remember to check the total, remember to validate the status, and remember to verify indirect impacts. This model does not scale, because the larger the system becomes, the more things someone needs to remember.</p><p>And human memory is not architecture.</p><p>There is also an important difference between types of tests. Unit tests validate small units of behavior, such as functions, methods, or classes. Integration tests validate whether different parts of the system work correctly together, such as an API communicating with a database or one service calling another. End-to-end tests validate complete flows, simulating the real behavior of a user or a process from beginning to end.</p><p>Each layer has a purpose.</p><p>Unit tests are fast and help locate problems with precision. Integration tests increase confidence in the communication between components. End-to-end tests provide a view closer to the user’s reality. The common mistake is trying to solve everything with only one type of test.</p><p>A mature system usually combines these layers. Not because it looks beautiful from a technical standpoint, but because each one protects a different part of the risk.</p><p>When a company does not have this structure, growth starts producing side effects. The team spends more time validating than building. Small changes start generating fear. Old bugs reappear. Features that were working stop working after apparently unrelated changes. The customer begins to notice instability. The business starts losing confidence in the technical team.</p><p>And when the business loses confidence in the technical team, the entire evolution process becomes compromised.</p><p>This is where many companies confuse speed with haste. To deliver faster, they cut tests. To reduce deadlines, they remove validations. To meet a date, they push risk into production. The delivery may happen, but the bill comes later.</p><p>It comes as rework, incidents, emergency meetings, unhappy customers, and loss of predictability.</p><p>Automated tests do not prevent all errors from happening. That expectation is unrealistic. Their role is to reduce the chances of predictable errors reaching places where they should never arrive. They act as a safety net for the system, allowing the team to move with more confidence.</p><p>This point matters because there is still cultural resistance in many companies. Testing is often seen as a cost, as something that slows development down, as a step that can be left for later. The problem is that “later” almost never comes. And when it does, the system is already large, coupled, full of invisible rules, and too expensive to test easily.</p><p>The later a company starts building a testing culture, the harder it becomes.</p><p>Not because the tools are complex, but because the system was built without that concern. Code without clear separation of responsibilities is hard to test. Business rules mixed with interface logic are hard to validate. Overly coupled services require too much effort to isolate. Poorly organized external dependencies make tests unstable and unreliable.</p><p>That is why automated tests should not be seen only as a quality practice. They reflect the architecture of the system itself.</p><p>A testable system is usually a better organized system. To test well, you need to separate responsibilities, reduce coupling, make business rules clearer, and create consistent validation points. In other words, the search for better tests often leads to better code.</p><p>This is a benefit many people ignore.</p><p>When a team starts writing tests, it begins to notice design problems. Functions that are too large, classes that do too much, hidden dependencies, duplicated rules, implicit behaviors. Testing exposes the mess. And at first, this can be uncomfortable.</p><p>But it is a necessary discomfort.</p><p>In systems that need to scale, the discomfort of organizing now is smaller than the cost of fixing later. A company can survive for some time without automated tests, especially if the system is still small. But as the operation grows, the absence of this foundation becomes a real limitation.</p><p>There comes a moment when the company wants to evolve, but the system cannot keep up. It wants to launch features, but every change generates instability. It wants to hire more developers, but new people take too long to understand the impact of what they are changing. It wants to move faster, but the technical foundation itself pulls the brake.</p><p>At that point, the problem is no longer just technical. It starts affecting the company’s ability to execute.</p><p>A system without tests may look cheaper in the beginning, but it usually becomes more expensive in the long run. The cost does not appear only in the code. It appears in operations, support, customer service, customer trust, and the company’s ability to respond to the market.</p><p>When a competitor can release features safely while your company needs weeks of manual validation to change a rule, the difference is not only the size of the team. It is the maturity of the process.</p><p>Automated tests are part of that maturity.</p><p>They are not the only pillar, but they are one of the most important. Without them, CI/CD loses strength because the pipeline has no meaningful validations to execute. Without them, continuous deployment becomes just risk automation. Without them, refactoring becomes fear. Without them, maintenance becomes trial and error.</p><p>This connection is fundamental. Tests do not live in isolation. They support other modern engineering practices. A continuous delivery pipeline only makes sense when there is a reliable validation base. An evolvable architecture only holds when there is confidence to change. A culture of continuous improvement only works when the team can measure the impact of changes.</p><p>Without tests, all of this becomes fragile.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/720/0*bJNHXzAgDjXr8o4M.jpg" /><figcaption>Apollo 13 disaster — Using the mon gravity such as slingshot</figcaption></figure><p>There is also a human aspect to this topic. Developers work better when they feel safe changing the system. When every change generates fear, the team becomes conservative. People avoid improvements, postpone refactorings, and learn to live with known problems because changing them feels too dangerous.</p><p>This slowly kills evolution.</p><p>A good automated test suite does not only protect the system. It protects the team’s ability to improve the system. It allows developers to change, refactor, reorganize, and evolve with more confidence. This changes the rhythm of work.</p><p>The difference is noticeable.</p><p>In an environment without tests, deployments are usually followed by tension. In an environment with good test coverage, deployment still requires responsibility, but it no longer depends exclusively on hope. The company starts having a more reliable process.</p><p>And a reliable process is one of the foundations of scale.</p><p>When we talk about scale, many people immediately think about infrastructure. Servers, Kubernetes, distributed databases, caching, queues, and load balancing. All of this can be important, but none of it solves a basic problem: if the company cannot safely change the system, it cannot scale sustainably.</p><p>Scale is not only about supporting more users. Scale is about supporting more users, more rules, more changes, and more people working on the same product without turning every delivery into a risk.</p><p>Automated tests help exactly at this point. They create a language of trust between code, team, and business. Expected behavior stops living only in people’s heads and becomes documented in executable form.</p><p>This is one of the strongest forms of documentation because it does not merely describe what the system should do. It verifies that the system still does it.</p><p>And that changes everything.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/980/1*jtKlQfrt5VK89Qcf0aDi2A.png" /><figcaption>Apollo 14 — Mission Control</figcaption></figure><p>Because no one tests everything manually forever.</p><p>At some point, the volume becomes too large. The complexity becomes too high. The risk becomes too expensive.</p><p>And when that moment comes, the company discovers that the problem was not only a lack of tests. It was a lack of structure to grow.</p><p>Automated tests do not guarantee the success of a system, but their absence greatly increases the chances of operational failure. They do not replace good developers, but they amplify good teams. They do not eliminate bugs, but they reduce the surface of risk. They do not solve every architectural problem, but they expose many of them before they become too invisible.</p><p>That is why, when someone says automated tests “slow the project down,” perhaps the right question is different.</p><p>Slow it down compared to what?</p><p>Compared to a rushed delivery that will generate rework later? Compared to a deployment made with fear? Compared to a feature released without confidence? Compared to a system that grows without a foundation and later requires months of correction?</p><p>There is a huge difference between speed and haste.</p><p>Speed is delivering with consistency. Haste is pushing risk forward.</p><p>Mature companies understand this difference.</p><p>And companies that intend to scale need to understand it before the system itself teaches the lesson in the worst possible way.</p><p>Without automated tests, your company may still be able to deliver software for some time. It may even grow in number of users, features, and customers. But as complexity increases, the absence of this foundation starts charging its price.</p><p>First, in the team’s speed. Then, in product stability. Eventually, in the trust of the business.</p><p>And when trust disappears, the system stops being a growth engine and becomes a permanent source of risk.</p><p>Automated tests are not a luxury. They are not a detail. They are not a concern only for large companies.</p><p>They are one of the first signs that a company takes its product seriously.</p><p>Because software that grows without tests is not scaling.</p><p>It is only accumulating risk in silence.</p><p>The question that remains is simple: is your company using automated tests as part of its growth strategy, or is it still treating manual validation as a process? 🚀</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1e834a088f5f" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>