Easy log per asp.net core: Serilog

E’ inutile spiegare perchè loggare sia un punto chiave nello sviluppo di un’applicazione. E’ altrettanto inutile dire quanto oggi sia inutile sviluppare un framework custom che lo faccia: ci sono mille plugin che lo fanno (e spesso anche molto bene) per cui c’è davvero l’imbarazzo della scelta. Non tutti però sono semplici da configurare (alcuni sono un vero incubo). La mia scelta dopo vari tentativi è ricaduta su SeriLog. Sul web trovate parecchia documentazione in merito ve ne suggerisco un paio sotto.

Nello specifico queste sono le azioni che ho condotto per installarlo e configurarlo:

  • Scaricare con NuGet Packages Manager il pacchetto Serilog.AspNetCore
  • Ho inizializzato il Logger all’interno del Program.cs
var logger = new LoggerConfiguration()
    .ReadFrom.Configuration(builder.Configuration)
    .Enrich.FromLogContext()
    .CreateLogger();

builder.Logging.ClearProviders();
builder.Logging.AddSerilog(logger);
  • Ho aggiunto nel file appsettings.js le configurazioni di scrittura, tra cui il nome del file dov’è posizionato etc…
"Serilog": {
  "Using": [ "Serilog.Sinks.File" ],
  "MinimumLevel": {
    "Default": "Information"
  },
  "WriteTo": [
    {
      "Name": "File",
      "Args": {
        "path": "../APIlogs/webapi-WebAPICoreAuthBearer.log",
        "rollingInterval": "Day",
        "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {CorrelationId} {Level:u3}] {Username} {Message:lj}{NewLine}{Exception}"
      }
    }
  ]
}

Con queste semplici azioni il vostro sistema già loggerà in automatico. Qualora vi servisse esplicitamente loggare all’interno dei vostri controller nel caso di un API naturalmente basta utilizzare la solita modalità “iniettiva”.

 private readonly ILogger<InfoAPIController> _logger;
 private readonly IConfiguration _configuration;

 public InfoAPIController(ILogger<InfoAPIController> logger, IConfiguration iConfig)
 {
     _logger = logger;
     _configuration = iConfig;
 }

 [HttpGet(Name = "GetInfo")] ]
 public InfoAPI Get()
 {
     _logger.Log(LogLevel.Information, "GetInfo");
....

[1] https://www.claudiobernasconi.ch/2022/01/28/how-to-use-serilog-in-asp-net-core-web-api/

[2] https://www.youtube.com/watch?v=QjO2Jac1uQw

[3] https://www.milanjovanovic.tech/blog/5-serilog-best-practices-for-better-structured-logging

Aggiungere i Log in una Web API

Una delle cose fondamentali che serve per debuggare un applicazione sono i Logs. Avere un sistemi di log efficiente accorcia le tempistiche e favorisce un troubleshooting benfatto. In questo post mostro, brevemente, cosa si deve fare per utilizzare NLog a tal fine. Non voglio essere troppo noioso analizzando tutte le varie casistiche (nel caso vi consiglio questa lettura [1]) ma, voglio arrivare dritto al punto. Quello che a me serve è qualcosa che ad ogni eccezione venga correttamente loggata indipendentemente dal fatto che sia gestita e scriva in un file tutto quello che è successo.

A questo scopo installiamo i seguenti package NuGet:

Install-Package NLog.Web.AspNetCore
Install-Package NLog

Ed inseriremo nella root del progetto il seguente file config:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
	<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
		  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		  autoReload="true"
		  internalLogLevel="Info"
		  internalLogFile="${basedir}\internal-nlog.txt">
		<!-- enable asp.net core layout renderers -->
		<extensions>
			<add assembly="NLog.Web.AspNetCore"/>
		</extensions>
		<variable name="basedir" value="${aspnet-appbasepath}\wwwroot\logs" />
		<targets>
			<target xsi:type="AsyncWrapper" name="AllAsyncWrapper" queueLimit="10000" batchSize="1000">
				<target xsi:type="File"
						name="allfile"
						fileName="${var:basedir}\nlog-all-${shortdate}-${environment:ASPNETCORE_ENVIRONMENT}.log"
						archiveFileName="${var:basedir}\archives\nlog-all-${shortdate}-${environment:ASPNETCORE_ENVIRONMENT}.archive-{#}.zip"
						archiveEvery="Day"
						maxArchiveDays="7"
						archiveNumbering="DateAndSequence"
						enableArchiveFileCompression="True"
						layout="${longdate}|${aspnet-traceidentifier}|${uppercase:${level}}|${threadid}|${logger}|${message} ${exception:format=ToString}|${aspnet-request-method}|${aspnet-request-url}|${aspnet-mvc-action}|${aspnet-request-posted-body}" />
			</target>
		</targets>
		<!-- rules to map from logger name to target -->
		<rules>
			<logger name="*" minlevel="Error" writeTo="AllAsyncWrapper" />
		</rules>
	</nlog>
</configuration>

Questo file fornisce le indicazioni su come comporre il file, dove metterlo come mantenerlo… Come detto non mi dilungo troppo ma vi pongo l’accento su un paio di punti:

<variable name="basedir" value="${aspnet-appbasepath}\wwwroot\logs" />

Questa riga sopra la utilizzo per definire come cartella dove salvare i files una cartella della www root, comoda se siete in una farm dove non avete controllo completo del file system. Per le Web API invece io uso questa dato che non esiste una wwwroot:

<variable name="basedir" value="${aspnet-appbasepath}\logs" />

Questa parte invece definisce tutte le proprità del file di log: da cosa deve contenere ed in che formato, alla dimensione massima, al nome, alla rotation… Insomma tutto quello che serve per meglio definire come loggare. Non dimenticate di flaggare il Copy del file nell’output.

				<target xsi:type="File"
						name="allfile"
						fileName="${var:basedir}\nlog-all-${shortdate}-${environment:ASPNETCORE_ENVIRONMENT}.log"
						archiveFileName="${var:basedir}\archives\nlog-all-${shortdate}-${environment:ASPNETCORE_ENVIRONMENT}.archive-{#}.zip"
						archiveEvery="Day"
						maxArchiveDays="7"
						archiveNumbering="DateAndSequence"
						enableArchiveFileCompression="True"
						layout="${longdate}|${aspnet-traceidentifier}|${uppercase:${level}}|${threadid}|${logger}|${message} ${exception:format=ToString}|${aspnet-request-method}|${aspnet-request-url}|${aspnet-mvc-action}|${aspnet-request-posted-body}" />

Fatto questo resta un unico punto: fare in modo che NLog venga correttamente lanciato nel Program.cs

	public class Program
	{
		public static void Main(string[] args)
		{
            var logger = NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
            try
            {
                logger.Debug("init main");
                CreateWebHostBuilder(args).Build().Run();
                //webhost.RunAsync();
            }
            catch (Exception exception)
            {
                //NLog: catch setup errors
                logger.Error(exception, "Stopped program because of exception");
                throw;
            }
            finally
            {
                // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux)
                NLog.LogManager.Shutdown();
            }

        }

        public static IHostBuilder CreateWebHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        })
        .ConfigureLogging(logging =>
        {
            logging.ClearProviders();
            logging.SetMinimumLevel(LogLevel.Trace);
        })
            .UseNLog();
    }

Un piccola nota: il codice del Main è strutturato per loggare l’errore nel caso l’inizializzazione non vada a buon fine.

A questo punto il più è fatto: lanciando l’applicazione è possibile trovare il logs nella cartella specificata:

File di log

Naturalmente è possibile utilizzare il logger in manier customizzata semplicemente iniettandolo all’interno del Controller e richiamandolo a piacere:

	public class HomeController : Controller
	{
		private readonly ILogger<HomeController> _logger;

		public HomeController(ILogger<HomeController> logger)
		{
			_logger = logger;
		}

		public IActionResult Index()
		{
			_logger.LogInformation("Home");
			return View();
		}

		public IActionResult Privacy()
		{
			return View();
		}

	}

[1] https://programmingcsharp.com/nlog-net-core/