DEV Community

Cover image for Serilog logging and EF Core logging
Karen Payne
Karen Payne

Posted on

Serilog logging and EF Core logging

Overview

One of the most important tools a developer has for debugging code is to write logs to a log file and optionally write to the console. For logging while writing code a developer starting out with logging may elect to use one log file and over time can become difficult to get to current entries.

The goal of this article is to provide methods to log to a file for non-data related details and a file for logging data related details.

Note
All code has been written in Razor Pages using Entity Framework Core 7 in Microsoft Visual Studio 2022. Should be easy to use the code in an ASP.NET Core project.

NuGet packages used

  • Serilog.AspNetCore - 6.1.0
  • Serilog.Extensions.Logging.File - 3.0.0
  • Serilog.Sinks.Console - 4.1.0
  • Serilog.Sinks.File - 5.0.0
  • SeriLogThemesLibrary - 1.0.0.1
  • Microsoft.EntityFrameworkCore.SqlServer - 7.0.2

Log to

Each day a new folder is created under the root of the project with the date LogFiles\yyyy-mm-dd. Beneath this folder a file named log.txt for non-data details.txt and EF_Log.txt for EF Core logging.

Log class for non-data information

  • Development writes to a folder under debug\bin
  • Production write to the same location as Development but for a real production environment set a path to where there is a central location for logs and provide a different unique name.
public class SetupLogging
{
    public static void Development()
    {

        Log.Logger = new LoggerConfiguration()
            .MinimumLevel.Verbose()
            .WriteTo.Console(theme: SeriLogCustomThemes.Theme1())
            .WriteTo.File(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "LogFiles", $"{Now.Year}-{Now.Month}-{Now.Day}", "Log.txt"),
                rollingInterval: RollingInterval.Infinite,
                outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level}] {Message}{NewLine}{Exception}")
            .CreateLogger();
    }

    public static void Production()
    {
        Log.Logger = new LoggerConfiguration()
            .WriteTo.File(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "LogFiles", "Log.txt"),
                rollingInterval: RollingInterval.Day)
            .CreateBootstrapLogger();
    }
}
Enter fullscreen mode Exit fullscreen mode

In Program.cs use the above methods dependent on environments.

if (builder.Environment.IsDevelopment())
{
    SetupLogging.Development();
}
else
{
    SetupLogging.Production();
}
Enter fullscreen mode Exit fullscreen mode

EF Core logging

Serilog will generate folders for logging as needed but not for EF Core so to ensure the log folder exists we add the following to the project file.

<Target Name="MakeMyDir" AfterTargets="Build">
    <MakeDir Directories="$(OutDir)LogFiles" />
</Target>
Enter fullscreen mode Exit fullscreen mode

Next, the following class is responsible for EF Core logging.

public class DbContextToFileLogger
{
    /// <summary>
    /// Log file name
    /// </summary>
    private readonly string _fileName = 
        Path.Combine(AppDomain.CurrentDomain.BaseDirectory, 
            "LogFiles", $"{Now.Year}-{Now.Month}-{Now.Day}", $"EF_Log.txt");

    /// <summary>
    /// Use to override log file name and path
    /// </summary>
    /// <param name="fileName"></param>
    public DbContextToFileLogger(string fileName)
    {
        _fileName = fileName;
    }

    /// <summary>
    /// Setup to use default file name for logging
    /// </summary>
    public DbContextToFileLogger()
    {

    }
    /// <summary>
    /// append message to the existing stream
    /// </summary>
    /// <param name="message"></param>
    [DebuggerStepThrough]
    public void Log(string message)
    {

        if (!File.Exists(_fileName))
        {
            File.CreateText(_fileName).Close();
        }

        StreamWriter streamWriter = new(_fileName, true);

        streamWriter.WriteLine(message);

        streamWriter.WriteLine(new string('-', 40));

        streamWriter.Flush();
        streamWriter.Close();
    }
}
Enter fullscreen mode Exit fullscreen mode

Configuration is done in Program.cs

if (builder.Environment.IsDevelopment())
{

    SetupLogging.Development();
    builder.Services.SensitiveDataLoggingConnection(builder);
}
else
{
    SetupLogging.Production();
    builder.Services.ProductionLoggingConnection(builder);
}
Enter fullscreen mode Exit fullscreen mode

Using the following class

  • As coded the connection string is read from appsettings
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=MockupApplication;Integrated Security=True"
  }
}
Enter fullscreen mode Exit fullscreen mode
  • There are several different configurations which a developer can modified to suit their needs. Take to to study these methods rather than copy-n-paste and use.

Peek ahead

In the next article on Serilog we will look a conditional logging, how to read from appsettings.json to decide on to log or not to log.

public static class DbContexts
{
    /// <summary>
    /// Test connection with exception handling
    /// </summary>
    /// <param name="context"><see cref="DbContext"/></param>
    /// <param name="ct">Provides a shorter time out from 30 seconds to in this case one second</param>
    /// <returns>true if database is accessible</returns>
    /// <remarks>
    /// Running asynchronous as synchronous.
    /// </remarks>
    public static bool CanConnectAsync(this DbContext context, CancellationToken ct)
    {
        try
        {
            return context.Database.CanConnectAsync(ct).Result;

        }
        catch
        {
            return false; 
        }
    }

    /// <summary>
    /// Enable sensitive logging for EF Core
    /// </summary>
    public static void SensitiveDataLoggingConnection(this IServiceCollection collection, WebApplicationBuilder builder)
    {

        collection.AddDbContextPool<Context>(options =>
            options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))
                .EnableSensitiveDataLogging()
                .LogTo(new DbContextToFileLogger().Log));
    }

    /// <summary>
    /// Single line logging with sensitive data enabled for EF Core
    /// </summary>
    public static void SingleLineSensitiveDataLoggingConnection(this IServiceCollection collection, WebApplicationBuilder builder)
    {

        collection.AddDbContextPool<Context>(options =>
            options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))
                .EnableSensitiveDataLogging().LogTo(
                    new DbContextToFileLogger().Log,
                    LogLevel.Debug,
                    DbContextLoggerOptions.DefaultWithLocalTime | DbContextLoggerOptions.SingleLine));

    }
    /// <summary>
    /// Production logging for EF Core
    /// </summary>
    /// <param name="collection"></param>
    public static void ProductionLoggingConnection(this IServiceCollection collection, WebApplicationBuilder builder)
    {

        collection.AddDbContextPool<Context>(options =>
            options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))
                .LogTo(
                    new DbContextToFileLogger().Log));

    }
}
Enter fullscreen mode Exit fullscreen mode

Trying the code out with an exception

Click the button on the Index page to invoke an exception which uses poor programming

public void OnPostInvokeException(int id)
{
    Log.Information("OnGet - before throwing exception with {P1}", id);
    try
    {
        // there is no record with this id
        var user = _context.UserLogin.First(x => x.Id == id);
    }
    catch (Exception e)
    {
        Log.Error(e, "");
    }

    Log.Information("OnGet - after throwing exception");
}
Enter fullscreen mode Exit fullscreen mode

Use this instead of the above.

public void OnPostInvokeException(int id)
{
    var user = _context.UserLogin.FirstOrDefault(x => x.Id == id);
    if (user == null)
    {
        Log.Information("No user with an id of {P1}", id);
    }
    else
    {
        Log.Information("Found user with email address {P1}", user.EmailAddress);
    }

}
Enter fullscreen mode Exit fullscreen mode

Summary

The code presented provides easy methods to split regular logging from EF Core logging in folders for the current day. Take time to read about configurations.

Source code

Clone the following GitHub repository.

Top comments (0)