Event schema
Formato canônico ObsEvent v1.0 publicado pelo obs-normalizer — campos obrigatórios, enriquecimento de severity/fingerprint e exemplo completo.
Event schema
O obs-normalizer consome webhooks heterogêneos (GlitchTip, Alertmanager) e publica eventos em um formato canônico chamado ObsEvent. Todos os consumidores downstream (RabbitMQ, triagem de incidentes, automações) tratam apenas esse schema — fontes adicionais devem ser adaptadas pelos receivers em src/receivers/.
Schema canônico
O tipo ObsEvent é exportado pelo pacote @simplafy-admin/obs-schema e usado tanto pelo parser de GlitchTip quanto pelo de Alertmanager. A versão atual é 1.0 e todo evento publicado declara explicitamente schema_version.
type ObsEvent = {
schema_version: '1.0'
event_id: string // ULID gerado pelo normalizer
event_type:
| 'issue.created'
| 'issue.regressed'
| 'issue.resolved'
| 'alert.fired'
| 'alert.resolved'
severity: 'info' | 'warning' | 'error' | 'critical'
timestamp: string // ISO-8601
source: {
system: 'glitchtip' | 'alertmanager'
project: string
service: string
environment: string
}
correlation: {
trace_id: string | null
span_id: string | null
issue_id: string | null
fingerprint: string // sha256 estável
}
payload: {
title: string
message: string
culprit: string
url: string
release: string
user_affected_count: number
event_count: number
}
trace_summary: TraceSummary | null
links: {
logs_query: string // LogQL pré-montado para Loki
traces_url: string // deep link Grafana/Tempo
repository: string
commit: string
file_hint: string | null
}
metadata: {
tags: Record<string, string>
custom: Record<string, unknown>
}
}Cada webhook do Alertmanager pode conter vários alertas (raw.alerts[]). O receiver parseAlertmanagerWebhook retorna um ObsEvent[], e o normalizer publica uma mensagem por alerta. GlitchTip sempre emite um único evento por webhook.
Campos obrigatórios
Todos os campos do ObsEvent são preenchidos em toda publicação. Quando a fonte não fornece dado equivalente, o normalizer aplica fallback determinístico em vez de omitir a chave.
| Campo | Origem GlitchTip | Origem Alertmanager |
|---|---|---|
event_id | ulid() gerado no parser | ulid() gerado no parser |
event_type | derivado de status + count | alert.fired ou alert.resolved |
severity | mapeado de issue.level | mapeado de labels.severity |
timestamp | issue.lastSeen | endsAt quando resolvido, senão startsAt |
source.system | "glitchtip" | "alertmanager" |
source.project | issue.project.slug | labels.namespace ou "unknown" |
source.service | project.slug sem prefixo simplafy- | labels.service ou "unknown" |
source.environment | tag environment ou NODE_ENV | labels.environment ou NODE_ENV |
correlation.trace_id | tag trace_id (ou null) | sempre null |
correlation.issue_id | String(issue.id) | sempre null |
correlation.fingerprint | sha256 calculado | alert.fingerprint ou sha256 calculado |
payload.title | issue.title | annotations.summary ou alertname |
payload.message | metadata.value ou title | annotations.description ou summary |
payload.event_count | issue.count | 1 |
payload.user_affected_count | issue.userCount | 0 |
links.logs_query | LogQL com trace_id quando disponível | {namespace=..., app=...} |
Receivers nunca propagam o payload bruto do upstream. Campos desconhecidos do GlitchTip ou Alertmanager terminam em metadata.custom ou metadata.tags — o restante do schema permanece fixo para garantir contratos estáveis com os consumidores.
Enriquecimento (severity, fingerprint)
Severity
Cada receiver normaliza para um dos quatro níveis canônicos: info, warning, error, critical.
GlitchTip — src/receivers/glitchtip.ts:
const levelToSeverity: Record<string, Severity> = {
debug: 'info',
info: 'info',
warning: 'warning',
error: 'error',
fatal: 'critical',
}
// fallback quando level é desconhecido
severity: levelToSeverity[issue.level] ?? 'error'Alertmanager — src/receivers/alertmanager.ts:
const validSeverities = ['info', 'warning', 'error', 'critical'] as const
function mapSeverity(raw: string | undefined): Severity {
if (raw && validSeverities.includes(raw)) return raw
return 'warning' // fallback quando label severity está ausente/inválido
}Fingerprint
correlation.fingerprint é a chave usada para deduplicação e agrupamento downstream. Ela é estável entre eventos repetidos do mesmo problema.
GlitchTip — sha256 sobre project.slug | metadata.type | culprit:
const fingerprint = createHash('sha256')
.update(`${issue.project.slug}|${issue.metadata.type ?? ''}|${issue.culprit}`)
.digest('hex')Alertmanager — usa o fingerprint do alerta quando presente. Caso contrário, sha256 sobre alertname | service | namespace:
const fingerprint =
alert.fingerprint
?? createHash('sha256')
.update(`${alertname}|${service}|${namespace}`)
.digest('hex')Trace summary opcional
Quando correlation.trace_id está presente, o normalizer pode enriquecer trace_summary consultando o Tempo via src/enrich/tempo.ts. O resumo inclui root_service, root_operation, duration_ms, services_involved, error_span_service e tempo_url. Falhas de fetch ou timeout resultam em trace_summary: null — o evento é publicado mesmo assim.
return {
root_service: root?.service ?? 'unknown',
root_operation: root?.span.name ?? 'unknown',
duration_ms: durationMs,
services_involved: Array.from(services),
error_span_service: errorSpan?.service ?? null,
tempo_url: `https://grafana.simplafy.com.br/explore?trace_id=${traceId}`,
}Links derivados
links.logs_query é uma expressão LogQL pronta para ser colada no Grafana/Loki. Para GlitchTip, quando há trace_id na tag, o normalizer gera o filtro por serviço + trace_id; sem trace_id, o campo fica vazio. Para Alertmanager, o filtro usa namespace e app.
links.commit é extraído do issue.release do GlitchTip aplicando o padrão <service>@<sha> (7 a 40 hex). Releases que não casam com o padrão são preservadas como string crua.
Exemplo completo
Evento gerado a partir do fixture test/fixtures/glitchtip-issue-created.json (issue com count = 23, portanto event_type = "issue.regressed"):
{
"schema_version": "1.0",
"event_id": "01HXYZABCDEFGHJKMNPQRSTVWX",
"event_type": "issue.regressed",
"severity": "error",
"timestamp": "2026-04-22T14:35:00Z",
"source": {
"system": "glitchtip",
"project": "simplafy-hub-api",
"service": "hub-api",
"environment": "production"
},
"correlation": {
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"issue_id": "42",
"fingerprint": "f3e5...a91c"
},
"payload": {
"title": "TypeError: Cannot read property 'x' of undefined",
"message": "Cannot read property 'x' of undefined",
"culprit": "apps/api/src/users.service.ts:123 in getUser",
"url": "https://glitchtip.simplafy.com.br/simplafy-hub-api/issues/42",
"release": "[email protected]",
"user_affected_count": 7,
"event_count": 23
},
"trace_summary": {
"root_service": "hub-api",
"root_operation": "GET /v1/users/:id",
"duration_ms": 312.4,
"services_involved": ["hub-api", "hub-postgres"],
"error_span_service": "hub-api",
"tempo_url": "https://grafana.simplafy.com.br/explore?trace_id=4bf92f3577b34da6a3ce929d0e0e4736"
},
"links": {
"logs_query": "{service=\"simplafy-hub-api\"} |= \"4bf92f3577b34da6a3ce929d0e0e4736\"",
"traces_url": "https://grafana.simplafy.com.br/explore?trace_id=4bf92f3577b34da6a3ce929d0e0e4736",
"repository": "https://github.com/Simplafy-tec/hub-api",
"commit": "1.4.2",
"file_hint": "apps/api/src/users.service.ts"
},
"metadata": {
"tags": {
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"environment": "production"
},
"custom": {}
}
}Equivalente para Alertmanager (alerta firing com labels.severity = "critical"):
{
"schema_version": "1.0",
"event_id": "01HXYZABCDEFGHJKMNPQRSTVWY",
"event_type": "alert.fired",
"severity": "critical",
"timestamp": "2026-04-22T14:35:00Z",
"source": {
"system": "alertmanager",
"project": "simplafy-prd-hub",
"service": "hub-langgraph",
"environment": "production"
},
"correlation": {
"trace_id": null,
"span_id": null,
"issue_id": null,
"fingerprint": "a1b2c3d4e5f6..."
},
"payload": {
"title": "HubLanggraphHighErrorRate",
"message": "Error rate above 5% for 10m on hub-langgraph",
"culprit": "simplafy-prd-hub/hub-langgraph",
"url": "https://prometheus.simplafy.com.br/graph?...",
"release": "",
"user_affected_count": 0,
"event_count": 1
},
"trace_summary": null,
"links": {
"logs_query": "{namespace=\"simplafy-prd-hub\", app=\"hub-langgraph\"}",
"traces_url": "https://prometheus.simplafy.com.br/graph?...",
"repository": "",
"commit": "",
"file_hint": null
},
"metadata": {
"tags": {
"alertname": "HubLanggraphHighErrorRate",
"namespace": "simplafy-prd-hub",
"service": "hub-langgraph",
"severity": "critical",
"environment": "production"
},
"custom": {
"annotations": {
"summary": "HubLanggraphHighErrorRate",
"description": "Error rate above 5% for 10m on hub-langgraph"
}
}
}
}Para adicionar uma nova fonte, crie um receiver em src/receivers/<source>.ts que aceite o payload bruto e retorne ObsEvent | ObsEvent[]. O contrato com os consumidores não muda, e o downstream não precisa saber da nova fonte.