Pular para o conteúdo principal
ImmutableLog logo
Voltar
ASP.NET CoreC#

Integração com ASP.NET Core

Integre o ImmutableLog em qualquer aplicação ASP.NET Core com um middleware `IMiddleware` que captura automaticamente todas as requisições HTTP. O envio é fire-and-forget — nunca bloqueia a resposta.

Código do middleware

Salve em `ImmutableLogMiddleware.cs`. O middleware implementa `IMiddleware` (resolvido via DI), captura o `Response.StatusCode` após o `next` e envia o evento em uma Task descartada.

csharp
// ImmutableLogMiddleware.cs
using System.Diagnostics;
using System.Net.Http.Json;
using System.Text.Json;

public sealed class ImmutableLogOptions
{
    public string ApiKey { get; init; } = "";
    public string ApiUrl { get; init; } = "https://api.immutablelog.com";
    public string Service { get; init; } = "aspnet-service";
    public string Env { get; init; } = "production";
    // Timezone do cliente — usado pelo dashboard para exibir os timestamps.
    public string ClientTz { get; init; } = "America/Sao_Paulo";
    public int ClientOffsetMinutes { get; init; } = -180;
    public HashSet<string> SkipPaths { get; init; } = new() { "/health", "/healthz" };
}

public sealed class ImmutableLogMiddleware : IMiddleware
{
    // Chaves cujos valores sao mascarados antes do envio (PII/segredos).
    private static readonly HashSet<string> Sensitive = new(StringComparer.OrdinalIgnoreCase)
    {
        "password", "pwd", "senha", "token", "access_token", "refresh_token", "api_key",
        "apikey", "secret", "client_secret", "authorization", "auth", "credit_card",
        "card_number", "cvv", "cpf", "otp", "code",
    };

    private readonly HttpClient _http;
    private readonly ImmutableLogOptions _opts;

    public ImmutableLogMiddleware(IHttpClientFactory factory, ImmutableLogOptions opts)
    {
        _http = factory.CreateClient("immutablelog");
        _opts = opts;
    }

    public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
    {
        if (_opts.SkipPaths.Contains(ctx.Request.Path))
        {
            await next(ctx);
            return;
        }

        var sw = Stopwatch.StartNew();
        var requestId = ctx.Request.Headers["X-Request-Id"].FirstOrDefault()
            ?? Guid.NewGuid().ToString();

        await next(ctx);
        sw.Stop();

        // fire-and-forget — nunca bloqueia a resposta.
        _ = EmitAsync(ctx, ctx.Response.StatusCode, sw.Elapsed, requestId);
    }

    private async Task EmitAsync(HttpContext ctx, int status, TimeSpan elapsed, string requestId)
    {
        try
        {
            if (string.IsNullOrEmpty(_opts.ApiKey)) return;

            var kind = status >= 400 ? "error"
                : status >= 300 ? "info"
                : status >= 200 ? "success" : "info";
            var method = ctx.Request.Method;
            var path = ctx.Request.Path.Value ?? "/";

            var eventName = ctx.Items["imtbl.eventName"] as string ?? $"http.{method}.{path}";
            var trail = SanitizeTrail(
                ctx.Items["imtbl.trail"] as string
                ?? ctx.Request.Headers["X-Imtbl-Trail"].FirstOrDefault());

            // query params passam por redact() — nunca enviar segredos em claro.
            var query = ctx.Request.Query.ToDictionary(
                kv => kv.Key,
                kv => Sensitive.Contains(kv.Key) ? "***REDACTED***" : kv.Value.ToString());

            var payload = new Dictionary<string, object?>
            {
                ["id"] = Guid.NewGuid().ToString(),
                ["kind"] = kind,
                ["message"] = $"{method} {path} -> {status}",
                ["timestamp"] = DateTimeOffset.UtcNow.ToString("o"),
                ["request"] = new { request_id = requestId, method, path,
                    query_params = query.Count > 0 ? query : null },
                ["metrics"] = new { latency_ms = (long)elapsed.TotalMilliseconds, status_code = status },
                ["severity"] = kind == "error" ? "high" : "low",
            };
            if (kind == "error")
            {
                payload["error"] = new
                {
                    status_code = status,
                    retryable = new[] { 408, 429, 500, 502, 503, 504 }.Contains(status),
                };
            }

            // Todos os valores de meta precisam ser strings.
            var meta = new Dictionary<string, string>
            {
                ["type"] = kind,
                ["event_name"] = eventName,
                ["service"] = _opts.Service,
                ["request_id"] = requestId,
                ["env"] = _opts.Env,
            };
            if (trail is not null) meta["immutable_trail"] = trail;

            var body = new { payload = JsonSerializer.Serialize(payload), meta };

            using var req = new HttpRequestMessage(HttpMethod.Post, $"{_opts.ApiUrl}/v1/events")
            {
                Content = JsonContent.Create(body),
            };
            req.Headers.Add("Authorization", $"Bearer {_opts.ApiKey}");
            // Idempotency-Key e OBRIGATORIO (sem ele a API responde 400).
            req.Headers.Add("Idempotency-Key", $"{eventName}-{requestId}");
            req.Headers.Add("Request-Id", requestId);
            req.Headers.Add("X-Client-TZ", _opts.ClientTz);
            req.Headers.Add("X-Client-Offset-Minutes", _opts.ClientOffsetMinutes.ToString());
            if (trail is not null) req.Headers.Add("X-Imtbl-Trail", trail);

            await _http.SendAsync(req);
        }
        catch
        {
            // Never let audit logging break the application.
        }
    }

    // Normaliza o immutable_trail (trim, nao-vazio, max 256, sem ':').
    private static string? SanitizeTrail(string? value)
    {
        if (string.IsNullOrWhiteSpace(value)) return null;
        var v = value.Trim().Replace(":", "-");
        if (v.Length > 256) v = v[..256];
        return v;
    }
}

Registro e Configuração

Registre as opções, o `IHttpClientFactory` e o middleware no `Program.cs`. Use variáveis de ambiente para as credenciais.

csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpClient("immutablelog", c => c.Timeout = TimeSpan.FromSeconds(5));
builder.Services.AddSingleton(new ImmutableLogOptions
{
    ApiKey = Environment.GetEnvironmentVariable("IMTBL_API_KEY") ?? "",
    Service = "my-aspnet-service",
    Env = builder.Environment.EnvironmentName,
    SkipPaths = new() { "/health", "/healthz", "/metrics" },
});
builder.Services.AddSingleton<ImmutableLogMiddleware>(); // IMiddleware resolvido via DI

var app = builder.Build();

// Registre cedo no pipeline para capturar todas as requisições.
app.UseMiddleware<ImmutableLogMiddleware>();

app.MapControllers();
app.Run();

Nunca coloque o token no código. Carregue IMTBL_API_KEY de variáveis de ambiente ou do user-secrets em desenvolvimento.

Como funciona

InvokeAsync

Registra request_id e Stopwatch antes do next(ctx)

await next(ctx)

Pipeline executa; Response.StatusCode fica disponível

_ = EmitAsync(...)

Task descartada envia o evento — não bloqueia a resposta

Evento e trilha customizados

Defina `HttpContext.Items["imtbl.eventName"]` e `HttpContext.Items["imtbl.trail"]` em qualquer controller ou endpoint. O middleware lê esses valores ao emitir o evento.

csharp
[ApiController]
[Route("payments")]
public class PaymentsController : ControllerBase
{
    [HttpPost]
    public IActionResult Create()
    {
        // Sobrescreve o nome do evento e agrupa numa trilha auditavel.
        HttpContext.Items["imtbl.eventName"] = "payment.created";
        HttpContext.Items["imtbl.trail"] = "order-7782";

        return Ok(new { ok = true, payment_id = "pay_123" });
    }
}

Padrão automático: http.METHOD.path.

Trilha imutável (immutable_trail)

A trilha agrupa eventos relacionados em uma mesma linha do tempo auditável. Defina `HttpContext.Items["imtbl.trail"]` em um endpoint, ou propague entre serviços com o header `X-Imtbl-Trail`. O valor passa por `SanitizeTrail` antes do envio.

csharp
[HttpPost("flows/{id}/run")]
public IActionResult RunFlow(string id)
{
    HttpContext.Items["imtbl.trail"] = $"flow-{id}";
    // ... business logic ...
    return Ok(new { status = "ok" });
}

A trilha não pode ser vazia, exceder 256 caracteres ou conter `:` — violações retornam `400 invalid_immutable_trail`. O `SanitizeTrail` corrige o valor antes do envio.

Resposta e códigos de status

A API de ingestão é assíncrona. Sucesso retorna 202 (novo) ou 200 (duplicata idempotente). Como o middleware é fire-and-forget, esses códigos importam principalmente se você enviar eventos manualmente.

StatusSignificado
202Evento aceito e enfileirado (novo)
200Idempotency-Key já existente — duplicate: true
400Idempotency-Key ausente/vazia ou invalid_immutable_trail
403Assinatura inativa/expirada, escopo ou retenção
413payload_too_large (limite do servidor: 16KB)
429monthly_limit_exceeded — não fazer retry
503mempool_full — transitório, pode dar retry

Esta documentação reflete o comportamento atual da integração. Para dúvidas ou integrações avançadas, entre em contato com o time de suporte.