Parte 3 — Serilog: logging structurat în fișier
Până în Partea 2, log-urile zboară doar în consolă. La oprirea aplicației se pierd. Pentru orice deploy real e nevoie de persistență: fișiere rolling pe zi, sau sink-uri către sisteme dedicate (Seq, Elasticsearch). Serilog e standardul de-facto pentru asta.
Pasul 15 — Pachetele (deja preinstalate)
În Lab10.csproj aveți deja:
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
Roluri:
| Pachet | Ce face |
|---|---|
Serilog.AspNetCore |
Integrarea cu host-ul ASP.NET Core (UseSerilog) + RequestLoggingMiddleware (opțional) |
Serilog.Sinks.Console |
Sink către stdout cu coloring și template configurabil |
Serilog.Sinks.File |
Sink către fișier, cu rolling pe zi/oră/dimensiune |
Pasul 16 — Configurare în Program.cs
În Lab10_start/Program.cs, înlocuiți TODO-ul de Serilog cu următoarele, înainte de // DbContext:
using Serilog;
var builder = WebApplication.CreateBuilder(args);
// Serilog
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.Enrich.FromLogContext()
.WriteTo.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.WriteTo.File(
path: "logs/newsportal-.txt",
rollingInterval: RollingInterval.Day,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
.CreateLogger();
builder.Host.UseSerilog();
// DbContext
// ... restul ramane neschimbat ...
Detalii:
logs/newsportal-.txt— Serilog adaugă data între-și.txt, deci fișierele vor filogs/newsportal-20260428.txt,logs/newsportal-20260429.txt, …{Properties:j}la file template — serializează toate proprietățile structurate (inclusiv{Method},{Path}, etc. din middleware-ul nostru) ca JSON. Asta e diferența: în consolă citiți log-ul, în fișier îl puteți interoga.Enrich.FromLogContext— necesar pentruLogContext.PushProperty(...)(nu îl folosim explicit, dar e bun să fie acolo).builder.Host.UseSerilog()— schimbă logger-ul global. ToateILogger<T>injectate (controller-e, service-uri, middleware-urile noastre) merg automat prin Serilog.
Pasul 17 — Adăugați logs/ în .gitignore
Fișierele de log nu sunt parte din cod. În .gitignore (la rădăcina repo-ului sau în Lab10_start/):
logs/
Pasul 18 — Structured logging — diferența care contează
Comparați cele două apeluri:
// DA structured logging - placeholder nominal
_logger.LogInformation("Article created: {ArticleId} by {UserId}", article.Id, userId);
// NU string interpolation - placeholder pierdut
_logger.LogInformation($"Article created: {article.Id} by {userId}");
Output în consolă, ambele variante arată la fel. Dar în fișier, varianta structurată produce:
2026-04-28 10:32:11.456 [INF] Article created: 42 by 7 {"ArticleId": 42, "UserId": 7}
Iar varianta interpolată produce doar mesajul, fără proprietăți. Beneficiul: la o investigație, puteți filtra fișierul după ArticleId=42 cu grep sau jq. La sink-uri către Seq / Elasticsearch, e ireductibil — căutarea după proprietăți e singura mod de scaling.
Pasul 19 — Adăugați logging structurat în service
Deschideți Services/ArticleService.cs și injectați ILogger<ArticleService>:
public class ArticleService : IArticleService
{
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<ArticleService> _logger;
public ArticleService(IUnitOfWork unitOfWork, ILogger<ArticleService> logger)
{
_unitOfWork = unitOfWork;
_logger = logger;
}
public async Task AddAsync(Article article, CancellationToken cancellationToken = default)
{
_logger.LogInformation("Creating article {Title} by author {AuthorId}",
article.Title, article.AuthorId);
article.PublishedAt = DateTime.Now;
await _unitOfWork.ArticleRepository.AddAsync(article, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
_logger.LogInformation("Article created with id {ArticleId}", article.Id);
}
public async Task UpdateAsync(Article article, CancellationToken cancellationToken = default)
{
_logger.LogInformation("Updating article {ArticleId} ({Title})", article.Id, article.Title);
_unitOfWork.ArticleRepository.Update(article);
await _unitOfWork.SaveChangesAsync(cancellationToken);
}
public async Task DeleteAsync(int id, CancellationToken cancellationToken = default)
{
_logger.LogInformation("Deleting article {ArticleId}", id);
var article = await _unitOfWork.ArticleRepository.GetByIdAsync(id, cancellationToken);
if (article != null)
{
_unitOfWork.ArticleRepository.Delete(article);
await _unitOfWork.SaveChangesAsync(cancellationToken);
}
}
}
Toate cele trei metode care modifică starea (AddAsync, UpdateAsync, DeleteAsync) primesc LogInformation cu placeholder-e nominale. Read-only metodele (GetAllAsync, GetByIdAsync, CountAsync, GetPagedAsync) nu necesită log — generează zgomot pentru fiecare request GET.
Atenție la testele Lab 9.
ArticleServiceTestsinstanțiazăArticleServicecu un singur parametru. Dacă adăugați al doilea parametru (ILogger<ArticleService> logger), testele nu mai compilează. Două soluții:
- Faceți logger-ul opțional:
ILogger<ArticleService>? logger = nullși folosiți_logger?.LogInformation(...)(nu rupe testele vechi).- Treceți un
NullLogger<ArticleService>.Instance(dinMicrosoft.Extensions.Logging.Abstractions) în testulCreateService.Recomandare: opțiunea 2 — testele rămân stricte, semnătura constructorului rămâne curată.
Pasul 20 — Verificare
Reporniți aplicația și faceți câteva request-uri (mai ales create-uri de articole). Verificați:
- În consolă vedeți log-urile cu format Serilog (
[10:32:11 INF] ...) - În folder-ul
logs/apare un fișiernewsportal-YYYYMMDD.txt - Conținutul fișierului include proprietățile structurate ca JSON la finalul fiecărei linii —
{"ArticleId": 42, "Method": "POST", ...}
# Pe Windows (PowerShell)
Get-Content -Tail 10 logs/newsportal-20260428.txt
Exercițiul 3 — Serilog cu sinks Console + File și structured logging (2p)
- [ ] Serilog configurat în
Program.cscu sinks Console + File rolling pe zi (Pasul 16) - [ ]
builder.Host.UseSerilog()apelat — logger-ul built-in înlocuit (Pasul 16) - [ ]
logs/în.gitignore(Pasul 17) - [ ]
ArticleServiceinjecteazăILogger<ArticleService>și folosește structured logging (placeholder-e nominale, nu interpolare) în toate cele 3 metode care modifică starea:AddAsync,UpdateAsync,DeleteAsync(Pasul 19) - [ ] Testele din
Lab10.Tests/continuă să compileze și să treacă (rezolvați conflictul cu constructor-ul service-ului prin una din cele două căi din Pasul 19) - [ ] Verificat manual: fișier de log creat, proprietăți structurate vizibile în el (Pasul 20)
Validare: după un POST /api/articlesapi cu un articol nou, în logs/newsportal-YYYYMMDD.txt găsiți o linie de forma:
2026-04-28 10:32:11.456 [INF] Article created: Titlu test by author 1 {"Title": "Titlu test", "AuthorId": 1, ...}
2026-04-28 10:32:11.789 [INF] Article created with id 42 {"ArticleId": 42, ...}
dotnet test continuă să fie verde.