8

My ASP.NET Core 2.1 app logs to a Serilog file sink, all the "usual stuff" - i.e. app related stuff such as debug, monitoring, performance, etc.

However we also need to log other data to a separate file. Not app related, but customer related - the kind of stuff that should go into a database. However for legacy reasons, there is no database on this system and so that data needs to be saved to a file instead. Obviously this can't be written to the same log file.

I could just write to a FileStream. But I prefer to do structured logging with Serilog.

So is there a way to have two loggers simultaneously? Which log different data to different file sinks.

(If so, how do I inject them into my classes - right now I just inject ILogger<ClassName>.)

Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
lonix
  • 14,255
  • 23
  • 85
  • 176
  • You seem to think database and files are your only options. There are a [ton of sinks](https://github.com/serilog/serilog/wiki/Provided-Sinks). At my company, we love Seq. – mason Jul 06 '18 at 16:00
  • 1
    @mason Unfortunately those are the only options for this project. Besides, even if I use a different sink I'm unsure how it would solve the issue (log different data to different sinks - probably with the use of different loggers). – lonix Jul 06 '18 at 16:18

3 Answers3

9

The simplest way I've managed to do this was to implement a new unique interface that I could pass to the DI system to be consumed by the controllers.
Note: In the example I used the Serilog.Sinks.RollingFile NuGet package. Use any other sinks as you see fit. I use asp.net Core 2.1.

New Interface

using Serilog;
using Serilog.Core;

public interface ICustomLogger
{
    ILogger Log { get; }
}

public class CustomLogger : ICustomLogger
{
    private readonly Logger _logger;

    public ILogger Log { get { return _logger; } }

    public CustomLogger( Logger logger )
    {
        _logger = logger;
    }
}

Implementation in Startup.cs (prior to .Net 6)

using Serilog;
...
public void ConfigureServices( IServiceCollection services )
{
    ...

    ICustomLogger customLogger = new CustomLogger( new LoggerConfiguration()
        .MinimumLevel.Debug()
        .WriteTo.RollingFile( @"Logs\CustomLog.{Date}.log", retainedFileCountLimit: 7 )
        .CreateLogger() );
    services.AddSingleton( customLogger );
     
    ...
}

Implementation in Program.cs (.Net 6)

using Serilog;
...

... 
ICustomLogger customLogger = new CustomLogger( new LoggerConfiguration()
    .MinimumLevel.Debug()
    .WriteTo.RollingFile( @"Logs\CustomLog.{Date}.log", retainedFileCountLimit: 7 )
    .CreateLogger() );
builder.Services.AddSingleton( customLogger );
     
...

Usage in Controller

public class MyTestController : Controller
{
    private readonly ICustomLogger _customLogger;

    public MyTestController( ICustomLogger customLogger )
    {
        _customLogger = customLogger;
    }

    public IActionResult Index()
    {
        ...
        _customLogger.Log.Debug( "Serving Index" );
        ...
    }
}
drewid
  • 2,737
  • 3
  • 16
  • 11
Christo Carstens
  • 741
  • 7
  • 14
  • Thanks for this. I was able to use this with the .Net 6 format and also inject this into controllers and blazor as well. Instead of implementing in Startup.cs it happens in Program.cs and instead of services.AddSingleton it becomes builder.Services.AddSingleton What is confusing is that most Dependency injection shows having the injected but in this case it isn't needed. And DI is critical, otherwise it can't be reused by the application and is locked if doing it on a controller by controller or page by page basis. – drewid Dec 16 '21 at 14:50
  • With my implementation and edits, I now have the regular Serilog logging doing its regular thing, and my 2nd special case logging doing things to it's own files (this could be useful for example if you needed admin activities, or creation activities going to it's own file, etc.) – drewid Dec 16 '21 at 14:58
7

Serilog.Sinks.Map does this, and includes a file logging example:

Log.Logger = new LoggerConfiguration()
    .WriteTo.Map("EventId", "Other", (name, wt) => wt.File($"./logs/log-{name}.txt"))
    .CreateLogger();
Nicholas Blumhardt
  • 30,271
  • 4
  • 90
  • 101
4

You can definitely do that.

  1. You need to import package Serilog.Sinks.File

  2. Then you have to configure Serilog.

    In program.cs do following thing.

    Log.Logger = new LoggerConfiguration()
            .MinimumLevel.Debug()
            .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
            .Enrich.FromLogContext()
        .WriteTo.File(
            @"<<your log file path>>",
        fileSizeLimitBytes: 10000000,
        rollOnFileSizeLimit: true,
        shared: true,
        flushToDiskInterval: TimeSpan.FromSeconds(1))
            .CreateLogger();
    

In buildWebHost function add UseSerilog().

public static IWebHost BuildWebHost(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
    .UseStartup<Startup>()
        .UseSerilog() // <-- Add this line
        .Build();

Update 1

I have used EventId property. This is just demo that how you can use different file based on eventId but for your requirement you have to implement additional thing your own.

Program.cs

public class Program
    {
        public static void Main(string[] args)
        {
            Log.Logger = new LoggerConfiguration()
                .WriteTo.Logger(cc => cc.Filter.ByIncludingOnly(WithProperty("EventId",1001)).WriteTo.File("Test1001.txt",flushToDiskInterval: TimeSpan.FromSeconds(1)))
                .WriteTo.Logger(cc => cc.Filter.ByIncludingOnly(WithProperty("EventId", 2001)).WriteTo.File("Test2001.txt", flushToDiskInterval: TimeSpan.FromSeconds(1)))
                .CreateLogger();

            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args).UseSerilog()
                .UseStartup<Startup>();

        public static Func<LogEvent, bool> WithProperty(string propertyName, object scalarValue)
        {
            if (propertyName == null) throw new ArgumentNullException("propertyName");           
            ScalarValue scalar = new ScalarValue(scalarValue);
            return e=>
            {
                LogEventPropertyValue propertyValue;
                if (e.Properties.TryGetValue(propertyName, out propertyValue))
                {
                    var stValue = propertyValue as StructureValue;
                    if (stValue != null)
                    {
                        var value = stValue.Properties.Where(cc => cc.Name == "Id").FirstOrDefault();
                        bool result = scalar.Equals(value.Value);
                        return result;
                    }
                }
                return false;
            };
        }
    }

My HomeController.cs

  public class HomeController : Controller
    {
        ILogger<HomeController> logger;
        public HomeController(ILogger<HomeController> logger)
        {
            this.logger = logger;
        }
        public IActionResult Index()
        {
            logger.Log(LogLevel.Information,new EventId(1001), "This is test 1");
            logger.Log(LogLevel.Information, new EventId(2001), "This is test 2");
            return View();
        } 
    }

Note: Main thing is that you have to use some type of filter.

dotnetstep
  • 17,065
  • 5
  • 54
  • 72
  • Thanks. Yes I do this already, but it only writes to one log file. This is the standard approach. My use case is different. – lonix Jul 06 '18 at 16:02
  • @lonix You know you can create a new Logger instance with its own configuration instead of using the static logger? – mason Jul 06 '18 at 16:26
  • Yes but like my question says, how would I use it in the app? The standard logger is static but can also be injected, but what about another one? Where would be a good place to create it, how should it be stored, etc. in the container. There are no explanations about that use case anywhere that I can find. – lonix Jul 06 '18 at 16:30
  • Please see my update 1. I have provide sample that is near to your requirement. – dotnetstep Jul 07 '18 at 05:41
  • That's cool, thanks. Only problem is you need to include those properties in every logging call. Which is ugly and messy. But if this is the only way to do it, then it's good enough. Thanks again. – lonix Jul 09 '18 at 07:41