Tornar o código localizável usando IDs de mensagem globalmente exclusivos

39

Um padrão comum para localizar um bug segue este script:

  1. Observar estranheza, por exemplo, sem saída ou um programa de suspensão.
  2. Localize a mensagem relevante no log ou na saída do programa, por exemplo, "Não foi possível encontrar o Foo". (O seguinte é relevante apenas se esse for o caminho usado para localizar o bug. Se um rastreamento de pilha ou outras informações de depuração estiverem prontamente disponíveis, isso é outra história.)
  3. Localize o código no qual a mensagem é impressa.
  4. Depure o código entre o primeiro local onde Foo entra (ou deve entrar) na imagem e onde a mensagem é impressa.

Esse terceiro passo é o local onde o processo de depuração geralmente fica paralisado porque há muitos locais no código onde "Não foi possível encontrar o Foo" (ou uma string com modelo Could not find {name} ) é impresso. De fato, várias vezes um erro de ortografia me ajudou a encontrar o local real muito mais rápido do que eu faria - tornou a mensagem única em todo o sistema e em todo o mundo, resultando em um mecanismo de pesquisa relevante imediatamente.

A conclusão óbvia disso é que devemos usar IDs de mensagem globalmente exclusivos no código, codificá-lo como parte da cadeia de mensagens e, possivelmente, verificar se há apenas uma ocorrência de cada ID na base de código. Em termos de sustentabilidade, o que essa comunidade acha que são os prós e contras mais importantes dessa abordagem, e como você implementaria isso ou garantiria que a implementação nunca se tornasse necessária (supondo que o software sempre tenha bugs)?

    
por l0b0 30.01.2018 / 03:54
fonte

6 respostas

12

No geral, esta é uma estratégia válida e valiosa. Aqui estão alguns pensamentos.

Essa estratégia também é conhecida como "telemetria" no sentido de que quando todas essas informações são combinadas, elas ajudam a "triangular" o rastreamento de execução e permitem que um solucionador de problemas faça sentido do que o usuário / aplicativo está tentando realizar. o que realmente aconteceu.

Alguns dados essenciais que devem ser coletados (todos nós sabemos) são:

  • Localização do código, ou seja, pilha de chamadas e a linha aproximada de código
    • "Aproximada linha de código" não é necessária se as funções forem decompostas razoavelmente em unidades adequadamente pequenas.
  • Quaisquer dados que sejam pertinentes ao sucesso / fracasso da função
  • Um "comando" de alto nível que pode descobrir o que o usuário humano / agente externo / usuário da API está tentando realizar.
    • A ideia é que um software aceite e processe comandos vindos de algum lugar.
    • Durante esse processo, dezenas a centenas de milhares de chamadas de função podem ter ocorrido.
    • Gostaríamos que qualquer telemetria gerada em todo este processo fosse rastreável até o comando de nível mais alto que aciona esse processo.
    • Para sistemas baseados na Web, a solicitação HTTP original e seus dados seriam um exemplo de tais "informações de solicitação de alto nível"
    • Para sistemas de GUI, o usuário que clica em algo caberia nessa descrição.

Muitas vezes, as abordagens de registro tradicionais ficam aquém, devido à falha em rastrear uma mensagem de log de baixo nível até o comando de nível mais alto que a aciona. Um rastreamento de pilha captura apenas os nomes das funções superiores que ajudaram a manipular o comando de nível mais alto, não os detalhes (dados) às vezes necessários para caracterizar esse comando.

Normalmente, o software não foi escrito para implementar este tipo de requisitos de rastreabilidade. Isso torna mais difícil correlacionar a mensagem de baixo nível ao comando de alto nível. O problema é particularmente pior em sistemas multi-threaded livremente, onde muitas solicitações e respostas podem se sobrepor, e o processamento pode ser transferido para um thread diferente do thread de recebimento de solicitações original.

Assim, para obter o máximo valor da telemetria, serão necessárias alterações na arquitetura geral do software. A maioria das interfaces e chamadas de funções precisará ser modificada para aceitar e propagar um argumento "tracer".

Até mesmo as funções utilitárias precisarão adicionar um argumento "tracer", de modo que, se falhar, a mensagem de log se permitirá correlacionar com um certo comando de alto nível.

Outra falha que dificultará o rastreamento de telemetria é a falta de referências a objetos (ponteiros nulos ou referências). Quando algum dado crucial está faltando, pode ser impossível relatar qualquer coisa útil para a falha.

Em termos de gravação das mensagens de log:

  • Alguns projetos de software podem exigir localização (tradução em um idioma estrangeiro) mesmo para mensagens de log destinadas apenas a administradores.
  • Alguns projetos de software podem precisar de separação clara entre dados confidenciais e dados não confidenciais, mesmo para fins de registro, e que os administradores não teriam a chance de ver acidentalmente determinados dados confidenciais.
  • Não tente ofuscar a mensagem de erro. Isso prejudicaria a confiança dos clientes. Os administradores dos clientes esperam ler esses registros e entendê-los. Não os faça sentir que há algum segredo de propriedade que deve ser escondido dos administradores dos clientes.
  • Esperamos que os clientes tragam um log de telemetria e grelhem sua equipe de suporte técnico. Eles esperam saber. Treine sua equipe de suporte técnico para explicar o log de telemetria corretamente.
por 30.01.2018 / 08:08
fonte
59

Imagine que você tenha uma função de utilidade trivial usada em centenas de lugares em seu código:

decimal Inverse(decimal input)
{
    return 1 / input;
}

Se fôssemos fazer o que você sugere, poderíamos escrever

decimal Inverse(decimal input)
{
    try 
    {
        return 1 / input;
    }
    catch(Exception ex)
    {
        log.Write("Error 27349262 occurred.");
    }
}

Um erro que pode ocorrer é se a entrada for zero; isso resultaria em uma divisão por exceção zero.

Então, digamos que você veja 27349262 em sua saída ou em seus registros. Onde você procura para encontrar o código que passou o valor zero? Lembre-se, a função - com seu ID exclusivo - é usada em centenas de lugares. Então, enquanto você pode saber que a divisão por zero ocorreu, você não tem idéia de quem é 0 .

Parece-me que você vai se incomodar em registrar os IDs das mensagens, assim como você pode registrar o rastreamento da pilha.

Se o detalhamento do rastreio de pilha é o que o incomoda, você não precisa despejá-lo como uma string como o tempo de execução fornece a você. Você pode personalizá-lo. Por exemplo, se você quisesse que um rastreio de pilha abreviado fosse apenas para n , você poderia escrever algo assim (se você usa c #):

static class ExtensionMethods
{
    public static string LimitedStackTrace(this Exception input, int layers)
    {
        return string.Join
        (
            ">",
            new StackTrace(input)
                .GetFrames()
                .Take(layers)
                .Select
                (
                    f => f.GetMethod()
                )
                .Select
                (
                    m => string.Format
                    (
                        "{0}.{1}", 
                        m.DeclaringType, 
                        m.Name
                    )
                )
                .Reverse()
        );
    }
}

E use assim:

public class Haystack
{
    public static void Needle()
    {
        throw new Exception("ZOMG WHERE DID I GO WRONG???!");
    }

    private static void Test()
    {
        Needle();
    }

    public static void Main()
    {
        try
        {
            Test();
        }
        catch(System.Exception e)
        {
            //Get 3 levels of stack trace
            Console.WriteLine
            (
                "Error '{0}' at {1}", 
                e.Message, 
                e.LimitedStackTrace(3)
            );  
        }
    }
}

Saída:

Error 'ZOMG WHERE DID I GO WRONG???!' at Haystack.Main>Haystack.Test>Haystack.Needle

Talvez seja mais fácil do que manter IDs de mensagens e mais flexível.

Roube meu código do DotNetFiddle

    
por 30.01.2018 / 04:14
fonte
6

O SAP NetWeaver está fazendo isso por décadas.

Ele provou ser uma ferramenta valiosa ao solucionar erros no gigante do código massivo que é o sistema SAP ERP típico.

As mensagens de erro são gerenciadas em um repositório central, onde cada mensagem é identificada por sua classe de mensagem e número de mensagem.

Quando você deseja enviar uma mensagem de erro, você indica apenas as variáveis específicas de classe, número, gravidade e mensagem. A representação de texto da mensagem é criada no tempo de execução. Você geralmente vê a classe e o número da mensagem em qualquer contexto em que as mensagens aparecem. Isto tem vários efeitos puros:

  • Você pode encontrar automaticamente qualquer linha de código na base de código do ABAP que crie uma mensagem de erro específica.

  • Você pode definir pontos de interrupção do depurador dinâmico que são acionados quando uma mensagem de erro específica é gerada.

  • Você pode pesquisar erros nos artigos da base de conhecimento SAP e obter resultados de pesquisa mais relevantes do que procurar "Não foi possível encontrar o Foo".

  • As representações de texto das mensagens são traduzíveis. Assim, incentivando o uso de mensagens em vez de strings, você também obtém recursos do i18n.

Um exemplo de um pop-up de erro com o número da mensagem:

Pesquisandooerronorepositóriodeerros:

Encontrenabasedecódigo:

Noentanto,existemdesvantagens.Comovocêpodever,essaslinhasdecódigonãosãomaisautodocumentadas.QuandovocêlêocódigofonteevêumadeclaraçãoMESSAGEcomoasdaimagemacima,vocêsópodeinferirdocontextooquerealmentesignifica.Alémdisso,àsvezes,aspessoasimplementammanipuladoresdeerrospersonalizadosquerecebemaclasseeonúmerodamensagemnotempodeexecução.Nessecaso,oerronãopodeserencontradoautomaticamenteounãopodeserencontradonolocalondeoerrorealmenteocorreu.Asoluçãoalternativaparaoprimeiroproblemaécriarohábitodesempreadicionarumcomentárionocódigo-fonteinformandoaoleitoroqueamensagemsignifica.Osegundoéresolvidoadicionandoalgumcódigomortoparagarantirqueapesquisaautomáticademensagensfuncione.Exemplo:

" Do not use special characters
my_custom_error_handler->post_error( class = 'EU' number = '271').
IF 1 = 2.
   MESSAGE e271(eu).
ENDIF.    

Mas há algumas situações em que isso não é possível. Existem, por exemplo, algumas ferramentas de modelagem de processos de negócios baseados em UI, nas quais é possível configurar mensagens de erro para serem exibidas quando as regras de negócios são violadas. A implementação dessas ferramentas é completamente orientada a dados, portanto, esses erros não serão exibidos na lista de utilizações. Isso significa que confiar demais na lista de itens usados ao tentar encontrar a causa de um erro pode ser uma pista falsa.

    
por 30.01.2018 / 14:55
fonte
5

O problema com essa abordagem é que isso leva a um registro cada vez mais detalhado. 99,9999% dos quais você nunca olhará.

Em vez disso, recomendo capturar o estado no início de seu processo e o sucesso / fracasso do processo.

Isso permite que você reproduza o bug localmente, percorrendo o código e limitando seu registro a dois locais por processo. por exemplo.

OrderPlaced {id:xyz; ...order data..}
OrderPlaced {id:xyz; ...Fail, ErrorMessage..}

Agora eu posso usar exatamente o mesmo estado em minha máquina dev para reproduzir o erro, percorrendo o código no meu depurador e escrevendo um novo teste de unidade para confirmar a correção.

Além disso, posso, se necessário, evitar mais registros apenas registrando falhas ou mantendo o estado em outro lugar (fila de mensagens do banco de dados?)

Obviamente, precisamos ter muito cuidado ao registrar dados confidenciais. Portanto, isso funciona particularmente bem se sua solução estiver usando filas de mensagens ou o padrão de armazenamento de eventos. Como o log precisa apenas dizer "Message xyz Failed"

    
por 30.01.2018 / 14:40
fonte
1

Eu sugeriria que a extração de madeira não é o caminho a seguir, mas sim que essa circunstância é considerada excepcional (bloqueia seu programa) e uma exceção deve ser lançada. Digamos que seu código seja:

public Foo GetFoo() {

     //Expecting that this should never by null.
     var aFoo = ....;

     if (aFoo == null) Log("Could not find Foo.");

     return aFoo;
}

Parece que o seu código de chamada não está configurado para lidar com o fato de que o Foo não existe e você poderia potencialmente ser:

public Foo GetFooById(int id) {
     var aFoo = ....;

     if (aFoo == null) throw new ApplicationException("Could not find Foo for ID: " + id);

     return aFoo;
}

E isso retornará um rastreamento de pilha junto com a exceção que pode ser usada para ajudar na depuração.

Como alternativa, se esperamos que Foo possa ser nulo quando recuperado e não há problema, precisamos corrigir os sites de chamada:

void DoSomeFoo(Foo aFoo) {

    //Guard checks on your input - complete with stack trace!
    if (aFoo == null) throw new ArgumentNullException(nameof(aFoo));

    ... operations on Foo...
}

O fato de que seu software trava ou age 'estranhamente' sob circunstâncias inesperadas parece errado para mim - se você precisa de um Foo e não consegue lidar com ele não estando lá, então parece melhor sair do que tentar prosseguir ao longo de um caminho que pode corromper seu sistema.

    
por 30.01.2018 / 15:06
fonte
0

Bibliotecas de registro adequadas fornecem mecanismos de extensão, portanto, se você quiser saber o método de origem de uma mensagem de registro, ele poderá fazer isso imediatamente. Ele tem um impacto na execução, já que o processo requer a geração de um rastreamento de pilha e a sua passagem até que você esteja fora da biblioteca de registro.

Dito isso, isso realmente depende do que você deseja que sua identidade faça por você:

  • Correlacionar mensagens de erro fornecidas ao usuário para seus registros?
  • Fornece anotação sobre qual código estava sendo executado quando a mensagem foi gerada?
  • Acompanhe o nome da máquina e a instância do serviço?
  • Acompanhe o ID do tópico?

Todas essas coisas podem ser feitas imediatamente com o software de registro adequado (ou seja, não Console.WriteLine() ou Debug.WriteLine() ).

Pessoalmente, o mais importante é a capacidade de reconstruir caminhos de execução. Isso é o que ferramentas como Zipkin são projetadas para realizar. Um ID para rastrear o comportamento de uma ação do usuário em todo o sistema. Ao colocar seus registros em um mecanismo de pesquisa central, você pode não apenas encontrar as ações de execução mais longas, mas também acessar os registros que se aplicam a essa ação única (como o ELK stack ).

IDs opacos que mudam com cada mensagem não são muito úteis. Um ID consistente usado para rastrear o comportamento por meio de um conjunto inteiro de microsserviços ... imensamente útil.

    
por 30.01.2018 / 19:38
fonte