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


39

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

  1. Observe a estranheza, por exemplo, nenhuma saída ou um programa suspenso.
  2. Localize a mensagem relevante na saída do log ou do programa, por exemplo, "Não foi possível encontrar o Foo". (O seguinte é relevante apenas se este 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 onde a mensagem é impressa.
  4. Depure o código entre o primeiro lugar em que o Foo entra (ou deve entrar) na imagem e onde a mensagem é impressa.

Essa terceira etapa é onde o processo de depuração geralmente é interrompido porque há muitos locais no código em que "Não foi possível encontrar o Foo" (ou uma seqüência de modelo Could not find {name}) é impressa. 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, muitas vezes, em todo o mundo, resultando em um mecanismo de pesquisa relevante atingido imediatamente.

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


54
Faça uso de seus rastreamentos de pilha. O rastreamento da pilha não apenas informará exatamente onde o erro ocorreu, mas também todas as funções que chamaram todas as funções que o chamaram. Registre o rastreamento inteiro quando ocorrer uma exceção, se necessário. Se você está trabalhando em um idioma que não tem exceções, como C, essa é uma história diferente.
Robert Harvey

6
@ l0b0 um pequeno conselho sobre redação. "o que essa comunidade acha ... prós e contras" são frases que podem ser vistas como muito amplas. Este é um site que permite perguntas "boas subjetivas" e, em troca de permitir esse tipo de perguntas, você, como OP, deveria fazer o trabalho de "pastorear" os comentários e as respostas para um consenso significativo.
Rwong

@rwong Obrigado! Eu sinto que a pergunta já recebeu uma resposta muito boa e pontual, embora isso possa ter sido melhor perguntado em um fórum. Recuei minha resposta ao comentário de RobertHarvey depois de ler a resposta esclarecedora de JohnWu, para o que você está se referindo. Caso contrário, você tem alguma dica específica de pastoreio?
L0b0

1
Minhas mensagens se parecem com "Não foi possível encontrar o Foo durante a chamada para bar ()". Problema resolvido. Dar de ombros. A desvantagem é que é um pouco gotejante para ser visto pelos clientes, mas tendemos a ocultar os detalhes das mensagens de erro de qualquer maneira, tornando-os disponíveis apenas para administradores de sistemas que não podiam dar aos macacos que pudessem ver alguns nomes de funções. Caso contrário, sim, um código / ID único e agradável fará o truque.
Lightness Races com Monica

1
Isso é MUITO útil quando um cliente liga para você e o computador não está funcionando em inglês! Muito menos de um problema nos dias de hoje como temos agora de e-mail e arquivos de log .....
Ian

Respostas:


12

No geral, essa é 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 da execução e permitem que um solucionador de problemas compreenda o que o usuário / aplicativo está tentando realizar e o que realmente aconteceu. .

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

  • Localização do código, ou seja, pilha de chamadas e a linha de código aproximada
    • A "linha aproximada de código" não é necessária se as funções forem razoavelmente decompostas em unidades adequadamente pequenas.
  • Quaisquer dados pertinentes ao sucesso / falha da função
  • Um "comando" de alto nível que pode determinar o que o usuário humano / agente externo / usuário da API está tentando realizar.
    • A idéia é que um software aceite e processe comandos vindos de algum lugar.
    • Durante esse processo, dezenas a centenas a milhares de chamadas de função podem ter ocorrido.
    • Gostaríamos que qualquer telemetria gerada ao longo desse 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 dessas "informações de solicitação de alto nível"
    • Para sistemas GUI, o usuário clicando em algo se encaixaria nessa descrição.

Muitas vezes, as abordagens tradicionais de registro 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) que às vezes são necessários para caracterizar esse comando.

Normalmente, o software não foi escrito para implementar esse tipo de requisitos de rastreabilidade. Isso dificulta a correlação da mensagem de baixo nível com o comando de alto nível. O problema é particularmente pior em sistemas livremente multiencadeados, onde muitas solicitações e respostas podem se sobrepor e o processamento pode ser transferido para um encadeamento diferente do que o encadeamento original de recebimento de solicitações.

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ção precisarão ser modificadas para aceitar e propagar um argumento "rastreador".

Até as funções utilitárias precisarão adicionar um argumento "rastreador", para que, se falhar, a mensagem de log permita correlacionar-se com um determinado 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 faltam alguns dados cruciais, pode ser impossível relatar algo útil para a falha.

Em termos de escrita das mensagens de log:

  • Alguns projetos de software podem exigir localização (tradução para um idioma estrangeiro), mesmo para mensagens de log destinadas apenas a administradores.
  • Alguns projetos de software podem precisar de uma separação clara entre dados confidenciais e dados não confidenciais, mesmo para fins de registro, e que os administradores não teriam chance de ver acidentalmente determinados dados confidenciais.
  • Não tente ofuscar a mensagem de erro. Isso minaria a confiança dos clientes. Os administradores dos clientes esperam ler esses logs e compreendê-los. Não os faça sentir que há algum segredo proprietário que deve ser oculto aos administradores dos clientes.
  • Espere que os clientes tragam um registro de telemetria e dividam sua equipe de suporte técnico. Eles esperam saber. Treine sua equipe de suporte técnico para explicar o log de telemetria corretamente.

1
De fato, a AOP divulgou, principalmente, sua capacidade inerente de resolver esse problema - adicionando o Tracer a todas as chamadas relevantes - com invasão mínima à base de código.
bispo

Eu acrescentaria também à lista de "gravação de mensagens de log" que é importante caracterizar a falha em termos de "por que" e "como corrigir" em vez de apenas "o que" aconteceu.
bispo

58

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

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

Se fizéssemos 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 poderia ocorrer é se a entrada fosse zero; isso resultaria em uma exceção de divisão por zero.

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

Parece-me que se você vai se incomodar em registrar os IDs de mensagem, também pode registrar o rastreamento de pilha.

Se a verbosidade do rastreamento da pilha é o que o incomoda, você não precisa despejá-lo como uma string da maneira que o tempo de execução o fornece. Você pode personalizá-lo. Por exemplo, se você quiser que um rastreio de pilha abreviado vá apenas para nníveis, escreva algo assim (se usar 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-o 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 mais fácil do que manter identificações de mensagens e mais flexível.

Roubar meu código do DotNetFiddle


32
Hmm, acho que não entendi meu argumento com clareza suficiente. Eu sei que eles são únicos Robert-- por localização do código . Eles não são exclusivos por caminho de código . Saber o local geralmente é inútil, por exemplo, se o verdadeiro problema é que uma entrada não foi configurada corretamente. Eu editei meu idioma um pouco para enfatizar.
John Wu

1
Bons pontos, vocês dois. Há um problema diferente com os rastreamentos de pilha, que podem ou não ser um rompedor de acordo com a situação: o tamanho deles pode resultar na troca de mensagens, especialmente se você deseja incluir o rastreamento de pilha inteiro em vez de uma versão reduzida, como alguns idiomas faça por padrão. Talvez uma alternativa seria escrever um log de rastreamento de pilha separadamente e incluir índices numerados nesse log na saída do aplicativo.
L0b0

12
Se você está recebendo tantas delas que está preocupado em inundar sua E / S, há algo seriamente errado. Ou você está apenas sendo mesquinho? O impacto real no desempenho é provavelmente o desenrolar da pilha.
John Wu

9
Editado com uma solução para rastreamentos de pilha encurtamento, caso em que você está escrevendo toras para um disquete de 3,5;)
John Wu

7
@JohnWu E também não se esqueça de "IOException 'File not Found' at at" [...] "que informa cerca de cinquenta camadas da pilha de chamadas, mas não informa qual arquivo sangrento exato não foi encontrado.
Joker_vD 30/01

6

O SAP NetWeaver faz isso há décadas.

Ele provou ser uma ferramenta valiosa na solução de erros no gigantesco código gigantesco que é o típico sistema SAP ERP.

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

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

  • Você pode encontrar automaticamente qualquer linha de código na base de código 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 procurar erros nos artigos da base de conhecimento SAP e obter resultados de pesquisa mais relevantes do que se procurar "Não foi possível encontrar o Foo".

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

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

error1

Procurando esse erro no repositório de erros:

error2

Encontre-o na base de código:

error3

No entanto, existem desvantagens. Como você pode ver, essas linhas de código não são mais auto-documentadas. Ao ler o código-fonte e ver uma MESSAGEdeclaração como a da captura de tela acima, você só pode inferir do contexto o que realmente significa. Além disso, às vezes as pessoas implementam manipuladores de erro personalizados que recebem a classe e o número da mensagem em tempo de execução. Nesse caso, o erro não pode ser encontrado automaticamente ou não pode ser encontrado no local em que o erro realmente ocorreu. A solução alternativa para o primeiro problema é criar o hábito de sempre adicionar um comentário no código fonte, informando ao leitor o significado da mensagem. O segundo é resolvido adicionando algum código morto para garantir que a pesquisa automática de mensagens funcione. Exemplo:

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

Mas existem algumas situações em que isso não é possível. Existem, por exemplo, algumas ferramentas de modelagem de processos de negócios baseadas na interface do usuário nas quais você pode configurar as mensagens de erro para aparecer quando as regras de negócios forem violadas. A implementação dessas ferramentas é totalmente orientada a dados; portanto, esses erros não aparecerão na lista de onde são usados. Isso significa que confiar demais na lista de utilizações ao tentar encontrar a causa de um erro pode ser um problema.


Os catálogos de mensagens também fazem parte do GNU / Linux - e UNIX geralmente como padrão POSIX - há algum tempo.
bispo

@ bispo Normalmente, não estou programando especificamente para sistemas POSIX, por isso não estou familiarizado com isso. Talvez você possa postar outra resposta que explique os catálogos de mensagens POSIX e o que o OP pode aprender com sua implementação.
Philipp

3
Eu fazia parte de um projeto que fez isso de volta nos deveres. Um problema que encontramos foi que, junto com todo o resto, colocamos a mensagem humana para "não foi possível conectar ao banco de dados" no banco de dados.
precisa saber é o seguinte

5

O problema dessa abordagem é que ela leva a um registro cada vez mais detalhado. 99,9999% dos quais você nunca verá.

Em vez disso, recomendo capturar o estado no início do seu processo e o sucesso / falha 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 na minha máquina de desenvolvimento 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, se necessário, posso evitar mais registros registrando apenas falhas de registro ou mantendo o estado em outro local (banco de dados? Fila de mensagens?)

Obviamente, precisamos ter cuidado extra 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 "Mensagem xyz com falha"


Colocar dados confidenciais em uma fila ainda está registrando-os. Isso é desaconselhável, assim como o armazenamento de entradas sensíveis no DB sem alguma forma de criptografia.
Jpmc26

se o seu sistema executar filas ou um banco de dados, os dados já estarão lá e a segurança também. Registrar em excesso é ruim apenas porque o registro tende a ficar fora de seus controles de segurança.
Ewan

Certo, mas esse é o ponto. Não é aconselhável, porque esses dados permanecem lá permanentemente e geralmente em texto completamente claro. Para dados confidenciais, é melhor não correr o risco e minimizar o período em que você os armazena e, em seguida, ter muito cuidado e cuidado com o modo como os armazena.
Jpmc26

É tradicionalmente permanente porque você está gravando em um arquivo. Mas uma fila de erros é transitória.
Ewan

Eu diria que provavelmente depende da implementação (e possivelmente até das configurações) da fila. Você não pode simplesmente despejá-lo em qualquer fila e esperar que seja seguro. E o que acontece depois que a fila é consumida? Os logs ainda devem estar em algum lugar para alguém visualizar. Além disso, esse não é um vetor de ataque extra que eu gostaria de abrir mesmo que temporariamente. Se um ataque descobrir que há dados confidenciais sendo enviados, até as entradas mais recentes podem ser valiosas. E também há o risco de alguém não conhecer e ativar um comutador para que ele também comece a registrar no disco. É apenas uma lata de vermes.
Jpmc26

1

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

public Foo GetFoo() {

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

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

     return aFoo;
}

Parece que você chamando o código não está configurado para lidar com o fato de o Foo não existir e você poderia 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 o Foo possa ser nulo quando recuperado e isso estiver correto, 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 o seu software travar ou agir 'estranhamente' em circunstâncias inesperadas me parece errado - se você precisa de um Foo e não consegue lidar com a ausência dele, é melhor travar do que tentar seguir um caminho que pode corromper seu sistema.


0

As bibliotecas de log adequadas fornecem mecanismos de extensão; portanto, se você quiser saber o método de origem de uma mensagem de log, elas poderão fazer isso imediatamente. Isso tem um impacto na execução, pois o processo requer a geração de um rastreamento de pilha e a sua passagem até que você esteja fora da biblioteca de criação de log.

Dito isso, realmente depende do que você deseja que seu ID faça por você:

  • Correlacionar as mensagens de erro fornecidas ao usuário nos seus logs?
  • Forneça notação sobre qual código estava sendo executado quando a mensagem foi gerada?
  • Acompanhar o nome da máquina e a instância do serviço?
  • Acompanhar a identificação do segmento?

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 que ferramentas como o 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 logs em um mecanismo de pesquisa central, você pode não apenas encontrar as ações mais longas, mas também chamar os logs que se aplicam a essa ação (como a pilha ELK ).

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

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.