Logging Configuration

📆 Date: 2023-12-06

Rationale

Provide more granular logging options, especially useful when you need to troubleshoot your server setup.

Affected Products

All the backend API projects.

Upgrade Path

More granular options have been added to the API. To include them in your project, follow these steps:

(1) add package Serilog.Sinks.Postgresql.Alternative if you want to include also the option to log into a PostgreSQL database.

(2) add these entries to your appsettings.json, enabling and disabling the various targets at will:

  "Auditing": {
    "File": true,
    "Mongo": true,
    "Postgres": true,
    "Console": true
  },

In the same file, change Log into MongoLog in ConnectionStrings; and add PostgresLog if you want PostgreSQL output (replace PRJ with your short project name):

  "ConnectionStrings": {
    ...
    "MongoLog": "mongodb://localhost:27017/cadmus-PRJ-log",
    "PostgresLog": "Server=localhost;Database=cadmus-PRJ-log;User Id=postgres;Password=postgres;Include Error Detail=True"
  },

Finally, ensure that your Serilog entry in this file has entries in Using (remove the ones you do not use):

"Using": [
    "Serilog.Sinks.Console",
    "Serilog.Sinks.File",
    "Serilog.Sinks.MongoDB",
    "Serilog.Sinks.Postgresql.Alternative"
],

(3) in Program.cs add this code:

private static bool IsAuditEnabledFor(IConfiguration config, string key)
{
    bool? value = config.GetValue<bool?>($"Auditing:{key}");
    return value != null && value != false;
}

private static void ConfigurePostgreLogging(HostBuilderContext context,
    LoggerConfiguration loggerConfiguration)
{
    string? cs = context.Configuration.GetConnectionString("PostgresLog");
    if (string.IsNullOrEmpty(cs))
    {
        Console.WriteLine("Postgres log connection string not found");
        return;
    }

    Regex dbRegex = new("Database=(?<n>[^;]+);?");
    Match m = dbRegex.Match(cs);
    if (!m.Success)
    {
        Console.WriteLine("Postgres log connection string not valid");
        return;
    }
    string cst = dbRegex.Replace(cs, "Database={0};");
    string dbName = m.Groups["n"].Value;
    PgSqlDbManager mgr = new(cst);
    if (!mgr.Exists(dbName))
    {
        Console.WriteLine($"Creating log database {dbName}...");
        mgr.CreateDatabase(dbName, "", null);
    }

    IDictionary<string, ColumnWriterBase> columnWriters =
        new Dictionary<string, ColumnWriterBase>
    {
        { "message", new RenderedMessageColumnWriter(
            NpgsqlDbType.Text) },
        { "message_template", new MessageTemplateColumnWriter(
            NpgsqlDbType.Text) },
        { "level", new LevelColumnWriter(
            true, NpgsqlDbType.Varchar) },
        { "raise_date", new TimestampColumnWriter(
            NpgsqlDbType.TimestampTz) },
        { "exception", new ExceptionColumnWriter(
            NpgsqlDbType.Text) },
        { "properties", new LogEventSerializedColumnWriter(
            NpgsqlDbType.Jsonb) },
        { "props_test", new PropertiesColumnWriter(
            NpgsqlDbType.Jsonb) },
        { "machine_name", new SinglePropertyColumnWriter(
            "MachineName", PropertyWriteMethod.ToString,
            NpgsqlDbType.Text, "l") }
    };

    loggerConfiguration
        .WriteTo.PostgreSQL(cs, "log", columnWriters,
        needAutoCreateTable: true, needAutoCreateSchema: true);
}

(4) also, in your Program.cs Main method, replace the host creation with this:

var host = await CreateHostBuilder(args)
    .UseSerilog((hostingContext, loggerConfiguration) =>
    {
        var maxSize = hostingContext.Configuration["Serilog:MaxMbSize"];

        loggerConfiguration.ReadFrom.Configuration(
            hostingContext.Configuration);

        if (IsAuditEnabledFor(hostingContext.Configuration, "File"))
        {
            Console.WriteLine("Logging to file enabled");
            loggerConfiguration.WriteTo.File("cadmus-log.txt",
                rollingInterval: RollingInterval.Day);
        }

        if (IsAuditEnabledFor(hostingContext.Configuration, "Mongo"))
        {
            Console.WriteLine("Logging to Mongo enabled");
            string? cs = hostingContext.Configuration
                .GetConnectionString("MongoLog");

            if (!string.IsNullOrEmpty(cs))
            {
                loggerConfiguration.WriteTo.MongoDBCapped(cs,
                    cappedMaxSizeMb: !string.IsNullOrEmpty(maxSize) &&
                    int.TryParse(maxSize, out int n) && n > 0 ? n : 10);
            }
            else
            {
                Console.WriteLine("Mongo log connection string not found");
            }
        }

        if (IsAuditEnabledFor(hostingContext.Configuration, "Postgres"))
        {
            Console.WriteLine("Logging to Postgres enabled");
            ConfigurePostgreLogging(hostingContext, loggerConfiguration);
        }

        if (IsAuditEnabledFor(hostingContext.Configuration, "Console"))
        {
            Console.WriteLine("Logging to console enabled");
            loggerConfiguration.WriteTo.Console();
        }
    })
    .Build()
    .SeedAsync(); // see Services/HostSeedExtension