Se você já desenvolveu para a web, conhece a sensação: um usuário reporta um erro que você não consegue reproduzir. “Deu um problema na hora de salvar”, ele diz. Sem mais detalhes. Se o problema aconteceu no servidor, você provavelmente tem logs para investigar. Mas e se o erro ocorreu inteiramente no navegador do usuário? Os logs do console, se existirem, foram perdidos para sempre no momento em que a aba foi fechada.

É aqui que o desafio do logging em Blazor WebAssembly (WASM) se manifesta. Nossas aplicações .NET agora rodam em uma sandbox dentro do navegador do cliente, longe do ambiente controlado do nosso servidor. Como podemos capturar os eventos, os avisos e os erros críticos de uma aplicação cliente de forma confiável?

A resposta é: construindo um sistema de logging customizado.

Neste guia completo, faremos exatamente isso. Iremos muito além de uma simples receita de código. Você vai aprender:

  • Os fundamentos da Injeção de Dependência (DI), o pilar que sustenta as aplicações .NET modernas.
  • Por que soluções de logging populares, como o NLog, podem não funcionar como esperado no ambiente Blazor WASM, especialmente com chamadas de rede.
  • O passo a passo para construir um provedor de log HTTP robusto, capaz de enviar logs para uma API central de forma segura e eficiente.
  • Como diagnosticar e resolver problemas avançados de DI que podem surgir no caminho.

Ao final, você terá não apenas o código, mas o conhecimento profundo para tornar suas aplicações Blazor verdadeiramente observáveis.

Fundamentos: O Que é Injeção de Dependência (DI) e Por Que Ela é Essencial?

Antes de escrevermos nosso logger, precisamos entender a magia que permite ao .NET conectar tudo de forma tão flexível: a Injeção de Dependência (DI). Se você já usou ASP.NET Core ou Blazor, você já se beneficiou dela, talvez sem perceber.

Em termos simples, DI é um padrão de projeto que segue o Princípio da Inversão de Controle (IoC). Em vez de uma classe criar suas próprias dependências, ela as recebe “injetadas” de fora.

Pense em um chef de cozinha. Em vez de ele mesmo ir à dispensa buscar cada ingrediente (criando suas dependências), ele simplesmente diz o que precisa, e um assistente (o contêiner de DI) entrega tudo pronto na bancada. O chef não precisa saber de onde vieram os ingredientes, apenas que eles estão disponíveis.

No universo .NET, essa “mágica” é orquestrada por dois atores principais no arquivo Program.cs:

  1. IServiceCollection (builder.Services): É o “cardápio” da nossa aplicação. É aqui que registramos todos os serviços e suas implementações, dizendo ao .NET: “Quando alguém pedir por X, entregue uma instância de Y”.
  2. IServiceProvider: É o “assistente” ou “garçom”. Após o cardápio ser montado, é ele quem de fato cria e entrega as instâncias dos serviços quando são solicitados.

Ao registrar um serviço, também definimos seu ciclo de vida:

  • AddTransient: Um novo objeto é criado toda vez que é solicitado. É como pedir uma nova bebida a cada vez.
  • AddScoped: Um único objeto é criado por requisição (no contexto de uma requisição web) ou por conexão (no Blazor Server).
  • AddSingleton: Um único objeto é criado para toda a vida da aplicação. É o mesmo objeto para todos que o solicitarem.

E o que isso tem a ver com logging?

O sistema de ILogger<T> do .NET é um dos exemplos mais clássicos de DI em ação. Quando você adiciona ILogger<MinhaClasse> ao construtor da sua classe, o IServiceProvider vê sua solicitação, cria (ou reutiliza) uma instância de logger configurada para aquela classe e a entrega para você. Nosso objetivo é justamente ensinar o contêiner de DI a usar o nosso provedor de log customizado para atender a esses pedidos.

O Obstáculo Comum: Por Que NLog (e outros) Falham com Chamadas HTTP no Blazor WASM?

Uma reação comum de desenvolvedores experientes em backend é tentar usar uma biblioteca de logging familiar como NLog ou Serilog. Eles são extremamente poderosos no servidor. O problema é que o ambiente Blazor WASM impõe restrições únicas que quebram a abordagem tradicional.

O problema central reside na colisão de duas naturezas:

  1. A interface ILogger.Log() é síncrona. Ela retorna void e não foi projetada para operações de longa duração ou de I/O, como chamadas de rede.
  2. Chamadas de rede (HttpClient) são, por natureza, assíncronas. A melhor prática é sempre usar await para esperar a resposta.

Você não pode simplesmente chamar um método async dentro de um método síncrono sem await (uma prática conhecida como “fire-and-forget”) e esperar que funcione. No Blazor WASM, isso é uma receita para a perda de dados.

Imagine o cenário mais crítico: um erro não tratado ocorre e sua aplicação está prestes a travar. O sistema de logging tenta enviar uma última mensagem de erro para sua API. Se essa chamada for “fire-and-forget”, a aplicação WASM pode terminar seu ciclo de vida e ser descarregada pelo navegador antes que a requisição HTTP seja completada. O resultado: o log de erro mais importante nunca chega ao seu destino.

É por isso que precisamos de uma arquitetura que desacople o ato de registrar um log (rápido e síncrono) do ato de enviá-lo pela rede (lento e assíncrono).

Passo a Passo: Construindo um Provedor de Log HTTP Robusto

Vamos construir uma solução resiliente usando o padrão “Producer/Consumer”. Nossos loggers serão os “produtores” de mensagens, que as colocarão em uma fila. Um serviço de background será o “consumidor”, que esvazia a fila e envia os dados.

Passo 1: A Fila Central de Logs

Primeiro, criamos um serviço singleton que servirá como nosso buffer de mensagens centralizado.

using System.Collections.Concurrent;

// Um serviço singleton para manter a fila de logs.
public class LogQueueService
{
    public ConcurrentQueue<string> Queue { get; } = new();
}

Passo 2: O Provedor e o Logger

Agora, criamos o ILogger que adiciona mensagens a essa fila e o ILoggerProvider que o cria.

// Nosso Logger, que produz mensagens para a fila.
public class HttpBatchLogger : ILogger
{
    private readonly string _categoryName;
    private readonly LogQueueService _logQueue;

    public HttpBatchLogger(string categoryName, LogQueueService logQueue)
    {
        _categoryName = categoryName;
        _logQueue = logQueue;
    }

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
    {
        if (!IsEnabled(logLevel))
        {
            return;
        }

        // Formata a mensagem e a adiciona na fila. Operação rápida e síncrona.
        var message = $"[{logLevel}] {_categoryName}: {formatter(state, exception)}";
        _logQueue.Queue.Enqueue(message);
    }

    public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;

    public IDisposable BeginScope<TState>(TState state) => default!;
}

// Nosso Provedor, que cria instâncias do Logger.
public class HttpBatchLoggerProvider : ILoggerProvider
{
    private readonly LogQueueService _logQueue;

    public HttpBatchLoggerProvider(LogQueueService logQueue)
    {
        _logQueue = logQueue;
    }

    public ILogger CreateLogger(string categoryName)
    {
        return new HttpBatchLogger(categoryName, _logQueue);
    }

    public void Dispose() { }
}

Passo 3: O Serviço de Envio em Background

Este é o “consumidor”. Um IHostedService que roda em segundo plano, periodicamente enviando os logs da fila para a nossa API.

// Serviço que consome a fila e envia os logs em lote.
public class HttpLogSenderService : IHostedService, IAsyncDisposable
{
    private readonly LogQueueService _logQueue;
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IConfiguration _configuration;
    private Timer? _timer;

    public HttpLogSenderService(LogQueueService logQueue, IHttpClientFactory httpClientFactory, IConfiguration configuration)
    {
        _logQueue = logQueue;
        _httpClientFactory = httpClientFactory;
        _configuration = configuration;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        // Inicia um timer para verificar a fila a cada 5 segundos.
        _timer = new Timer(SendLogs, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
        return Task.CompletedTask;
    }

    private void SendLogs(object? state)
    {
        // Usamos Task.Run para não bloquear o timer e poder usar async/await.
        _ = Task.Run(async () =>
        {
            if (_logQueue.Queue.IsEmpty)
            {
                return;
            }

            var logsToSend = new List<string>();
            while (_logQueue.Queue.TryDequeue(out var logMessage))
            {
                logsToSend.Add(logMessage);
            }

            if (logsToSend.Any())
            {
                try
                {
                    var client = _httpClientFactory.CreateClient("LoggingApi");
                    // O endpoint real viria da configuração.
                    var endpoint = _configuration["LoggingApi:Url"]; 
                    // Em um cenário real, o corpo seria um JSON bem estruturado.
                    await client.PostAsJsonAsync(endpoint, logsToSend);
                }
                catch (Exception ex)
                {
                    // Se o envio falhar, podemos tentar reenfileirar ou logar no console.
                    Console.WriteLine($"Error sending logs: {ex.Message}");
                }
            }
        });
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _timer?.Change(Timeout.Infinite, 0);
        // Tenta enviar quaisquer logs restantes antes de parar.
        SendLogs(null);
        return Task.CompletedTask;
    }

    public async ValueTask DisposeAsync()
    {
        _timer?.Dispose();
        // Garante o envio final ao descartar o serviço.
        await Task.Run(() => SendLogs(null));
    }
}

Passo 4: Juntando Tudo no Program.cs

Finalmente, registramos todos os nossos novos serviços no contêiner de DI.

// Em Program.cs
builder.Logging.ClearProviders();

// 1. Registra a fila como Singleton para ser compartilhada.
builder.Services.AddSingleton<LogQueueService>();

// 2. Adiciona nosso provedor de log.
builder.Services.AddSingleton<ILoggerProvider, HttpBatchLoggerProvider>();

// 3. Registra um HttpClient nomeado para a API de logging.
builder.Services.AddHttpClient("LoggingApi");

// 4. Adiciona nosso serviço de background.
builder.Services.AddHostedService<HttpLogSenderService>();

// ... resto da configuração

Com esta arquitetura, nosso sistema de logging é resiliente, eficiente e respeita as restrições do ambiente Blazor WASM.

Desafios Avançados de DI: A Armadilha do BuildServiceProvider

Agora que temos uma solução robusta, vamos explorar um problema mais sutil que pode fazer tudo parar de funcionar. Em um dos nossos projetos, mesmo com a configuração acima, os logs não chegavam. A causa era uma linha de código aparentemente inofensiva no Program.cs:

// Anti-padrão perigoso no meio da configuração de serviços!
var tempServiceProvider = builder.Services.BuildServiceProvider(); 

Chamar BuildServiceProvider() antes de builder.Build() cria um “contêiner de DI cindido”. Isso gera um provedor de serviços temporário e descarta-o, o que pode corromper a resolução de dependências complexas, como o ILogger<T>, para serviços registrados após essa chamada.

Se você se deparar com uma situação em que a injeção de ILogger<T> falha, mas a injeção de ILoggerFactory funciona, você pode ter caído nesta armadilha. Para cenários como este, ou quando uma biblioteca externa exige a interface não genérica ILogger, o Padrão Adapter é uma solução elegante. Você pode criar um ILoggerAdapter<T> que herda de ILogger e injeta a ILoggerFactory para criar o logger real internamente, servindo como uma “ponte” para contornar a resolução quebrada.

Conclusão: Rumo a Aplicações Observáveis

Construir um sistema de logging eficaz em Blazor WASM é mais do que apenas enviar uma requisição HTTP. Exige uma compreensão clara da Injeção de Dependência, do ciclo de vida da aplicação e das restrições do ambiente do navegador.

As lições que aprendemos são:

  • DI é seu alicerce: Entendê-la é crucial para construir aplicações manuteníveis.
  • Respeite a sincronicidade: A interface ILogger é síncrona. Force operações de rede nela por sua conta e risco.
  • Desacople com o padrão Producer/Consumer: Usar uma fila e um serviço de background é a abordagem mais robusta para I/O em loggers.
  • Cuidado com anti-padrões de DI: BuildServiceProvider() prematuro é uma fonte de bugs difíceis de rastrear.
  • Padrões de Projeto salvam o dia: O Adapter pode ser a ferramenta certa para resolver conflitos de dependência.

Com essas técnicas, você está bem equipado para transformar suas aplicações Blazor de “caixas-pretas” em sistemas transparentes e observáveis.