blazor_radzen_dotnet8

Blazor Radzen .NET 8 Serilog Logging to Console, File and Database

Logging: Logging is the process of recording events, messages, or exceptions that occur during the execution of an application. It helps developers understand the behavior of the application, diagnose issues, and track its performance. Logging is an essential aspect of software development and plays a crucial role in maintaining and troubleshooting applications.

serilog_logo

Serilog: Serilog is a popular logging library for .NET applications. It provides a flexible and extensible logging framework that allows developers to capture and store log events for debugging, monitoring, and analysis purposes. Serilog supports various logging sinks, including console logging, file logging, and database logging.

Console Logging: Console logging is a type of logging where log messages are displayed in the console window. It is useful during development and debugging.

File Logging: File logging is a type of logging where log messages are written to a file. It helps in storing log data for future analysis and troubleshooting.

SQLite Database Logging: SQLite database logging is a type of logging where log messages are stored in an SQLite database. It provides a structured way to store and query log data.

Log.cs

The provided code defines a Log model class within the BlazorAppSerilogLogging.Models namespace. The Log class has the following properties:

Log.cs

namespace BlazorAppSerilogLogging.Models;

public class Log
{
    public int id { get; set; }
    public DateTime Timestamp { get; set; }
    public string Level { get; set; } = string.Empty;
    public string Exception { get; set; } = string.Empty;
    public string RenderedMessage { get; set; } = string.Empty;
    public string Properties { get; set; } = string.Empty;
}

id: An integer property that represents the unique identifier of the log entry.

Timestamp: A DateTime property that stores the timestamp when the log entry was created.

Level: A string property that indicates the log level of the entry (e.g., Information, Warning, Error).

Exception: A string property that holds the exception details, if any, associated with the log entry.

RenderedMessage: A string property that contains the formatted log message.

Properties: A string property that stores additional properties or metadata related to the log entry.

ApplicationLoggerDbContext.cs

The ApplicationLoggerDbContext class is a C# code that represents the database context for logging in a Blazor application using Serilog. It is responsible for managing the connection to the database and providing access to the Logs table.

ApplicationLoggerDbContext.cs

using BlazorAppRadzenNet8SerilogLogging.Models;
using Microsoft.EntityFrameworkCore;

namespace BlazorAppRadzenNet8SerilogLogging.Data;

public class ApplicationLoggerDbContext : DbContext
{
    public ApplicationLoggerDbContext(DbContextOptions<ApplicationLoggerDbContext> options)
    : base(options)
    {
    }

    public DbSet<Log> Logs => Set<Log>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
    }
}

The ApplicationLoggerDbContext class is defined within the BlazorAppSerilogLogging.Data namespace. It extends the DbContext class provided by Entity Framework Core.

The class has a constructor that takes an instance of DbContextOptions<ApplicationLoggerDbContext> as a parameter. This allows the class to configure the database connection options.

The class also defines a property called Logs of type DbSet<Log>. This property represents the Logs table in the database and allows you to query and manipulate log data.

The OnModelCreating method is overridden but left empty in this code example. This method is used to configure the database model and define relationships between entities.

LoggerService.cs

The LoggerService class is defined within the BlazorAppSerilogLogging.Data namespace. It implements several methods for logging operations and interacts with the ApplicationLoggerDbContext class.

LoggerService.cs

using BlazorAppRadzenNet8SerilogLogging.Models;
using Microsoft.EntityFrameworkCore;
using Radzen;
using System.Linq.Dynamic.Core;

namespace BlazorAppRadzenNet8SerilogLogging.Data;

public class LoggerService
{
    private readonly ILogger<LoggerService> _logger;
    private readonly ApplicationLoggerDbContext _loggerDbContext;

    public LoggerService(ILogger<LoggerService> logger, ApplicationLoggerDbContext loggerDbContext)
    {
        _logger = logger;
        _loggerDbContext = loggerDbContext;
    }

    public async Task<Log?> GetLogByIdAsync(int id)
    {
        _logger.LogInformation($"Called GetLogByIdAsync", id);
        return await _loggerDbContext.Logs.FirstOrDefaultAsync(x => x.id == id);
    }

    public async Task<(IEnumerable<Log> Result, int TotalCount)> GetLogsAsync(string? filter = default, int? top = default, int? skip = default, string? orderby = default, string? expand = default, string? select = default, bool? count = default)
    {
        _logger.LogInformation($"Called GetLogsAsync");

        var query = _loggerDbContext.Logs.AsQueryable();

        if (!string.IsNullOrEmpty(filter))
            query = query.Where(filter);

        if (!string.IsNullOrEmpty(orderby))
            query = query.OrderBy(orderby);

        int totalCount = 0;
        if (count == true)
            totalCount = query.Count();

        IEnumerable<Log>? result;
        if (skip == null || top == null)
            result = await query.ToListAsync();
        else
            result = await query.Skip(skip.Value).Take(top.Value).ToListAsync();

        return (result, totalCount);
    }

    public async Task<bool> DeleteLogByIdAsync(int id)
    {
        _logger.LogInformation($"Called DeleteLogByIdAsync", id);

        var log = await _loggerDbContext.Logs.FirstOrDefaultAsync(x => x.id == id);
        if (log == null)
            return false;

        _loggerDbContext.Logs.Remove(log);
        await _loggerDbContext.SaveChangesAsync();

        return true;
    }

    public async Task<bool?> DeleteAllLogsAsync()
    {
        _logger.LogInformation($"Called DeleteAllLogsAsync");
        var all = await _loggerDbContext.Logs.ToListAsync();
        _loggerDbContext.Logs.RemoveRange(all); ;
        await _loggerDbContext.SaveChangesAsync();
        _logger.LogInformation($"Deleted All Logs.");
        return true;
    }
}

The class has the following members:

_logger: An instance of the ILogger<LoggerService> interface, which is used for logging messages.

_loggerDbContext: An instance of the ApplicationLoggerDbContext class, which represents the database context for logging.

The constructor of the LoggerService class takes in an ILogger<LoggerService> instance and an ApplicationLoggerDbContext instance as parameters. These dependencies are injected into the class using dependency injection.

Index.razor

Index.razor is a Blazor component that displays logs retrieved from a logging service. It allows users to view and delete logs. The logs are fetched asynchronously and displayed in a table format.

Index.razor

@page "/Log"

@inject DialogService DialogService

<PageTitle>Logs</PageTitle>

<RadzenRow>

    <RadzenColumn SizeSM="12" SizeMD="12" SizeLG="4">

        <RadzenStack Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center">
            <RadzenText Text="Logs" TextStyle="TextStyle.H5" />
            <RadzenButton Text="DELETE ALL LOGS" Icon="delete_forever"
                          Click="DeleteAllLogs"
                          ButtonStyle="ButtonStyle.Danger" class="rz-mb-2 rz-p-2" />
        </RadzenStack>

    </RadzenColumn>

</RadzenRow>

<RadzenDataGrid KeyProperty="id" IsLoading="@isLoading" ShowPagingSummary=true
                Count="@totalCount" Data="@logs" LoadData="@LoadData"
                FilterPopupRenderMode="PopupRenderMode.OnDemand"
                FilterCaseSensitivity="FilterCaseSensitivity.CaseInsensitive"
                FilterMode="FilterMode.Advanced" AllowSorting="true" AllowFiltering="true"
                AllowPaging="true" PageSize="@itemPageSize" PagerHorizontalAlign="HorizontalAlign.Center"
                TItem="LogViewModel" ColumnWidth="200px">
    <Columns>
        <RadzenDataGridColumn TItem="LogViewModel" Property="id" Filterable="false" Title="Id" Frozen="true" Width="30px" MinWidth="30px" TextAlign="TextAlign.Center" />

        <RadzenDataGridColumn TItem="LogViewModel" Property="Timestamp" Title="Timestamp" />
        <RadzenDataGridColumn TItem="LogViewModel" Property="Level" Title="Level" Context="log">
            <Template>
                <span class="text-@Helpers.LogEventLevelHelper.GetBootstrapUIClass(log.Level)">
                    @log.Level
                </span>
            </Template>
        </RadzenDataGridColumn>

        <RadzenDataGridColumn TItem="LogViewModel" Property="Exception" Title="Exception" />
        <RadzenDataGridColumn TItem="LogViewModel" Property="RenderedMessage" Title="RenderedMessage" />
        <RadzenDataGridColumn TItem="LogViewModel" Property="Properties" Title="Properties" />

        <RadzenDataGridColumn TItem="LogViewModel" Context="log" Filterable="false" Sortable="false" Width="150px" TextAlign="TextAlign.Center">
            <Template Context="log">

                <RadzenRow JustifyContent="JustifyContent.Center">
                    <RadzenButton Icon="pageview" ButtonStyle="ButtonStyle.Info" Variant="Variant.Flat" Size="ButtonSize.Medium"
                                  Click="@(args => NavigatetoDetail(log.id))" @onclick:stopPropagation="true">
                    </RadzenButton>
                    <RadzenButton Icon="delete_forever" ButtonStyle="ButtonStyle.Danger" Variant="Variant.Flat" Size="ButtonSize.Medium"
                                  Click="@(args => NavigatetoDelete(log.id))" @onclick:stopPropagation="true">
                    </RadzenButton>
                </RadzenRow>

            </Template>
        </RadzenDataGridColumn>
    </Columns>
</RadzenDataGrid>


@code {

    const int itemPageSize = 10;
    private bool isLoading;
    private int totalCount;
    private IEnumerable<LogViewModel>? logs;

    private async Task LoadData(LoadDataArgs args)
    {
        isLoading = true;

        var result = await LoggerService.GetLogsAsync(filter: args.Filter, top: args.Top, skip: args.Skip, orderby: args.OrderBy, count: true);

        logs = Mapper.Map<IEnumerable<Log>, IEnumerable<LogViewModel>>(result.Result);
        totalCount = result.TotalCount;

        isLoading = false;
    }

    private async Task DeleteAllLogs()
    {
        var dialogResult = await DialogService.Confirm("Are you sure DELETE All Logs?", "Delete All Logs",
                    new ConfirmOptions { OkButtonText = "Ok", CancelButtonText = "Cancel" });

        if (dialogResult == true)
        {
            var deleteAllLogsResult = await LoggerService.DeleteAllLogsAsync();
            if (deleteAllLogsResult == true)
                NavigationManager.NavigateTo("/Log", true);
        }

    }

    private void NavigatetoDetail(int id) => NavigationManager.NavigateTo($"/Log/Detail/{id}");
    private void NavigatetoDelete(int id) => NavigationManager.NavigateTo($"/Log/Delete/{id}");

}

This code block is executed when the component is initialized. It calls the GetLogsAsync method of the LoggerService to fetch the logs asynchronously. The retrieved logs are then mapped to LogViewModel objects using AutoMapper. The logs variable is assigned the mapped logs.

Program.cs

Program.cs

builder.Host.UseSerilog((ctx, lc) => lc
	.MinimumLevel.Information()
	//.WriteTo.Console(new JsonFormatter(), restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Information)
	.WriteTo.Console(restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Information)
	.WriteTo.Seq("http://localhost:5001", restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Information)
	.WriteTo.File(serilogFileLoggerFilePath,
					restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Verbose,
					rollingInterval: RollingInterval.Hour,
					encoding: System.Text.Encoding.UTF8)
	.WriteTo.SQLite(sqliteDbFilePath,
					tableName: "Logs",
					restrictedToMinimumLevel:
						builder.Environment.IsDevelopment() ? Serilog.Events.LogEventLevel.Information : Serilog.Events.LogEventLevel.Warning,
					storeTimestampInUtc: false,
					batchSize:
						builder.Environment.IsDevelopment() ? (uint)1 : (uint)100,
					retentionPeriod: new TimeSpan(0, 1, 0, 0, 0),
					maxDatabaseSize: 10)
);

Retrieves the connection string for the SQLite logger from the configuration file and modifies it to include the current directory path.

Configures Serilog with various log sinks, including console logging, Seq logging, file logging, and SQLite database logging. It sets the minimum log level based on the application environment.

Registers the ApplicationLoggerDbContext and ApplicationDbContext services in the dependency injection container. It configures the ApplicationLoggerDbContext to use the SQLite logger connection string and the ApplicationDbContext to use an in-memory database.

We discussed the key concepts of logging, console logging, file logging, and SQLite database logging. We also examined the code structure and provided code examples to illustrate the configuration process. By understanding this code, developers can effectively set up logging in their Blazor applications using Serilog.

Source

Full source code is available at this repository in GitHub:
https://github.com/akifmt/DotNetCoding/tree/main/src/BlazorAppRadzenNet8SerilogLogging