Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add a custom property to ILogger

Tags:

c#

serilog

I want to add a custom property and value to an instance of ILogger obtained from ForContext. The idea being that I can tag all statements with some name, like "Render", or "AI", and then see that group name in the output and also filter on it.

All the examples I see online:

  • Use ForContext for the class name

I already do that but want this property in addition to the class name

  • PushProperty, using statements and global Context for per statement properties

I don't want that. I don't want the caller to have to do anything per statement.


Here is some code I have thus far for learning purposes:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Serilog;
using Serilog.Core;
using Serilog.Events;

namespace SerilogExtensions {

    public static class SerilogForContextExtension {

        /// <summary>
        /// Extension method that adds the class name to the source context associated with the logger interface
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="that"></param>
        /// <returns></returns>
        public static ILogger ForMyContext<T>(this ILogger that) =>
            that.ForContext(Constants.SourceContextPropertyName, typeof(T).Name);

        public static ILogger ForMyContextWithTag<T>(this ILogger that, string tag) {
            // TODO - What goes here?
            that.ForContext(Constants.SourceContextPropertyName, typeof(T).Name);
            return that;
        }

        /// <summary>
        /// Extension method that adds the class name to the source context associated with the logger interface
        /// For use with static classes
        /// </summary>
        /// <param name="that"></param>
        /// <param name="t"></param>
        /// <returns></returns>
        public static ILogger ForMyContextWithExplicitType(this ILogger that, Type t) =>
            that.ForContext(Constants.SourceContextPropertyName, t.Name);
    }

    /// <summary>
    /// POD Class, for serialization to and from file, that contains configurable option we will pass to Serilog
    /// </summary>
    public class LogConfiguration {

        public LogConfiguration() {
            DefaultLevel = LogEventLevel.Verbose;
            Enabled = new List<string>();
            Disabled = new List<string>();
            LogLevelsBySource = new Dictionary<string, LogEventLevel>() { {"SomeClass", 0 }, { "OtherClass", 0 }  };
            OutputTemplate = "[{ThreadId} {Level:u3} {SourceContext} {Tag}] {Message:lj}{NewLine}{Exception}";
        }

        /// <summary>
        /// The default logging level
        /// </summary>
        public LogEventLevel DefaultLevel { get; set; }

        /// <summary>
        /// Enable logging by source context class name
        /// </summary>
        public List<string> Enabled { get; set; }

        /// <summary>
        /// Disable logging by source context class name
        /// </summary>
        public List<string> Disabled { get; set; }

        /// <summary>
        /// Configure logging level by source context class name
        /// </summary>
        public Dictionary<string, LogEventLevel> LogLevelsBySource;

        /// <summary>
        /// Determines what each log message will look like. 
        /// Uses Serilog's rules
        /// </summary>
        public string OutputTemplate { get; set; }

        /// <summary>
        /// Overides any previous configuration Serilog is using with one dictated by this class' state
        /// </summary>
        public void ConfigureSerilog() {

            var configuration = new LoggerConfiguration()
                .MinimumLevel.ControlledBy(new Serilog.Core.LoggingLevelSwitch(DefaultLevel))
                .Enrich.WithThreadId()
                .Enrich.FromLogContext()
                .WriteTo.TextWriter(Console.Out, outputTemplate: OutputTemplate);

            var filterExpression = new StringBuilder();

            if(Enabled.Count > 0) {

                filterExpression.Append($"@Properties['{Serilog.Core.Constants.SourceContextPropertyName}'] in ['{Enabled[0]}'");
                for(int index = 1; index < Enabled.Count; ++index) {
                    filterExpression.Append($",'{Enabled[index]}'");
                }
                filterExpression.Append("]");

                configuration.Filter.ByIncludingOnly(filterExpression.ToString());
            }
            else if(Disabled.Count > 0) {

                filterExpression.Append($"@Properties['{Serilog.Core.Constants.SourceContextPropertyName}'] in ['{Disabled[0]}'");
                for (int index = 1; index < Disabled.Count; ++index) {
                    filterExpression.Append($",'{Disabled[index]}'");
                }
                filterExpression.Append("]");

                configuration.Filter.ByExcluding(filterExpression.ToString());
            }

            foreach(var logLevelForSource in LogLevelsBySource) {
                configuration.MinimumLevel.Override(logLevelForSource.Key, logLevelForSource.Value);
            }

            Log.Logger = configuration.CreateLogger();
        }
    }
}

using System;
using System.IO;

using Newtonsoft.Json;
using Serilog;
using SerilogExtensions;

namespace SeriConfigurable {
    public static class MyOptions {
        private static readonly object __lock = new object();
        private static FileSystemWatcher __logLevelWatcher;

        /// <summary>
        /// Allows us to configure Serilog from option in a file
        /// </summary>
        /// <param name="file"></param>
        private static void ReadLogLevel(string file) {

            LogConfiguration configuration = null;
            if (!File.Exists(file)) {
                configuration = new LogConfiguration();
                var jsonAsText = JsonConvert.SerializeObject(configuration);

                using (StreamWriter writer = new StreamWriter(file)) {
                    writer.Write(jsonAsText);
                }
            }
            else {
                using (StreamReader reader = new StreamReader(file)) {
                    var jsonAsText = reader.ReadToEnd();
                    configuration = JsonConvert.DeserializeObject<LogConfiguration>(jsonAsText);
                }
            }

            configuration.ConfigureSerilog();
        }

        public static void SetOptionsPath(string path) {
            lock (__lock) {
                string logLevelFile = Path.Combine(path, "logLevel");

                ReadLogLevel(logLevelFile);

                if (__logLevelWatcher != null) {
                    __logLevelWatcher.EnableRaisingEvents = false;
                    __logLevelWatcher.Dispose();
                }

                __logLevelWatcher = new FileSystemWatcher {
                    Path = Path.GetDirectoryName(logLevelFile),
                    Filter = Path.GetFileName(logLevelFile),
                    NotifyFilter = NotifyFilters.LastWrite
                };

                __logLevelWatcher.Changed += (sender, e) => { ReadLogLevel(e.FullPath); };
                __logLevelWatcher.EnableRaisingEvents = true;
            }
        }
    }
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

using Serilog;
using SerilogExtensions;
using Serilog.Sinks;

namespace SeriConfigurable {

    public class SomeClass {
        private static Serilog.ILogger _log = Serilog.Log.Logger.ForMyContext<SomeClass>();

        public SomeClass() {
            _log.Debug("Constructed");
        }

        public virtual void Foo() {
            _log.Verbose("Doing Verbose Stuff");
            _log.Information("Doing Information Stuff");
            _log.Debug("Doing Debug Stuff");
            _log.Warning("Doing Warning Stuff");
            _log.Error("Doing Error Stuff");
            _log.Fatal("Doing Fatal Stuff");

            var dummyData = new byte[] { 0x01, 0x03, 0xFF, 0x6E, 0xFF };
            StringBuilder hex = new StringBuilder(dummyData.Length * 6);
            foreach (byte oneByte in dummyData)
                hex.AppendFormat("0x{0:x2}, ", oneByte);

            _log.Verbose(string.Format("Received {0} bytes of data: {1}", dummyData.Length, hex.ToString()));
        }
    }

    public class OtherClass {
        private static Serilog.ILogger _log = Serilog.Log.Logger.ForMyContext<OtherClass>();

        public OtherClass() {
            _log.Debug("Constructed");
        }

        public void Foo() {
            _log.Verbose("Doing Verbose Stuff");
            _log.Information("Doing Information Stuff");
            _log.Debug("Doing Debug Stuff");
            _log.Warning("Doing Warning Stuff");
            _log.Error("Doing Error Stuff");
            _log.Fatal("Doing Fatal Stuff");
        }
    }

    public class DerivedClass : SomeClass {
        private static Serilog.ILogger _log = Serilog.Log.Logger.ForMyContextWithTag<DerivedClass>("Poop");

        public DerivedClass() {
            _log.Debug("Constructed");
        }

        public override void Foo() {

            _log.Verbose("Doing Verbose Stuff");
            _log.Information("Doing Information Stuff");
            _log.Debug("Doing Debug Stuff");
            _log.Warning("Doing Warning Stuff");
            _log.Error("Doing Error Stuff");
            _log.Fatal("Doing Fatal Stuff");

            try {
                MakeExceptions();
            }
            catch(Exception e) {
                _log.Error(e, "Bad Things");
            }
        }

        public void MakeExceptions() {
            var inner = new BadImageFormatException("You made us look at x86 things");
            var e = new ArgumentException("All of your arguments are bad. You skipped philosophy class", inner);
            throw e;
        }
    }

    class Program {
        static void Main(string[] args) {

            MyOptions.SetOptionsPath(System.IO.Path.GetDirectoryName(Assembly.GetEntryAssembly().Location));

            var someClass = new SomeClass();
            someClass.Foo();

            var otherClass = new OtherClass();
            otherClass.Foo();

            var derived = new DerivedClass();
            derived.Foo();
        }
    }
}

Expected Behavior:

[1 DBG OtherClass] Constructed
[1 INF OtherClass] Doing Information Stuff
[1 DBG OtherClass] Doing Debug Stuff
[1 WRN OtherClass] Doing Warning Stuff
[1 ERR OtherClass] Doing Error Stuff
[1 FTL OtherClass] Doing Fatal Stuff
[1 DBG DerivedClass Poop] Constructed
[1 VRB DerivedClass Poop] Doing Verbose Stuff
[1 INF DerivedClass Poop] Doing Information Stuff
[1 DBG DerivedClass Poop] Doing Debug Stuff
[1 WRN DerivedClass Poop] Doing Warning Stuff
[1 ERR DerivedClass Poop] Doing Error Stuff
[1 FTL DerivedClass Poop] Doing Fatal Stuff
[1 ERR DerivedClass Poop] Bad Things
System.ArgumentException: All of your arguments are bad. You skipped philosophy class ---> System.BadImageFormatException: You made us look at x86 things
   --- End of inner exception stack trace ---
   at SeriConfigurable.DerivedClass.MakeExceptions() in Program.cs:line 82
   at SeriConfigurable.DerivedClass.Foo() in Program.cs:line 72
like image 220
Christopher Pisz Avatar asked Nov 13 '18 22:11

Christopher Pisz


People also ask

How to add a custom logger to the ILogger?

To add a custom logger, you need to add an ILoggerProvider to the ILoggerFactory, that is provided in the method Configure in the Startup.cs: The ILoggerProvider creates one or more ILogger which are used by the framework to log the information.

What is iloggerfactory logger instance?

A logger instance which is created by a logger provider will only log to the associated logger provider. The ILoggerFactory logger factory instance is the boostrapper of the logging system: It is used to attach logger providers and create logger instances - either typed ( ILogger<T>) or untyped ( ILogger ).

How to use ILogger with a student constructor?

First, we create an instance of ILogger using the loggerFactory called logger. Then we pass it as an argument to the student constructor. Once we create a student instance the logger is going to write a message to our console output:

What does the iloggerprovider do?

The ILoggerProvider ’s only responsibility is to create ILogger instances which log to an actual sink. A logger instance which is created by a logger provider will only log to the associated logger provider.


1 Answers

public static ILogger ForMyContextWithTag<T>(this ILogger that, string tag) {
    // TODO - What goes here?
    that.ForContext(Constants.SourceContextPropertyName, typeof(T).Name);
    return that;
}

Needs to become:

public static ILogger ForMyContextWithTag<T>(this ILogger that, string tag) {
    return that
         .ForContext(Constants.SourceContextPropertyName, typeof(T).Name)
         .ForContext("Tag", tag);
}

Which is

public static ILogger ForMyContextWithTag<T>(this ILogger that, string tag) =>
   that.ForContext(Constants.SourceContextPropertyName, typeof(T).Name)
       .ForContext("Tag", tag);
like image 173
Ruben Bartelink Avatar answered Oct 05 '22 22:10

Ruben Bartelink