Rendere il codice rintracciabile utilizzando ID univoci globali

39

Un modello comune per individuare un bug segue questo script:

  1. Osserva la stranezza, ad esempio, nessun output o un programma sospeso.
  2. Individua il messaggio pertinente nell'output del registro o del programma, ad esempio "Impossibile trovare Foo". (Quanto segue è rilevante solo se questo è il percorso intrapreso per localizzare 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 viene stampato il messaggio.
  4. Esegui il debug del codice tra il primo posto che Foo immette (o dovrebbe inserire) l'immagine e dove viene stampato il messaggio.

Questo terzo passo è dove il processo di debug spesso si arresta perché ci sono molti posti nel codice in cui "Impossibile trovare Foo" (o una stringa di template Could not find {name} ) viene stampato. Infatti, più volte un errore di ortografia mi ha aiutato a trovare la posizione effettiva molto più rapidamente di quanto avrei fatto altrimenti - ha reso il messaggio unico nell'intero sistema e spesso in tutto il mondo, determinando un notevole successo nei motori di ricerca immediatamente.

L'ovvia conclusione di questo è che dovremmo usare gli ID di messaggi globalmente univoci nel codice, codificarli come parte della stringa del messaggio e possibilmente verificare che ci sia una sola occorrenza di ciascun ID nella base di codice. In termini di manutenibilità, cosa pensa questa community sono i pro e gli svantaggi più importanti di questo approccio, e come implementeresti questo o altrimenti assicurarti che l'implementazione non sia mai necessaria (supponendo che il software abbia sempre bug)?

    
posta l0b0 30.01.2018 - 03:54
fonte

6 risposte

12

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

Questa strategia è anche nota come "telemetria" nel senso che quando tutte queste informazioni vengono combinate, aiutano a "triangolare" la traccia di esecuzione e consentono a un programma di risoluzione dei problemi di dare un senso a ciò che l'utente / applicazione sta cercando di realizzare, e cosa è successo in realtà.

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

  • Posizione del codice, cioè stack di chiamate e la riga approssimativa del codice
    • "La linea approssimativa di codice" non è necessaria se le funzioni sono ragionevolmente scomposte in unità opportunamente piccole.
  • Qualsiasi pezzo di dati 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 realizzare.
    • L'idea è che un software accetti ed elabori comandi provenienti da qualche parte.
    • Durante questo processo, potrebbero essersi verificati dozzine di centinaia o migliaia di chiamate di funzione.
    • Vorremmo che qualsiasi telemetria generata durante questo processo fosse riconducibile al comando di livello più alto che attiva questo processo.
    • Per i sistemi basati sul Web, la richiesta HTTP originale e i relativi dati sarebbero un esempio di tali "informazioni di richiesta di alto livello"
    • Per i sistemi GUI, l'utente che fa clic su qualcosa si adatterebbe a questa descrizione.

Spesso, gli approcci di registrazione tradizionali sono insufficienti, a causa della mancata traccia di un messaggio di registro di basso livello che riporta al comando di livello più alto che lo attiva. Una traccia dello stack acquisisce solo i nomi delle funzioni di livello superiore che hanno aiutato a gestire il comando di livello più alto, non i dettagli (dati) a volte necessari per caratterizzare il 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 al comando di alto livello. Il problema è particolarmente peggiore nei sistemi liberamente multi-thread, in cui molte richieste e risposte possono sovrapporsi e l'elaborazione può essere scaricata su un thread diverso rispetto al thread di ricezione delle richieste 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 "tracciante", in modo che se fallisce, il messaggio di log si lascerà correlare con un certo comando di alto livello.

Un altro errore che rende difficile il tracciamento della telemetria è che mancano riferimenti a oggetti (puntatori o riferimenti null). Quando manca una parte fondamentale dei dati, potrebbe essere impossibile riportare 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 cercare di offuscare il messaggio di errore. Ciò minerebbe la fiducia dei clienti. Gli amministratori dei clienti si aspettano di leggere quei log e dare un senso a questo. Non farli sentire che esiste un segreto proprietario che deve essere nascosto agli amministratori dei clienti.
  • Aspettatevi che i clienti portino un pezzo di registro telemetrico e facciano da grill al personale di supporto tecnico. Si aspettano di sapere. Istruisci il tuo staff di supporto tecnico per spiegare correttamente il log di telemetria.
risposta data 30.01.2018 - 08:08
fonte
59

Immagina di avere una funzione di utilità banale che viene utilizzata in centinaia di punti del 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'input fosse zero; questo comporterebbe una divisione per eccezione zero.

Quindi diciamo che vedi 27349262 nel tuo output o nei tuoi registri. Dove cerchi di trovare il codice che ha superato il valore zero? Ricorda, la funzione-- con il suo ID univoco-- è usata in centinaia di posti. Così mentre tu puoi sapere che la divisione per zero è avvenuta, non hai idea di chi sia 0 .

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

Se la verbosità della traccia dello stack è ciò che ti infastidisce, non devi scaricarlo come una stringa come il runtime ti dà. Puoi personalizzarlo Ad esempio, se volessi una traccia abbreviata dello stack che andasse solo ai livelli n , potresti scrivere qualcosa come questo (se usi 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)
            );  
        }
    }
}

Output:

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

    
risposta data 30.01.2018 - 04:14
fonte
6

SAP NetWeaver lo fa da decenni.

Si è dimostrato uno strumento prezioso per la risoluzione di errori nel gigantesco codice di codice che è il tipico sistema SAP ERP.

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

Quando si desidera annullare un messaggio di errore, si indicano solo la classe, il numero, la gravità e le variabili specifiche del messaggio. La rappresentazione testuale del messaggio viene creata in fase di runtime. Di solito vedi la classe e il numero del messaggio in qualsiasi contesto in cui appaiono i messaggi. Questo ha molti effetti puliti:

  • È possibile trovare automaticamente qualsiasi riga di codice nella base di codici ABAP che crea un messaggio di errore specifico.

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

  • Puoi cercare gli errori negli articoli della knowledge base SAP e ottenere risultati di ricerca più pertinenti di quelli che cerchi "Impossibile trovare Foo".

  • Le rappresentazioni di testo dei messaggi sono traducibili. Quindi, incoraggiando l'uso dei messaggi invece delle stringhe, ottieni anche funzionalità i18n.

Un esempio di popup di errore con numero di messaggio:

Ricercadiquell'errorenelrepositorydeglierrori:

Trovalonellabasedicodice:

Tuttavia,cisonodeglisvantaggi.Comepuoivedere,questelineedicodicenonsonopiùauto-documentanti.Quandoleggiilcodicesorgenteevediun'istruzioneMESSAGEsimileaquellanell'immaginesopra,puoisolodedurredalcontestochecosasignificainrealtà.Inoltre,avoltelepersoneimplementanogestoridierroripersonalizzatichericevonolaclasseeilnumerodimessaggiinfasediruntime.Intalcaso,l'errorenonpuòesseretrovatoautomaticamenteononpuòesseretrovatonellaposizioneincuisièverificatoeffettivamentel'errore.Lasoluzioneperilprimoproblemaèdiprenderel'abitudinediaggiungeresempreuncommentonelcodicesorgentechediceallettorechecosasignificailmessaggio.Ilsecondoèrisoltoaggiungendouncodicemortoperassicurarsichelaricercaautomaticadeimessaggifunzioni.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 questo non è possibile. Esistono ad esempio alcuni strumenti di modellazione dei processi aziendali basati su UI in cui è possibile configurare i messaggi di errore affinché vengano visualizzati quando vengono violate le regole aziendali. L'implementazione di questi strumenti è completamente basata sui dati, quindi questi errori non verranno visualizzati nell'elenco dei casi in cui sono utilizzati. Ciò significa che affidarsi troppo alla lista dei casi in cui si è tentato di trovare la causa di un errore può essere una falsa pista.

    
risposta data 30.01.2018 - 14:55
fonte
5

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

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

Ciò consente di riprodurre localmente il bug, passare attraverso il codice e limitare la registrazione a due posizioni per processo. ad es.

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, passare il codice nel mio debugger e scrivere un nuovo test di unità per confermare la correzione.

In aggiunta, posso, se necessario, evitare più registrazioni solo da errori di registrazione o mantenendo lo stato altrove (coda dei messaggi del database?)

Ovviamente dobbiamo fare molta attenzione alla registrazione dei dati sensibili. Quindi questo funziona particolarmente bene se la tua soluzione sta usando code di messaggi o il modello di archivio di eventi. Poiché il registro deve solo pronunciare "Messaggio xyz non riuscito"

    
risposta data 30.01.2018 - 14:40
fonte
1

Suggerirei che la registrazione non è la strada da percorrere per questo, 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 ritirato e che 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 blocca o agisca "stranamente" in circostanze inaspettate mi sembra sbagliato - se hai bisogno di un Foo e non riesci a gestirlo non essendoci, allora sembra meglio andare in crash piuttosto che tentare di procedere lungo un percorso che potrebbe danneggiare il tuo sistema.

    
risposta data 30.01.2018 - 15:06
fonte
0

Le librerie di registrazione appropriate forniscono meccanismi di estensione, quindi se si desidera conoscere il metodo da cui ha origine un messaggio di registro, è possibile farlo immediatamente. Ha un impatto sull'esecuzione poiché il processo richiede la generazione di una traccia di stack e la sua attraversazione finché non si esce dalla libreria di logging.

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

  • Correlare i messaggi di errore forniti all'utente ai tuoi registri?
  • Fornire notazione su quale codice era in esecuzione quando il messaggio è stato generato?
  • Tieni traccia del nome del computer e dell'istanza del servizio?
  • Tieni traccia dell'ID del thread?

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

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

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

    
risposta data 30.01.2018 - 19:38
fonte

Leggi altre domande sui tag