25 novembre 2021 19:38
Usare Seq e Serilog in una applicazione Blazor (o ASP.NET Core in generale)
UPDATED: aggiornata configurazione per .NET 6
Chiunque sviluppi anche una banale web-application ai giorni nostri, sa che è fondamentale loggare sia durante lo sviluppo ma soprattutto in produzione errori, warning ed eventualmente informazioni utili al debug.
La piattaforma ASP.NET da sempre incorpora le interfacce per iniettare nelle nostre “pagine” (che poi siano controller MVC, metodi rest di una webapi o SPA pages Blazor è indifferente) un ILogger che sarà poi nostro compito adattare alle nostre esigenze (cosa, dove e come loggare).
Perché allora molti, come il sottoscritto, ci abbinano due componenti esterni come Serilog e Seq? Vediamo in brevissimo le mie motivazione e soprattutto i passi necessari per quando si inizia un nuovo progetto
Seq
Ok loggare, ma dove? File ascii csv/txt, su una tabella del database di produzione, su un db separato, su un servizio remoto? E poi come fare per interrogare le migliaia e migliaia di righe? Mi tocca farmi una GUI ?
Seq risponde a tutte queste domande in maniera estremamente elegante e facile da deployare. Altro non è che un “ricevitore” di log con un suo db e una web-ui con qui fare query, filtri, statistiche, report, ecc.
Può essere installato sulla macchina di sviluppo o su un server di produzione (lo stesso webserver della nostra app o uno separato) sia come applicativo stand-alone (servizio windows) sia come container Docker. Ecco la più grande figata, soprattutto per le workstation dev è proprio l’estrema rapidità di mettere su il servizio in pochissimo tempo e quando non serve di tenerlo spento (come tutti i container docker, direte voi…).
Concettualmente può essere usato direttamente con l’ambiente asp.net essendo un provider del succitato Microsoft.Extensions.Logging, ed infatti all’inizio anche io lo usavo in questa forma, ma come loro stessi consigliano sulla loro esauriente documentazione, la sinergia con Serilog è la cosa giusta da usare per i benifici descritti qui sotto.
Serilog
Serilog altro non è che una libreria .NET che si occupa di logging che ha dei grossi pilastri su cui si basa:
- Può loggare –come dicono pomposamente loro- praticamente everywhere tramite i suoi Sink, ovvero dei connettori verso, file, database, cloud providers, protocolli, ecc. Se ne trovano veramente molti e uno di questi ovviamente è quello dedicato a Seq
- Può scrivere i messaggi di logging sotto forma di “template” come ad esempio
_logger.LogInformation("User {UserName} logged in.", Input.Email);
Questo consentirà al motore di Seq di andare a fare dei filtri tramite il suo metalinguaggio tramite le nostre “proprietà” che abbiamo loggato e quindi ad esempio di estrarre tutti i log che riguardano UserName = “myUsername”
- Può avvantaggiarsi dei suoi “Enrichers” ovvero andare a loggare per ogni evento informazioni come ThreadId, Nome del server, Nome della nostra applicazione, ecc.
- Può configurare i Minimum Levels in modo differente per sink, quindi ad esempio su Seq potrei voler loggare solo i miei custom events e solo le eccezioni (errors) che vengono dai namespace microsoft.* o system.* mentre su un altro media potrei fare viceversa
- Può essere estremamente selettivo su cosa e come loggare utilizzando i suoi Filters e Sub-loggers per un controllo veramente granulare
Configurazione di una Blazor Server Side App
Questi sono i passi necessari che io personalmente utilizzo per configurare le mie Blazor app o i miei servizi API (si presume che seq sia già installato in ascolto sulla porta standard 5341). Ovviamente non è da prendere alla lettera ma di adattare minimum levels e enrichers al vostro caso.
Package necessari
Installare tramite Nuget Package Manager o direttamente dal csproj questi packages (le versioni sono ovviamente le ultime alla data di questo post). Come vedete NON serve installare il package di Datalust Seq.Extension.Logging perché se ne occupa direttamente il sink di Serilog (servirebbe nel caso volessimo usare solo Seq e il motore asp.net)
<ItemGroup>
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="2.2.0" />
<PackageReference Include="Serilog.Enrichers.Process" Version="2.0.2" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.3.0" />
<PackageReference Include="Serilog.Sinks.Seq" Version="5.1.0" />
</ItemGroup>
Configurazione in appsettings.json
Questa è la mia configurazione base, come detto va adattata ai vostri gusti e ovviamente può essere diversa tra sviluppo, staging, produzione
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning",
"Serilog": "Warning"
}
},
"Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ],
"Properties": {
"ApplicationName": "MyCompany.myApplicationName"
},
"WriteTo": [
{
"Name": "Seq",
"Args": {
"serverUrl": "http://myseqserver:5341",
"ApiKey": "myApikey"
}
}
]
}
Uso in .NET5 e 3.1
Program.cs
Nel metodo main si inizializza Serilog dicendo di leggere la configurazione dal file appsettings e poi ne approfittiamo per loggare da subito un eventuale crash dell’appdomain (che di solito butta fuori un generico 500 difficile da diagnosticare)
Importante non dimenticarsi lo .UseSerilog() nella CreateDefaultBuilder() oltre ovviamente ad un using Serilog; che però ormai VS2019 nelle ultime versioni è così intelligente da mettere da solo.
public static void Main(string[] args)
{
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.CreateLogger();
try
{
Log.Information("Application RizIntranet started");
CreateHostBuilder(args).Build().Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application RizIntranet failed to start");
}
finally
{
Log.CloseAndFlush();
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup();
});
Startup.cs
Nel metodo Configure() solitamente in questa posizione utilizzare il middleware in oggetto. Questo fa si che la verbosità del motore di logging di asp.net venga ridotta a un unico evento corredato di N parametri (vedi qui per un confronto più chiaro)
app.UseStaticFiles();
app.UseSerilogRequestLogging();
app.UseRouting();
Uso in .NET6
Program.cs
Come sappiamo in .NET il fiel startup.cs non esiste piú e tutto è centralizzato nel program.cs che ora assume questa forma (ho anche gestito meglio il nome dell'applicazione prendendola dal appsettings)
using Serilog;
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.CreateLogger();
var section = configuration.GetSection("Serilog:Properties:ApplicationName");
var applicationName = section != null ? section.Value : "unknow";
try
{
Log.Information($"Application {applicationName} started");
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog((ctx, lc) => lc
.ReadFrom.Configuration(configuration));
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, $"Application {applicationName} failed to start");
}
finally
{
Log.Information("Shut down complete");
Log.CloseAndFlush();
}
Utilizzo in un Razor controller
Siamo arrivati alla fine della configurazione. Già così se la nostra app crasha per qualche motivo, non siamo costretti ad andare a vedere nella development console del browser il motivo dell’errore, ma ce lo ritroveremo in Seq. Se poi vogliamo loggare eventi custom non ci resta che “injectare” il componente ILogger<MyPage> nella pagina ed usare i vari metodi LogInformation, LogWarning, LogError, ecc.
Io uso esclusivamente il code-behind ma la stessa cosa può essere fatta con le direttive @inject e la sezione @code
public class ReviewListBase : ComponentList
{
[Inject] public ILogger<ReviewListBase> Logger { get; set; }
protected override async Task OnInitializedAsync()
{
await BindGrid();
Logger.LogInformation("Reviews read from db");
}
public async Task OnRowDeleting(Review dataItem)
{
...
await ReviewService.DeleteAsync(dataItem);
Logger.LogWarning("Review deleted");
}
...
}