simpla.fydocs
Obs Normalizer

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.

CampoOrigem GlitchTipOrigem Alertmanager
event_idulid() gerado no parserulid() gerado no parser
event_typederivado de status + countalert.fired ou alert.resolved
severitymapeado de issue.levelmapeado de labels.severity
timestampissue.lastSeenendsAt quando resolvido, senão startsAt
source.system"glitchtip""alertmanager"
source.projectissue.project.sluglabels.namespace ou "unknown"
source.serviceproject.slug sem prefixo simplafy-labels.service ou "unknown"
source.environmenttag environment ou NODE_ENVlabels.environment ou NODE_ENV
correlation.trace_idtag trace_id (ou null)sempre null
correlation.issue_idString(issue.id)sempre null
correlation.fingerprintsha256 calculadoalert.fingerprint ou sha256 calculado
payload.titleissue.titleannotations.summary ou alertname
payload.messagemetadata.value ou titleannotations.description ou summary
payload.event_countissue.count1
payload.user_affected_countissue.userCount0
links.logs_queryLogQL 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.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.

On this page