Rendere il codice reperibile utilizzando ID messaggio univoci a livello globale


39

Un modello comune per individuare un bug segue questo script:

  1. Osservare la stranezza, ad esempio, nessun output o un programma sospeso.
  2. Individua il messaggio rilevante nel log o nell'output del programma, ad esempio "Impossibile trovare Foo". (Quanto segue è rilevante solo se questo è il percorso intrapreso per individuare il bug. Se una traccia dello stack o altre informazioni di debug sono prontamente disponibili, questa è un'altra storia.)
  3. Trova il codice in cui è stampato il messaggio.
  4. Esegui il debug del codice tra il primo punto in cui Foo inserisce (o deve inserire) l'immagine e dove viene stampato il messaggio.

Il terzo passo è quello in cui il processo di debug si interrompe spesso perché ci sono molti punti nel codice in cui Could not find {name}viene stampato "Impossibile trovare Foo" (o una stringa basata su modelli ). In effetti, più volte un errore di ortografia mi ha aiutato a trovare la posizione effettiva molto più velocemente di quanto avrei altrimenti - ha reso il messaggio unico in tutto il sistema e spesso in tutto il mondo, causando immediatamente un rilevante motore di ricerca.

La conclusione ovvia da ciò è che dovremmo usare ID di messaggi univoci a livello globale nel codice, codificandoli come parte della stringa di messaggi e verificando possibilmente che vi sia una sola occorrenza di ciascun ID nella base di codice. In termini di manutenibilità, quali sono i vantaggi e gli svantaggi di questa community di questo approccio e come lo implementeresti o assicureresti che l'implementazione non diventi mai necessaria (supponendo che il software abbia sempre dei bug)?


54
Utilizza invece le tracce dello stack. La traccia dello stack non solo ti dirà esattamente dove si è verificato l'errore, ma anche ogni funzione che ha chiamato ogni funzione che lo ha chiamato. Registra l'intera traccia quando si verifica un'eccezione, se necessario. Se stai lavorando in una lingua che non ha eccezioni, come C, questa è una storia diversa.
Robert Harvey,

6
@ l0b0 un piccolo consiglio sulla formulazione. "cosa pensa questa comunità ... pro e contro" sono frasi che possono essere considerate troppo ampie. Questo è un sito che consente domande "buone soggettive" e, in cambio, per consentire questo tipo di domande, ci si aspetta che l'OP faccia il lavoro di "guida" dei commenti e delle risposte verso un consenso significativo.
rwong

@rwong Grazie! Ritengo che la domanda abbia già ricevuto una risposta molto valida e puntuale, sebbene ciò possa essere stato meglio posto in un forum. Ho ritirato la mia risposta al commento di RobertHarvey dopo aver letto la risposta chiarificatrice di JohnWu, nel caso sia quello a cui ti riferisci. In caso contrario, hai qualche consiglio specifico per il pastore?
l0b0

1
I miei messaggi sembrano "Impossibile trovare Foo durante la chiamata a bar ()". Problema risolto. Scrollata di spalle. Il rovescio della medaglia è che è un po 'permeabile per essere visto dai clienti, ma tendiamo comunque a nascondere loro i dettagli dei messaggi di errore, rendendoli disponibili solo per gli amministratori di sistema che non potevano dare una scimmia che vedessero alcuni nomi di funzioni. In caso contrario, sì, un bel codice / ID univoco farà il trucco.
Lightness Races con Monica

1
Questo è MOLTO utile quando un cliente ti telefona e il suo computer non funziona in inglese! Molto meno un problema in questi giorni dato che ora abbiamo e-mail e file di registro .....
Ian

Risposte:


12

Nel complesso questa è una strategia valida e preziosa. Ecco alcuni pensieri.

Questa strategia è anche nota come "telemetria", nel senso che quando tutte queste informazioni vengono combinate, aiutano a "triangolare" la traccia dell'esecuzione e consentono a uno strumento di risoluzione dei problemi di dare un senso a ciò che l'utente / applicazione sta cercando di realizzare e ciò che è realmente accaduto .

Alcuni dati essenziali che devono essere raccolti (che tutti sappiamo) sono:

  • Posizione del codice, cioè stack di chiamate e riga approssimativa del codice
    • La "riga approssimativa di codice" non è necessaria se le funzioni sono ragionevolmente scomposte in unità adeguatamente piccole.
  • Qualsiasi dato pertinente al successo / fallimento della funzione
  • Un "comando" di alto livello che può inchiodare ciò che l'utente umano / agente esterno / utente API sta cercando di compiere.
    • L'idea è che un software accetterà ed elaborerà i comandi provenienti da qualche parte.
    • Durante questo processo, potrebbero aver avuto luogo da dozzine a centinaia o migliaia di chiamate di funzione.
    • Vorremmo che qualsiasi telemetria generata durante questo processo fosse rintracciabile al comando di livello più alto che innesca questo processo.
    • Per i sistemi basati sul web, la richiesta HTTP originale e i suoi dati sarebbero un esempio di tali "informazioni di richiesta di alto livello"
    • Per i sistemi con interfaccia grafica, l'utente che fa clic su qualcosa si adatterà a questa descrizione.

Spesso, gli approcci di registrazione tradizionali non sono sufficienti, a causa della mancata traccia di un messaggio di registro di basso livello al comando di livello più alto che lo attiva. Una traccia dello stack cattura solo i nomi delle funzioni di livello superiore che hanno aiutato a gestire il comando di livello più alto, non i dettagli (dati) che a volte sono necessari per caratterizzare quel comando.

Normalmente il software non è stato scritto per implementare questo tipo di requisiti di tracciabilità. Ciò rende più difficile correlare il messaggio di basso livello con il comando di alto livello. Il problema è particolarmente grave nei sistemi multi-thread liberamente, in cui molte richieste e risposte possono sovrapporsi e l'elaborazione può essere scaricata su un thread diverso rispetto al thread di ricezione della richiesta originale.

Pertanto, per ottenere il massimo valore dalla telemetria, saranno necessarie modifiche all'architettura generale del software. La maggior parte delle interfacce e delle chiamate di funzione dovranno essere modificate per accettare e propagare un argomento "tracciante".

Anche le funzioni di utilità dovranno aggiungere un argomento "tracer", in modo che se fallisce, il messaggio di log si permetterà di essere correlato con un certo comando di alto livello.

Un altro errore che renderà difficile la traccia della telemetria è la mancanza di riferimenti a oggetti (puntatori o riferimenti null). Quando mancano alcuni dati cruciali, potrebbe essere impossibile segnalare qualcosa di utile per l'errore.

In termini di scrittura dei messaggi di registro:

  • Alcuni progetti software potrebbero richiedere la localizzazione (traduzione in una lingua straniera) anche per i messaggi di registro destinati esclusivamente agli amministratori.
  • Alcuni progetti software potrebbero richiedere una chiara separazione tra dati sensibili e dati non sensibili, anche ai fini della registrazione, e che gli amministratori non avrebbero la possibilità di vedere accidentalmente determinati dati sensibili.
  • Non tentare di offuscare il messaggio di errore. Ciò minerebbe la fiducia dei clienti. Gli amministratori dei clienti si aspettano di leggere quei log e di dargli un senso. Non far loro sentire che esiste un segreto proprietario che deve essere nascosto agli amministratori dei clienti.
  • Aspettatevi che i clienti portino un pezzo di registro di telemetria e grigliano il personale del supporto tecnico. Si aspettano di saperlo. Formare il personale di supporto tecnico per spiegare correttamente il registro di telemetria.

1
In effetti, AOP ha propagandato, in primo luogo, la sua capacità intrinseca di risolvere questo problema - aggiungendo Tracer a ogni chiamata rilevante - con un'invasione minima alla base di codice.
vescovo

Vorrei anche aggiungere all'elenco dei "messaggi di log di scrittura" che è importante caratterizzare l'errore in termini di "perché" e "come risolvere" invece di "cosa" è accaduto.
vescovo

58

Immagina di avere una banale funzione di utilità che viene utilizzata in centinaia di posti nel tuo codice:

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

Se dovessimo fare come suggerisci, potremmo scrivere

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

Un errore che potrebbe verificarsi è se l'ingresso fosse zero; ciò comporterebbe una divisione per zero eccezioni.

Supponiamo quindi di vedere 27349262 nell'output o nei registri. Dove cerchi per trovare il codice che ha passato il valore zero? Ricorda, la funzione, con il suo ID univoco, viene utilizzata in centinaia di luoghi. Quindi, mentre potresti sapere che si è verificata la divisione per zero, non hai idea di chi 0sia.

Mi sembra che se ti preoccupi di registrare gli ID dei messaggi, puoi anche registrare la traccia dello stack.

Se la verbosità della traccia dello stack è ciò che ti disturba, non devi scaricarla come una stringa nel modo in cui il runtime ti dà. Puoi personalizzarlo. Ad esempio, se si desidera che una traccia dello stack abbreviata passi solo ai nlivelli, è possibile scrivere qualcosa del genere (se si utilizza 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 usalo in questo modo:

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)
            );  
        }
    }
}

Produzione:

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

Forse più semplice del mantenimento degli ID dei messaggi e più flessibile.

Ruba il mio codice da DotNetFiddle


32
Hmm immagino di non aver chiarito il punto abbastanza chiaramente. So che sono unici Robert-- per posizione del codice . Non sono univoci per percorso di codice . Conoscere la posizione è spesso inutile, ad esempio se il vero problema è che un input non è stato impostato correttamente. Ho modificato leggermente la mia lingua per enfatizzare.
John Wu,

1
Aspetti positivi, entrambi. Esiste un problema diverso con le tracce dello stack, che possono o meno interrompere la transazione a seconda della situazione: le loro dimensioni possono comportare la loro inondazione dei messaggi, soprattutto se si desidera includere l' intera traccia dello stack anziché una versione abbreviata come alcune lingue fare di default. Forse un'alternativa sarebbe scrivere separatamente un registro di traccia dello stack e includere indici numerati in quel registro nell'output dell'applicazione.
10

12
Se ne ricevi così tanti che sei preoccupato di inondare il tuo I / O, c'è qualcosa di gravemente sbagliato. O sei solo avaro? Il vero successo prestazionale è probabilmente lo svolgersi dello stack.
John Wu,

9
Modificato con una soluzione per accorciare le tracce dello stack, nel caso in cui si stiano scrivendo i log su un floppy da 3,5;)
John Wu

7
@JohnWu E inoltre, non dimenticare "IOException 'File non trovato' in [...]" che ti dice circa cinquanta strati dello stack di chiamate ma non dice quale esatto file sanguinante non è stato trovato.
Joker_vD,

6

SAP NetWeaver lo fa da decenni.

Ha dimostrato di essere uno strumento prezioso per la risoluzione degli errori nell'enorme codice Behemoth che è il tipico sistema ERP SAP.

I messaggi di errore sono gestiti in un repository centrale in cui ogni messaggio è identificato dalla sua classe e numero di messaggio.

Quando si desidera eseguire l'output di un messaggio di errore, si dichiarano solo variabili specifiche di classe, numero, gravità e messaggio. La rappresentazione testuale del messaggio viene creata in fase di esecuzione. Di solito vedi la classe e il numero del messaggio in qualsiasi contesto in cui appaiono i messaggi. Questo ha diversi effetti:

  • È possibile trovare automaticamente tutte le righe di codice nella base di codice ABAP che creano un messaggio di errore specifico.

  • È possibile impostare breakpoint dinamici del debugger che si attivano quando viene generato un messaggio di errore specifico.

  • È possibile cercare errori negli articoli della knowledge base di SAP e ottenere risultati di ricerca più pertinenti rispetto a se si cerca "Impossibile trovare Foo".

  • Le rappresentazioni testuali dei messaggi sono traducibili. Quindi, incoraggiando l'uso dei messaggi anziché delle stringhe, si ottengono anche le funzionalità i18n.

Un esempio di un popup di errore con numero di messaggio:

ERROR1

Cercare quell'errore nel repository degli errori:

ERROR2

Lo trovi nella base di codice:

error3

Tuttavia, ci sono degli svantaggi. Come puoi vedere, queste righe di codice non sono più auto-documentanti. Quando leggi il codice sorgente e vedi MESSAGEun'istruzione come quella nello screenshot qui sopra, puoi solo dedurre dal contesto cosa significa effettivamente. Inoltre, a volte le persone implementano gestori di errori personalizzati che ricevono la classe e il numero del messaggio in fase di esecuzione. In tal caso, l'errore non può essere trovato automaticamente o non può essere trovato nella posizione in cui si è effettivamente verificato l'errore. La soluzione alternativa per il primo problema è prendere l'abitudine di aggiungere sempre un commento nel codice sorgente indicando al lettore che cosa significa il messaggio. Il secondo è risolto aggiungendo del codice morto per assicurarsi che la ricerca automatica dei messaggi funzioni. Esempio:

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

Ma ci sono alcune situazioni in cui ciò non è possibile. Esistono, ad esempio, alcuni strumenti di modellazione dei processi aziendali basati sull'interfaccia utente in cui è possibile configurare i messaggi di errore da visualizzare in caso di violazione delle regole aziendali. L'implementazione di questi strumenti è completamente basata sui dati, quindi questi errori non verranno visualizzati nell'elenco dei siti utilizzati. Ciò significa che fare troppo affidamento sull'elenco di dove usato quando si cerca di trovare la causa di un errore può essere un'aringa rossa.


Anche i cataloghi di messaggi fanno parte di GNU / Linux - e UNIX generalmente come standard POSIX - per qualche tempo.
vescovo

@bishop Di solito non sto programmando specificamente per i sistemi POSIX, quindi non ne ho familiarità. Forse potresti pubblicare un'altra risposta che spiega i cataloghi dei messaggi POSIX e ciò che l'OP potrebbe imparare dalla loro implementazione.
Philipp

3
Facevo parte di un progetto che lo ha fatto negli anni '80. Un problema che abbiamo riscontrato è che, insieme a tutto il resto, abbiamo inserito il messaggio umano per "impossibile connettersi al database" nel database.
JimmyJames,

5

Il problema con questo approccio è che porta a una registrazione sempre più dettagliata. Il 99,9999% di cui non guarderai mai.

Invece, raccomando di acquisire lo stato all'inizio del processo e il successo / fallimento del processo.

Ciò consente di riprodurre il bug localmente, scorrere il codice e limitare la registrazione a due posizioni per processo. per esempio.

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

Ora posso usare lo stesso identico stato sulla mia macchina di sviluppo per riprodurre l'errore, scorrere il codice nel mio debugger e scrivere un nuovo test unit per confermare la correzione.

Inoltre, se necessario, posso evitare ulteriori registrazioni registrando solo errori o mantenendo lo stato altrove (database? Coda messaggi?)

Ovviamente dobbiamo prestare particolare attenzione alla registrazione di dati sensibili. Pertanto, ciò funziona particolarmente bene se la soluzione utilizza code di messaggi o il modello di archivio eventi. Poiché il registro deve solo dire "Messaggio xyz non riuscito"


Inserire dati sensibili in una coda significa comunque registrarli. Questo è sconsigliato, così come lo è la memorizzazione di input sensibili nel DB senza alcuna forma di crittografia.
jpmc26,

se il tuo sistema esegue code o un db, i dati sono già lì, e così dovrebbe essere la sicurezza. Registrare troppo è solo un male perché il registro tende a non rientrare nei controlli di sicurezza.
Ewan,

Giusto, ma questo è il punto. È sconsigliato perché i dati rimangono permanentemente lì e di solito in un testo completamente chiaro. Per i dati sensibili, è meglio non correre il rischio e ridurre al minimo il punto in cui vengono archiviati, quindi essere molto consapevoli e molto attenti a come vengono archiviati.
jpmc26

È tradizionalmente permanente perché stai scrivendo su un file. Ma una coda di errori è temporanea.
Ewan,

Direi che probabilmente dipende dall'implementazione (e forse anche dalle impostazioni) della coda. Non puoi semplicemente scaricarlo in qualsiasi coda e aspettarti che sia sicuro. E cosa succede dopo che la coda viene consumata? I registri devono essere ancora da qualche parte affinché qualcuno possa visualizzarli. Inoltre, questo non è un vettore di attacco extra che vorrei aprire anche temporaneamente. Se un attacco scopre che ci sono dati sensibili che vanno lì, anche le voci più recenti potrebbero essere preziose. E poi c'è il rischio che qualcuno non sappia e lanci un interruttore in modo che inizi anche a registrare su disco. È solo una lattina di vermi.
jpmc26

1

Vorrei suggerire che la registrazione non è la strada da percorrere, ma piuttosto che questa circostanza è considerata eccezionale (blocca il programma) e dovrebbe essere generata un'eccezione. Dì che il tuo codice era:

public Foo GetFoo() {

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

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

     return aFoo;
}

Sembra che il tuo codice di chiamata non sia impostato per far fronte al fatto che Foo non esiste e potresti potenzialmente essere:

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

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

     return aFoo;
}

E questo restituirà una traccia dello stack insieme all'eccezione che può essere utilizzata per facilitare il debug.

In alternativa, se ci aspettiamo che Foo possa essere nullo quando viene recuperato e ciò va bene, dobbiamo correggere i siti di chiamata:

void DoSomeFoo(Foo aFoo) {

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

    ... operations on Foo...
}

Il fatto che il tuo software si blocchi o agisca "in modo strano" in circostanze inaspettate mi sembra sbagliato - se hai bisogno di un Foo e non riesci a gestirlo non essendoci, allora sembra meglio schiantarsi piuttosto che tentare di procedere lungo un percorso che potrebbe corrompi il tuo sistema.


0

Le librerie di registrazione appropriate forniscono meccanismi di estensione, quindi se si desidera conoscere il metodo di origine di un messaggio di registro, possono farlo immediatamente. Ha un impatto sull'esecuzione poiché il processo richiede la generazione di una traccia dello stack e l'attraversamento fino a quando non si esce dalla libreria di registrazione.

Detto questo, dipende davvero da cosa vuoi che il tuo ID faccia per te:

  • Correlare i messaggi di errore forniti all'utente ai registri?
  • Fornire notazione su quale codice era in esecuzione al momento della generazione del messaggio?
  • Tieni traccia del nome della macchina e dell'istanza del servizio?
  • Tieni traccia dell'ID discussione?

Tutte queste cose possono essere fatte fuori dalla scatola con un software di registrazione adeguato (cioè no Console.WriteLine()o Debug.WriteLine()).

Personalmente, ciò che è più importante è la capacità di ricostruire percorsi di esecuzione. Questo è ciò che strumenti come Zipkin sono progettati per realizzare. Un ID per tracciare il comportamento di un'azione di un utente in tutto il sistema. Inserendo i log in un motore di ricerca centrale, è possibile non solo trovare le azioni più lunghe in esecuzione, ma richiamare i log che si applicano a quell'unica azione (come lo stack ELK ).

Gli ID opachi che cambiano ad ogni messaggio non sono molto utili. Un ID coerente utilizzato per tracciare il comportamento attraverso un'intera suite di microservizi ... immensamente utile.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.