web-dev-qa-db-fra.com

Rendre le code détectable à l'aide d'ID de message uniques au monde

Un modèle courant pour localiser un bogue suit ce script:

  1. Observez l'étrangeté, par exemple, aucune sortie ou un programme suspendu.
  2. Recherchez le message pertinent dans le journal ou la sortie du programme, par exemple, "Impossible de trouver Foo". (Ce qui suit n'est pertinent que s'il s'agit du chemin emprunté pour localiser le bogue. Si une trace de pile ou d'autres informations de débogage sont facilement disponibles, c'est une autre histoire.)
  3. Recherchez le code où le message est imprimé.
  4. Déboguez le code entre le premier endroit où Foo entre (ou devrait entrer) l'image et où le message est imprimé.

Cette troisième étape est celle où le processus de débogage s'arrête souvent car il y a de nombreux endroits dans le code où "Can't find Foo" (ou une chaîne de modèle Could not find {name}) est imprimé. En fait, plusieurs fois un faute d'orthographe m'a aidé à trouver l'emplacement réel beaucoup plus rapidement que je ne le ferais autrement - cela a rendu le message unique sur l'ensemble du système et souvent à travers le monde, ce qui a donné lieu à un hit du moteur de recherche pertinent immédiatement.

La conclusion évidente de ceci est que nous devons utiliser des ID de message globalement uniques dans le code, le coder en dur dans le cadre de la chaîne de message et éventuellement vérifier qu'il n'y a qu'une seule occurrence de chaque ID dans la base de code. En termes de maintenabilité, quels sont les avantages et les inconvénients les plus importants de cette approche pour cette communauté, et comment pourriez-vous l'implémenter ou vous assurer que sa mise en œuvre ne deviendra jamais nécessaire (en supposant que le logiciel aura toujours des bogues)?

39
l0b0

Dans l'ensemble, il s'agit d'une stratégie valable et valable. Voici quelques réflexions.

Cette stratégie est également connue sous le nom de "télémétrie" en ce sens que lorsque toutes ces informations sont combinées, elles aident à "trianguler" la trace d'exécution et permettent à un utilitaire de résolution des problèmes de comprendre ce que l'utilisateur/l'application essaie d'accomplir et ce qui s'est réellement passé. .

Voici quelques informations essentielles à collecter (que nous connaissons tous):

  • Emplacement du code, c'est-à-dire la pile d'appels et la ligne de code approximative
    • Une "ligne de code approximative" n'est pas nécessaire si les fonctions sont raisonnablement décomposées en unités suffisamment petites.
  • Toutes les données pertinentes pour le succès/l'échec de la fonction
  • Une "commande" de haut niveau qui peut définir ce que l'utilisateur humain/l'agent externe/l'utilisateur de l'API essaie d'accomplir.
    • L'idée est qu'un logiciel accepte et traite les commandes provenant de quelque part.
    • Au cours de ce processus, des dizaines à des centaines à des milliers d'appels de fonction peuvent avoir eu lieu.
    • Nous aimerions que toute télémétrie générée tout au long de ce processus soit traçable jusqu'à la commande de plus haut niveau qui déclenche ce processus.
    • Pour les systèmes basés sur le Web, la requête HTTP d'origine et ses données seraient un exemple de ces "informations de requête de haut niveau"
    • Pour les systèmes GUI, l'utilisateur cliquant sur quelque chose correspondrait à cette description.

Souvent, les approches de journalisation traditionnelles échouent, en raison de l'échec du suivi d'un message de journal de bas niveau vers la commande de plus haut niveau qui le déclenche. ne trace de pile ne capture que les noms des fonctions supérieures qui ont aidé à gérer la commande de plus haut niveau, pas les détails (données) qui sont parfois nécessaires pour caractériser cette commande.

Normalement, aucun logiciel n'a été écrit pour implémenter ce type d'exigences de traçabilité. Cela rend plus difficile la corrélation du message de bas niveau avec la commande de haut niveau. Le problème est particulièrement pire dans les systèmes librement multithreads, où de nombreuses demandes et réponses peuvent se chevaucher et le traitement peut être déchargé sur un thread différent de celui du thread de réception de requêtes d'origine.

Ainsi, pour tirer le meilleur parti de la télémétrie, des modifications de l'architecture logicielle globale seront nécessaires. La plupart des interfaces et des appels de fonction devront être modifiés pour accepter et propager un argument "traceur".

Même les fonctions utilitaires devront ajouter un argument "traceur", de sorte qu'en cas d'échec, le message de journal se permette d'être corrélé avec une certaine commande de haut niveau.

Un autre échec qui rendra le suivi de la télémétrie difficile est l'absence de références d'objet (pointeurs ou références nuls). Lorsqu'une donnée cruciale manque, il peut être impossible de signaler quoi que ce soit d'utile à l'échec.

En termes d'écriture des messages du journal:

  • Certains projets logiciels peuvent nécessiter une localisation (traduction dans une langue étrangère) même pour les messages de journal destinés uniquement aux administrateurs.
  • Certains projets logiciels peuvent nécessiter une séparation claire entre les données sensibles et les données non sensibles, même à des fins de journalisation, et que les administrateurs n'auraient aucune chance de voir accidentellement certaines données sensibles.
  • N'essayez pas de masquer le message d'erreur. Cela minerait la confiance des clients. Les administrateurs des clients s'attendent à lire ces journaux et à les comprendre. Ne leur faites pas sentir qu'il existe un secret propriétaire qui doit être caché aux administrateurs des clients.
  • Attendez-vous à ce que les clients apportent un morceau de journal de télémétrie et grillent votre personnel de support technique. Ils s'attendent à savoir. Formez votre personnel de support technique pour expliquer correctement le journal de télémétrie.
12
rwong

Imaginez que vous ayez une fonction utilitaire triviale qui est utilisée dans des centaines d'endroits dans votre code:

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

Si nous devions faire ce que vous suggérez, nous pourrions écrire

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

Une erreur qui pourrait se produire si l'entrée était nulle; cela entraînerait une division par exception de zéro.

Supposons donc que vous voyez 27349262 dans votre sortie ou vos journaux. Où cherchez-vous pour trouver le code qui a passé la valeur zéro? N'oubliez pas que la fonction - avec son ID unique - est utilisée dans des centaines d'endroits. Donc, bien que vous sachiez que la division par zéro s'est produite, vous n'avez aucune idée de qui 0 c'est.

Il me semble que si vous allez déranger la journalisation des ID de message, vous pouvez également enregistrer la trace de la pile.

Si la verbosité de la trace de la pile vous dérange, vous n'avez pas besoin de la vider sous forme de chaîne de la façon dont le runtime vous la donne. Vous pouvez le personnaliser. Par exemple, si vous vouliez une trace de pile abrégée allant uniquement aux niveaux n, vous pourriez écrire quelque chose comme ceci (si vous utilisez 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()
        );
    }
}

Et utilisez-le comme ceci:

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

Production:

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

Peut-être plus facile que de maintenir les ID de message et plus flexible.

Volez mon code à DotNetFiddle

58
John Wu

SAP NetWeaver le fait depuis des décennies.

Il s'est avéré être un outil précieux lors du dépannage d'erreurs dans le géant du code massif qu'est le système SAP ERP.

Les messages d'erreur sont gérés dans un référentiel central où chaque message est identifié par sa classe et son numéro de message.

Lorsque vous souhaitez afficher un message d'erreur, vous indiquez uniquement les variables de classe, de nombre, de gravité et spécifiques au message. La représentation textuelle du message est créée lors de l'exécution. Vous voyez généralement la classe et le numéro du message dans n'importe quel contexte où les messages apparaissent. Cela a plusieurs effets intéressants:

  • Vous pouvez trouver automatiquement toutes les lignes de code dans la base de code ABAP qui créent un message d'erreur spécifique.

  • Vous pouvez définir des points d'arrêt de débogueur dynamique qui se déclenchent lorsqu'un message d'erreur spécifique est généré.

  • Vous pouvez rechercher des erreurs dans les articles de la base de connaissances SAP et obtenir des résultats de recherche plus pertinents que si vous recherchez "Impossible de trouver Foo".

  • Les représentations textuelles des messages sont traduisibles. Ainsi, en encourageant l'utilisation de messages au lieu de chaînes, vous obtenez également des capacités i18n.

Un exemple de popup d'erreur avec le numéro de message:

error1

Recherche de cette erreur dans le référentiel d'erreurs:

error2

Trouvez-le dans la base de code:

error3

Cependant, il y a des inconvénients. Comme vous pouvez le voir, ces lignes de code ne sont plus auto-documentées. Lorsque vous lisez le code source et voyez une instruction MESSAGE comme celles de la capture d'écran ci-dessus, vous ne pouvez déduire du contexte que ce que cela signifie réellement. De plus, les utilisateurs implémentent parfois des gestionnaires d'erreurs personnalisés qui reçoivent la classe et le numéro du message lors de l'exécution. Dans ce cas, l'erreur ne peut pas être trouvée automatiquement ou ne peut pas être trouvée à l'emplacement où l'erreur s'est réellement produite. La solution de contournement pour le premier problème est de prendre l'habitude de toujours ajouter un commentaire dans le code source indiquant au lecteur ce que signifie le message. Le second est résolu en ajoutant du code mort pour vous assurer que la recherche automatique des messages fonctionne. Exemple:

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

Mais il y a des situations où cela n'est pas possible. Il existe par exemple certains outils de modélisation de processus métier basés sur l'interface utilisateur dans lesquels vous pouvez configurer des messages d'erreur à afficher lorsque les règles métier sont violées. L'implémentation de ces outils est entièrement basée sur les données, de sorte que ces erreurs n'apparaîtront pas dans la liste des cas d'utilisation. Cela signifie que trop s'appuyer sur la liste des cas d'utilisation lorsque vous essayez de trouver la cause d'une erreur peut être un hareng rouge.

6
Philipp

Le problème avec cette approche est qu'elle conduit à une journalisation de plus en plus détaillée. 99,9999% dont vous ne regarderez jamais.

Au lieu de cela, je recommande de capturer l'état au début de votre processus et le succès/l'échec du processus.

Cela vous permet de reproduire le bogue localement, de parcourir le code et de limiter votre journalisation à deux emplacements par processus. par exemple.

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

Maintenant, je peux utiliser exactement le même état sur ma machine de développement pour reproduire l'erreur, parcourir le code dans mon débogueur et écrire un nouveau test unitaire pour confirmer le correctif.

De plus, je peux si nécessaire éviter plus de journalisation en ne journalisant que les échecs ou en conservant l'état ailleurs (base de données? File d'attente de messages?)

Évidemment, nous devons être extrêmement prudents lors de la journalisation des données sensibles. Cela fonctionne donc particulièrement bien si votre solution utilise des files d'attente de messages ou le modèle de magasin d'événements. Comme le journal n'a qu'à dire "Message xyz a échoué"

5
Ewan

Je suggérerais que la journalisation n'est pas la façon de procéder, mais plutôt que cette circonstance est considérée comme exceptionnelle (elle verrouille votre programme) et une exception doit être levée. Dites que votre code était:

public Foo GetFoo() {

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

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

     return aFoo;
}

Il semble que vous n'appeliez pas de code pour gérer le fait que Foo n'existe pas et que vous pourriez potentiellement être:

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

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

     return aFoo;
}

Et cela retournera une trace de pile avec l'exception qui peut être utilisée pour aider au débogage.

Alternativement, si nous nous attendons à ce que Foo puisse être nul lorsqu'il est récupéré et que c'est bien, nous devons corriger les sites appelants:

void DoSomeFoo(Foo aFoo) {

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

    ... operations on Foo...
}

Le fait que votre logiciel se bloque ou agisse `` bizarrement '' dans des circonstances inattendues me semble mal - si vous avez besoin d'un Foo et que vous ne pouvez pas le gérer sans qu'il soit là, alors il semble préférable de tomber en panne que d'essayer de suivre un chemin qui peut corrompre votre système.

1
Paddy

Les bibliothèques de journalisation appropriées fournissent des mécanismes d'extension, donc si vous souhaitez connaître la méthode d'où provient un message de journal, elles peuvent le faire dès le départ. Cela a un impact sur l'exécution car le processus nécessite de générer une trace de pile et de la parcourir jusqu'à ce que vous soyez hors de la bibliothèque de journalisation.

Cela dit, cela dépend vraiment de ce que vous voulez que votre ID fasse pour vous:

  • Corréler les messages d'erreur fournis à l'utilisateur avec vos journaux?
  • Fournir une notation sur le code qui s'exécutait lorsque le message a été généré?
  • Gardez une trace du nom de la machine et de l'instance de service?
  • Gardez une trace de l'ID du fil?

Toutes ces choses peuvent être effectuées hors de la boîte avec un logiciel de journalisation approprié (c'est-à-dire pas Console.WriteLine() ou Debug.WriteLine()).

Personnellement, ce qui est plus important, c'est la possibilité de reconstruire les chemins d'exécution. C'est ce que des outils comme Zipkin sont conçus pour accomplir. Un ID pour tracer le comportement d'une action utilisateur dans tout le système. En plaçant vos journaux dans un moteur de recherche central, vous pouvez non seulement trouver les actions les plus longues, mais également appeler les journaux qui s'appliquent à cette action (comme pile ELK ).

Les identifiants opaques qui changent avec chaque message ne sont pas très utiles. Un identifiant cohérent utilisé pour tracer le comportement à travers une suite complète de microservices ... extrêmement utile.

0
Berin Loritsch