Uma falha de domínio que vira Problem Details não deixa rastro nos seus logs — ela nunca foi uma exceção. Offside.ApplicationInsights registra valores Error como traces do Application Insights, com severidade derivada do ErrorKind e dimensões estáveis para filtrar no Kusto.
Este pacote fala com o SDK clássico Microsoft.ApplicationInsights e precisa do TelemetryClient do host. Se o seu host estiver instrumentado com Azure.Monitor.OpenTelemetry.AspNetCore, não existe TelemetryClient para resolver — use o Offside.OpenTelemetry. Os dois são alternativas, não camadas, e concordam na severidade kind a kind.
dotnet add package Offside
dotnet add package Offside.ApplicationInsights
Configure o Application Insights no host primeiro, depois a integração:
using Offside.ApplicationInsights;
builder.Services.AddApplicationInsightsTelemetry();
builder.Services.AddOffsideApplicationInsights();
AddOffsideApplicationInsights registra IDomainErrorRecorder (namespace Offside) sobre o TelemetryClient do host. Ele nunca lê connection string nem chama AddApplicationInsightsTelemetry.
A mensagem do trace é a mensagem resolvida do catálogo, vinda do IErrorMessageResolver que o AddOffside registrou. Sem ele, o Code do erro é escrito no lugar.
Não registre este pacote e Offside.OpenTelemetry no mesmo host — são alternativas, e compartilham a mesma interface IDomainErrorRecorder.
Num host HTTP, registre o recorder e chame ToHttpResult / SendOffsideAsync. O pipeline grava segundo RecordMode (um evento por erro por padrão). RecordTo no endpoint é redundante. Hosts que alertam por falha de requisição, não por cada campo, definem RecordMode = ProblemRecordMode.PrimaryErrorOnly no AddOffsideAspNetCore. RecordTo em worker ou handler MediatR é sempre um evento por erro.
app.MapPost("/orders/{id}/cancel", (string id, HttpContext http) =>
_orders.Cancel(id).ToHttpResult(http));
Workers, handlers MediatR e qualquer caminho sem HttpContext ainda chamam RecordTo:
var result = _orders.Cancel(id).RecordTo(_recorder);
RecordTo escreve um trace por erro, na ordem do result, e devolve o result inalterado para caber numa cadeia. Um result de sucesso não registra nada. Dimensões extras entram assim:
result.RecordTo(_recorder, new Dictionary<string, string> { ["tenant"] = tenantId });
Dimensões extras HTTP vêm de OffsideAspNetCoreOptions.TelemetryProperties. As dimensões do Offside sempre vencem as fornecidas — uma chave de tenant nunca reescreve offside.kind. Veja Consultar erros de domínio para Kusto.
| Dimensão | Valor |
|---|---|
offside.code |
A chave de catálogo — order.already_shipped |
offside.errorCode |
O identificador de tela — ORDER_ALREADY_SHIPPED |
offside.kind |
A espécie de falha — Conflict |
offside.field |
O campo ofensor, quando o erro tem um |
A severidade vem do kind:
| Kind | Severidade |
|---|---|
Unexpected |
Critical |
ServiceUnavailable, Timeout |
Error |
Unauthorized, Forbidden, TooManyRequests, Conflict, PreconditionFailed, Gone, Unprocessable |
Warning |
NotFound, Validation, BadRequest |
Information |
A razão do corte: uma falha de validação é o sistema funcionando e não deve acordar ninguém; um 500 ou uma queda de dependência deve. Esse mapa é DomainErrorSeverityMap.Library, o padrão. Uma visão de operações que ainda quer 404/400 em Warning usa o outro preset:
builder.Services.AddOffsideApplicationInsights(options =>
options.SeverityFor = DomainErrorSeverityMap.Operations);
Operations sobe NotFound / Validation / BadRequest para Warning e desce Unexpected de Critical para Error. Quedas de dependência continuam Error. Troque o mapa inteiro com options.SeverityFor quando o seu time traçar a linha em outro lugar.
Uma consulta Kusto sobre o resultado está em Consultar erros de domínio.
Por padrão o texto do trace é só a mensagem resolvida do catálogo. Código, kind e campo viajam como dimensões, então nada se perde — e no Kusto essas dimensões são consultáveis sem lotar cada linha renderizada.
O trade-off se inverte quando alguém lê as linhas cruas — um console, o log de um container, um kubectl logs — onde nada renderiza as dimensões:
builder.Services.AddOffsideApplicationInsights(options =>
options.FormatMessage = DomainErrorMessageFormat.CodePrefixed);
[order.already_shipped] Pedido já enviado.
Três formatos vêm prontos:
| Formato | Linha |
|---|---|
MessageOnly (padrão) |
Pedido já enviado. |
CodePrefixed |
[order.already_shipped] Pedido já enviado. |
ErrorCodePrefixed |
[ORDER_ALREADY_SHIPPED] Pedido já enviado. |
O ErrorCodePrefixed se justifica quando o suporte lê o log pelo identificador que o usuário informa a partir da tela.
Qualquer Func<Error, string, string> serve — o erro, e a mensagem já resolvida:
options.FormatMessage = (error, message) => $"{error.Kind}/{error.Code}: {message}";
O formato molda o texto do trace e nada mais: as dimensões não são afetadas, então uma linha mais curta nunca custa um filtro. O Offside.OpenTelemetry oferece os mesmos três formatos com os mesmos nomes, e um teste falha se os dois passarem a renderizar diferente.
Error.Arguments não são escritos por padrão. Eles carregam o que o domínio colocou neles — identificadores, valores tentados, um motivo vindo de uma dependência — e telemetria sobrevive à requisição por meses. Ligue apenas quando souber que todo argumento é seguro:
builder.Services.AddOffsideApplicationInsights(options => options.IncludeArguments = true);
Eles aparecem como offside.arg.{nome}; argumentos nulos são ignorados.
Prefira uma allowlist quando só algumas chaves forem seguras:
builder.Services.AddOffsideApplicationInsights(options =>
options.IncludeArgumentKeys = ["rejectionReason"]);
IncludeArguments = true ignora a lista e escreve todo argumento.
| Opção | Padrão | O que faz |
|---|---|---|
PropertyPrefix |
offside. |
Prefixo de toda dimensão do Offside |
IncludeArguments |
false |
Escreve todo valor de Error.Arguments como dimensão |
IncludeArgumentKeys |
vazio | Escreve só os argumentos nomeados; ignorado quando IncludeArguments é true |
Culture |
InvariantCulture |
Cultura em que a mensagem do trace é resolvida — deliberadamente não é a cultura da requisição, para o log ficar num idioma só |
SeverityFor |
DomainErrorSeverityMap.Library |
Escolhe a severidade de um kind |
FormatMessage |
MessageOnly |
Monta o texto do trace a partir do erro e da mensagem resolvida |
Se você já publica falhas como domain notifications, o Offside.ApplicationInsights.MediatR registra cada uma — sem mudar nenhum ponto de chamada:
dotnet add package Offside.ApplicationInsights.MediatR
builder.Services.AddMediatR(c => c.RegisterServicesFromAssemblyContaining<Program>());
builder.Services.AddOffsideMediatR(); // o coletor com escopo
builder.Services.AddOffsideApplicationInsights(); // o gravador
builder.Services.AddOffsideApplicationInsightsMediatR(); // a ponte
AddOffsideApplicationInsightsMediatR é idempotente, e a ponte roda ao lado do coletor — um não substitui o outro. Veja o guia do MediatR para a publicação.
Offside.Refit expõe IExternalApiErrorObserver para falhas vistas no fio. Um pequeno adaptador as encaminha para cá; veja Observar falhas no fio.