Bonnes pratiques en matière d'enrubanneuse de journaux

91

Je veux utiliser un nlogger dans mon application, peut-être que dans le futur je devrai changer le système de journalisation. Je veux donc utiliser une façade forestière.

Connaissez-vous des recommandations pour des exemples existants comment les écrire? Ou donnez-moi simplement un lien vers les meilleures pratiques dans ce domaine.

Marcheur de nuit
la source
4
Il existe déjà: netcommon.sourceforge.net
R. Martinho Fernandes
Avez-vous vérifié le projet The Simple Logging Facade sur Codeplex ?
JefClaes

Réponses:

206

J'avais l'habitude d'utiliser des façades de journalisation telles que Common.Logging (même pour masquer ma propre bibliothèque CuttingEdge.Logging ), mais de nos jours, j'utilise le modèle d'injection de dépendance et cela me permet de cacher les enregistreurs derrière ma propre abstraction (simple) qui adhère à la fois à Dependency Principe d'inversion et principe de ségrégation d'interface(FAI) parce qu'il a un membre et parce que l'interface est définie par mon application; pas une bibliothèque externe. Minimiser les connaissances que les parties centrales de votre application ont sur l'existence de bibliothèques externes, mieux c'est; même si vous n'avez jamais l'intention de remplacer votre bibliothèque de journalisation. La forte dépendance à la bibliothèque externe rend le test de votre code plus difficile et complique votre application avec une API qui n'a jamais été conçue spécifiquement pour votre application.

Voici à quoi ressemble souvent l'abstraction dans mes applications:

public interface ILogger
{
    void Log(LogEntry entry);
}

public enum LoggingEventType { Debug, Information, Warning, Error, Fatal };

// Immutable DTO that contains the log information.
public class LogEntry 
{
    public readonly LoggingEventType Severity;
    public readonly string Message;
    public readonly Exception Exception;

    public LogEntry(LoggingEventType severity, string message, Exception exception = null)
    {
        if (message == null) throw new ArgumentNullException("message");
        if (message == string.Empty) throw new ArgumentException("empty", "message");

        this.Severity = severity;
        this.Message = message;
        this.Exception = exception;
    }
}

En option, cette abstraction peut être étendue avec quelques méthodes d'extension simples (permettant à l'interface de rester étroite et de continuer à adhérer au FAI). Cela rend le code pour les consommateurs de cette interface beaucoup plus simple:

public static class LoggerExtensions
{
    public static void Log(this ILogger logger, string message) {
        logger.Log(new LogEntry(LoggingEventType.Information, message));
    }

    public static void Log(this ILogger logger, Exception exception) {
        logger.Log(new LogEntry(LoggingEventType.Error, exception.Message, exception));
    }

    // More methods here.
}

Étant donné que l'interface ne contient qu'une seule méthode, vous pouvez facilement créer une ILoggerimplémentation qui effectue un proxy vers log4net , Serilog , Microsoft.Extensions.Logging , NLog ou toute autre bibliothèque de journalisation et configurer votre conteneur DI pour l'injecter dans les classes qui ont un ILoggerdans leur constructeur.

Notez qu'avoir des méthodes d'extension statiques au-dessus d'une interface avec une seule méthode est assez différent d'avoir une interface avec de nombreux membres. Les méthodes d'extension ne sont que des méthodes d'assistance qui créent un LogEntrymessage et le transmettent à la seule méthode de l' ILoggerinterface. Les méthodes d'extension font partie du code du consommateur; ne fait pas partie de l'abstraction. Non seulement cela permet aux méthodes d'extension d'évoluer sans qu'il soit nécessaire de changer l'abstraction, les méthodes d'extension et leLogEntryconstructeur sont toujours exécutés lorsque l'abstraction du journal est utilisée, même lorsque ce logger est stubbed / mocked. Cela donne plus de certitude quant à l'exactitude des appels à l'enregistreur lors de l'exécution dans une suite de tests. L'interface à un membre facilite également les tests; Avoir une abstraction avec de nombreux membres rend difficile la création d'implémentations (telles que des simulacres, des adaptateurs et des décorateurs).

Lorsque vous faites cela, il n'y a presque jamais besoin d'une abstraction statique que les façades de journalisation (ou toute autre bibliothèque) pourraient offrir.

Steven
la source
4
@GabrielEspinoza: Cela dépend totalement de l'espace de noms dans lequel vous placez les méthodes d'extension. Si vous les placez dans le même espace de noms que l'interface ou dans un espace de noms racine de votre projet, le problème n'existera pas.
Steven le
2
@ user1829319 Ce n'est qu'un exemple. Je suis sûr que vous pouvez proposer une implémentation basée sur cette réponse qui répond à vos besoins particuliers.
Steven
2
Je ne comprends toujours pas ... où est l'avantage d'avoir les 5 méthodes Logger comme extensions d'ILogger et de ne pas être membre d'ILogger?
Elisabeth
3
@Elisabeth L'avantage est que vous pouvez adapter l'interface de façade à TOUT cadre de journalisation, simplement en implémentant une seule fonction: "ILogger :: Log." Les méthodes d'extension garantissent que nous avons accès aux API «pratiques» (telles que «LogError», «LogWarning», etc.), quel que soit le framework que vous décidez d'utiliser. C'est un moyen détourné d'ajouter des fonctionnalités communes de «classe de base», malgré l'utilisation d'une interface C #.
BTownTKD
2
Je dois souligner à nouveau une raison pour laquelle c'est génial. Conversion ascendante de votre code de DotNetFramework vers DotNetCore. Les projets où j'ai fait cela, je n'avais qu'à écrire un seul nouveau béton. Celles où je n'ai pas ... gaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa! Je suis content d'avoir trouvé ce "chemin du retour".
grenadeCoder
10

J'ai utilisé le petit wrapper d'interface + adaptateur de https://github.com/uhaciogullari/NLog.Interface qui est également disponible via NuGet :

PM> Install-Package NLog.Interface 
Jon Adams
la source
10
Il existe une interface ILogger dans la bibliothèque NLog par v4.0. Vous n'avez plus besoin de cette bibliothèque
Jowen
8

Pour le moment, le meilleur pari est d'utiliser le package Microsoft.Extensions.Logging ( comme l'a souligné Julian ). La plupart des frameworks de journalisation peuvent être utilisés avec cela.

Définir votre propre interface, comme expliqué dans la réponse de Steven, est OK pour les cas simples, mais il manque quelques éléments que je considère importants:

  • Logging structuré et objets de-structuration (la notation @ dans Serilog et NLog)
  • Construction / mise en forme de chaîne retardée: comme il prend une chaîne, il doit tout évaluer / formater à l'appel, même si au final l'événement ne sera pas journalisé car il est en dessous du seuil (coût de performance, voir point précédent)
  • Contrôles conditionnels comme IsEnabled(LogLevel)vous le souhaitez, pour des raisons de performances encore une fois

Vous pouvez probablement implémenter tout cela dans votre propre abstraction, mais à ce stade, vous réinventerez la roue.

Philippe
la source
4

En général, je préfère créer une interface comme

public interface ILogger
{
 void LogInformation(string msg);
 void LogError(string error);
}

et dans le runtime, j'injecte une classe concrète qui est implémentée depuis cette interface.

crypté
la source
11
Et ne pas oublier les LogWarninget LogCriticalméthodes et leurs surcharges. En faisant cela, vous violerez le principe de séparation des interfaces . Préférez définir l' ILoggerinterface avec une seule Logméthode.
Steven
2
Je suis vraiment désolé, ce n'était pas mon intention. Pas besoin d'avoir honte. Je vois cette conception très souvent parce que de nombreux développeurs utilisent le populaire log4net (qui utilise cette conception exacte) comme exemple. Malheureusement, ce design n'est pas vraiment bon.
Steven
2
Je préfère cela à la réponse de @ Steven. Il introduit une dépendance à LogEntry, et donc une dépendance à LoggingEventType. L' ILoggerimplémentation doit LoggingEventTypesprobablement les gérer case/switch, ce qui est une odeur de code . Pourquoi cacher la LoggingEventTypesdépendance? La mise en œuvre doit gérer les niveaux de journalisation de toute façon , il serait préférable d' explicite sur ce que devrait faire une mise en œuvre, plutôt que de se cacher derrière une seule méthode avec un argument général.
DharmaTurtle
1
À titre d'exemple extrême, imaginez un ICommandqui a un Handlequi prend un object. Les implémentations doivent case/switchsur les types possibles afin de remplir le contrat de l'interface. Ce n'est pas idéal. N'ayez pas d'abstraction qui cache une dépendance qui doit de toute façon être gérée. Au lieu de cela, ayez une interface qui énonce clairement ce qui est attendu: "Je m'attends à ce que tous les enregistreurs gèrent les avertissements, erreurs, fatals, etc.". Ceci est préférable à "Je m'attends à ce que tous les enregistreurs gèrent les messages qui incluent des avertissements, des erreurs, des fatals, etc."
DharmaTurtle
Je suis plutôt d'accord avec @Steven et @DharmaTurtle. En outre, il LoggingEventTypedoit être appelé LoggingEventLevelcar les types sont des classes et doivent être codés comme tels dans la POO. Pour moi, il n'y a aucune différence entre ne pas utiliser de méthode d'interface et ne pas utiliser la enumvaleur correspondante . Utilisez plutôt ErrorLoggger : ILogger, InformationLogger : ILoggeroù chaque enregistreur définit son propre niveau. Ensuite, la DI doit injecter les enregistreurs nécessaires, probablement via une clé (enum), mais cette clé ne fait plus partie de l'interface. (Vous êtes maintenant SOLIDE).
Wouter
4

Une excellente solution à ce problème est apparue sous la forme du projet LibLog .

LibLog est une abstraction de journalisation avec prise en charge intégrée des principaux enregistreurs, notamment Serilog, NLog, Log4net et Enterprise. Il est installé via le gestionnaire de packages NuGet dans une bibliothèque cible en tant que fichier source (.cs) au lieu d'une référence .dll. Cette approche permet d'inclure l'abstraction de journalisation sans forcer la bibliothèque à prendre une dépendance externe. Il permet également à un auteur de bibliothèque d'inclure la journalisation sans forcer l'application consommatrice à fournir explicitement un enregistreur à la bibliothèque. LibLog utilise la réflexion pour déterminer quel enregistreur concret est utilisé et s'y connecter sans aucun code de câblage explicite dans le (s) projet (s) de bibliothèque.

Ainsi, LibLog est une excellente solution pour la journalisation dans les projets de bibliothèque. Il suffit de référencer et de configurer un enregistreur concret (Serilog pour la victoire) dans votre application ou service principal et d'ajouter LibLog à vos bibliothèques!

Rob Davis
la source
Je l'ai utilisé pour surmonter le problème de changement de rupture de log4net ( beurk ) ( wiktorzychla.com/2012/03/pathetic-breaking-change-between.html ) Si vous obtenez cela de nuget, il créera en fait un fichier .cs dans votre code plutôt que d'ajouter des références à des DLL précompilées. Le fichier .cs est associé à votre projet. Donc, si vous avez différentes couches (csprojs), vous aurez soit plusieurs versions, soit vous devrez consolider dans un csproj partagé. Vous comprendrez cela lorsque vous essayez de l'utiliser. Mais comme je l'ai dit, ce fut une bouée de sauvetage avec le problème de changement de rupture de log4net.
granadaCoder