Effettuare il tracing asincrono delle chiamate a un'applicazione ASP.NET Core

di Marco De Sanctis, in ASP.NET Core,

Uno dei requisiti più comuni nell'ambito delle applicazioni enterprise è quello di mantenere un audit delle chiamate effettuate dagli utenti, così che si possa eventualmente risalire a chi ha effettuato una determinata operazione, o monitorare accessi illeciti.

Si tratta di un problema in apparenza banale, basterebbe infatti creare un filter o un middleware che memorizzi gli estremi della richiesta su un database. Tuttavia, in produzione, effettuare una chiamata in scrittura su un database a ogni richiesta potrebbe sia causare tempi d'attesa non trascurabili, e addirittura causare dei problemi di scalabilità sul database se il traffico dovesse essere elevato.

Una possibile soluzione, allora, è quella di accodare queste istanze di log in memoria e poi procedere alla loro memorizzazione in batch, dopo che ne abbiamo accumulato un certo numero. Anche questo sembra un task in apparenza banale, ma che diventa tutt'altro che scontato in un contesto multi-thread come quello delle web application. Per fortuna c'è una serie di classi all'interno della Task Parallel Library che vengono in nostro aiuto: DataFlow.

Il primo passo è quello di creare un oggetto AuditTrace, che modellerà l'insieme di informazioni che vogliamo tracciare. Per esempio, possiamo usare qualcosa di simile al codice in basso:

public class AuditTrace
{
    public string Url { get; set; }
        
    public string HttpVerb { get; set; }
        
    public ClaimsPrincipal Principal { get; set; }
        
    public DateTime TimeStamp { get; set; }
}

Poi dobbiamo costruire la coda che useremo per accumulare un batch di trace da memorizzare, tramite la classe AuditQueue:

internal class AuditQueue
{
    private BatchBlock<AuditTrace> _queue;

    public AuditQueue(IAuditRepository auditRepository)
    {
        var batchSize = 50;

        _queue = new BatchBlock<AuditTrace>(batchSize);

        var actionBlock = new ActionBlock<AuditTrace[]>(async traces =>
        {
            await auditRepository.SaveTracesAsync(traces);
        });
        _queue.LinkTo(actionBlock);
    }

    public Task SendAsync(AuditTrace trace)
    {
        return _queue.SendAsync(trace);
    }
}

Questo codice incapsula interamente la nostra logica di spooling tramite una semplice pipeline costituita da due oggetti della DataFlow library: BatchBlock e ActionBlock.


BatchBlock funge da accumulatore dei trace: lo abbiamo inizializzato a una dimensione di 50 elementi e, ogni volta che inviamo un trace tramite il metodo SendAsync, questo verrà mantenuto in memoria fino al raggiungimento di questa soglia massima.

A questo punto, BatchBlock invierà l'intero batch di trace al secondo elemento della pipeline, ossia ActionBlock, che eseguirà l'azione che abbiamo specificato, ossia memorizzarli tramite una classe AuditRepository.

Le due classi sono thread safe, e pertanto sono adatte a essere utilizzato in un contesto web; inoltre il fatto di effettuare una chiamata a database ogni 50 richieste è molto più efficiente di 50 chiamate individuali, magari in parallelo, e farà sì che il carico su quest'ultimo sia assolutamente trascurabile.

L'ultimo passaggio è quello di registrare AuditQueue nell'IoC container, insieme al middleware che lo invochi a ogni richiesta. Come immaginiamo, possiamo farlo nella classe Startup:

public void ConfigureServices(IServiceCollection services)
{
    // ..altro codice qui..

    services.AddSingleton<AuditQueue>();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ..altro codice qui..
    app.Use(async (ctx, next) => 
    {
        var trace = new AuditTrace()
        {
            Url = ctx.Request.GetDisplayUrl()
            ...
        };

        var queue = ctx.RequestServices.GetService<AuditQueue>();
        await queue.SendAsync(trace);

        await next();
    });

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

Come possiamo notare, abbiamo registrato AuditQueue come singleton, perchè vogliamo che tutte le richieste accedano alla medesima coda. Il middleware, poi, è di per sé piuttosto semplice, e si limita a recuperare l'istanza di AuditQueue e inviare il trace della richiesta corrente.

Resta solo un nodo da sciogliere: come gestiamo il caso dello shutdown dell'applicazione, per assicurare di non perdere trace parzialmente accumulati? sarà l'argomento del prossimo script.

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Approfondimenti

I più letti di oggi