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.
// 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.
// 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.
[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.
[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.
| Status | Significado |
|---|---|
| 202 | Evento aceito e enfileirado (novo) |
| 200 | Idempotency-Key já existente — duplicate: true |
| 400 | Idempotency-Key ausente/vazia ou invalid_immutable_trail |
| 403 | Assinatura inativa/expirada, escopo ou retenção |
| 413 | payload_too_large (limite do servidor: 16KB) |
| 429 | monthly_limit_exceeded — não fazer retry |
| 503 | mempool_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.
