# Spec 06 — Observabilidade desde o Dia Zero

> **Dono:** Core/Observabilidade · **Status:** Em execução · **Plano:** 06
> **Relaciona:** ADR-024, ADR-020, ADR-021, ADR-022
> **Objetivo:** contrato único de log, correlação e erro — **nativo** (Monolog +
> `X-Request-ID` + RFC 7807), com OpenTelemetry **integrado e pervasivo** (fail-open).

## 1. Log como contrato

Perfis por `APP_ENV` (ADR-032): `dev` loga em `debug` (request + query SQL), `homolog` em
`debug`, `production` em `info`. O nível é ajustável por `LOG_LEVEL`.

Todo log é **JSON estruturado**, sem texto livre, com os campos obrigatórios **sempre
presentes** (ausentes viram `null`, nunca omitidos):

| Campo | Origem | Exemplo |
|---|---|---|
| `ts` | RFC 3339 (UTC) | `2026-10-15T17:00:48+00:00` |
| `level` | Monolog (minúsculo) | `info`, `warning`, `error` |
| `request_id` | `RequestIdMiddleware` (UUIDv7) via `Context` | `...-7...` |
| `trace_id` | header W3C `traceparent` via `Context` (opcional) | 32 hex |
| `module` | contexto do evento | `Noticias` |
| `action` | contexto do evento/rota | `publish` / `GET /blueprint` |
| `user` | `auth()->id()` ou contexto | `...` |
| `duration_ms` | latência medida | `15.19` |
| `message` | nome do evento (não frase) | `http.request`, `noticia.publicada` |
| `context` | objeto livre | `{"status":200}` |

- **Canal:** `stack` (`json` + `daily`) é o **padrão**. `json` = `monolog` +
  `StreamHandler` (`php://stderr`) + `JsonFormatter` (observabilidade). `daily` = arquivo local
  `storage/logs/concordia-<data>.log` com **rotação diária e retenção de 730 dias**
  (`LOG_DAILY_DAYS`), também em JSON/redigido. Ajustável por `LOG_CHANNEL`/`LOG_STACK`.
- **Helper:** `log_event(string $message, array $context = [], string $level = 'info')`
  — fino, delega ao `Log` nativo (sem fachada paralela).
- **Log de domínio (convenção):** a mensagem é o **nome do evento** `{module}.{action}` e o contexto
  traz `module`/`action` (+ `id` quando houver) — **mesmos tokens** do span e da auditoria
  (ADR-033/035):
  ```php
  log_event('noticias.process_cover_media', [
      'module' => 'noticias', 'action' => 'process_cover_media', 'id' => $noticia->id,
  ]);
  ```
  Atributos `scalar|null`, **sem PII**. O log técnico é **efêmero**; a evidência forense vive na
  **trilha de auditoria** ([spec 14](14-auditoria.md)).
- **Por request:** `RequestIdMiddleware` emite `http.request` com `duration_ms`/`status`
  (nível `warning` se exceder `observability.slow_request_ms`); desligável com
  `LOG_REQUESTS=false`.

## 2. Correlação `X-Request-ID`

- Gera **UUIDv7** quando ausente; **preserva** um `X-Request-ID` recebido que case
  `^[A-Za-z0-9._-]{8,128}$` (correlação distribuída).
- Sempre devolvido no header da resposta e disponível no `Context` (entra em todo log).
- `trace_id` é derivado de `traceparent` (W3C) quando presente; sem ele, um trace-id de
  32 hex é gerado por request — o mesmo usado pelo log, pelo span OTLP e pelo RFC 7807.

## 3. Erro → RFC 7807

Handler global formata `application/problem+json` (`type`, `title`, `status`, `detail`,
`instance`, `trace_id`) para JSON/API. Para HTML, um renderer acessível independente do shell
do Portal (`resources/views/errors/problem.blade.php`) exibe o **código de rastreio**
(`X-Request-ID`). Ambos aplicam **redaction de PII** (`PiiRedactor`).
*(Entregue em `F1.I5.T2` — o shell do Portal assume o HTML em F3.)*

## 4. Telemetria (integrada, pervasiva, fail-open)

> Telemetria **não é opcional**: é capacidade de primeira classe do projeto e deve estar
> **permeada** por core, services e modules (ADR-024).

- Porta fina `Core\Contracts\Telemetry` (`span(name, attributes, callback)`). A implementação é
  escolhida por perfil: `NullTelemetry` (fallback no-op) ou `OtlpTelemetry`.
- Habilitando `TELEMETRY_ENABLED=true`, cada span é exportado via **OTLP/HTTP JSON nativo**
  (`ext-curl`, sem SDK pesado) para `<OTEL_EXPORTER_OTLP_ENDPOINT>/v1/traces`.
- **Fail-open:** qualquer falha de exportação é absorvida (a request segue). O **circuit breaker**
  fica em memória compartilhada **APCu** (todas as requisições/workers), com cooldown
  `OTEL_CIRCUIT_BREAKER_SECONDS`; timeout curto `OTEL_EXPORTER_OTLP_TIMEOUT_MS`.
- Perfil `telemetry` do compose sobe o Jaeger v2 (OTLP nativo) para inspeção local.

### 4.1. Pervasividade (convenção)

Toda operação relevante emite um **span** — a instrumentação acompanha o código, não a borda:

| Camada | Span | Onde |
|---|---|---|
| HTTP | `http.server` | `RequestIdMiddleware` (por request) |
| Core | `audit.persist`, `module.discovery`, `core.port`, `core.listen`, `widget.*`, `health.check` | `AuditService`, `ModuleDiscoveryService`, `Core` (mensageria), `InMemoryWidgetRegistry`, `DiagnosingHealthListener` |
| Services | `{service}.{operacao}` (ex.: `media.ingest`, `richtext.render`, `seo.metadata`, `security.hash`) | injectar `Telemetry` e envolver a operação (`MediaInputer`, `SecureMediaStorage`, `ContentASTParser`, `GenerateSEOMetadata`, `HashingService`, `EncryptionService`) |
| Modules | `{modulo}.{acao}` (ex.: `blueprint.saudacao`, `noticias.process_cover_media`) | Actions/Casos de uso (`Logic/Actions/*`) |

Regras:
- Depender **só** do contrato `Telemetry` (injeção); nunca instanciar `OtlpTelemetry` diretamente.
- Envolver **a operação de domínio** (não `new`/loops triviais); atributos só escalares (`scalar|null`).
- **Nunca** capturar PII em atributos (`PiiRedactor` vale para spans também).
- Determinismo: em testes usa-se `RecordingTelemetry` (registra spans, não exporta).
- Portas **triviais/quentes** (`Clock`, `IdGen`) **não** emitem span por operação (evita ruído); a
  instrumentação recai sobre operações de domínio, mensageria/portas e superfícies de saúde.
- **Toda** Action sob `{módulo}/Logic/Actions` injeta `Telemetry` e emite `{modulo}.{acao}`
  (convenção garantida por teste estrutural).
- Em **services** e **modules**, toda operação de domínio instrumentada emite **span e log** com o
  **mesmo** `{module}.{action}` (consumidores distintos: OTLP × stderr), garantido por teste
  estrutural. **Core** segue a exceção de ADR-024: o span é `{domain}.{op}`
  (ex.: `core.settings.write`, `audit.persist`) e o log/auditoria usa `{module}.{action}`
  (ex.: `core.settings.updated`); os dois nomes não precisam coincidir no Core.

## 5. Evidências

- `JsonFormatterTest`: todos os campos obrigatórios + `null` quando ausentes.
- `RequestIdMiddlewareTest`: UUIDv7 gerado/ecoando; `X-Request-ID` no `Context`; 1 evento
  `http.request` por request com `duration_ms`/`status`.
- `LogEventTest`: `log_event()` delega ao `Log` nativo e normaliza nível inválido.
- `PiiRedactTest`: CPF/CNPJ/e-mail/telefone/segredos e redação recursiva em arrays.
- `ProblemDetailsTest`: RFC 7807 com `trace_id` e detalhe redigido; `ErrorHtmlRenderTest`: HTML com `X-Request-ID`.
- Log de domínio: `ModulesTelemetryTest` (toda Action emite span **e** `log_event('{token}.…')`) e
  `ServicesTelemetryTest` (todo service que injeta `Telemetry` também emite `log_event`).
- Telemetria: `NullTelemetryTest` (transparente), `OtlpTelemetryTest` (payload, fail-open e breaker),
  `TelemetryFailOpenTest` (request 200 com coletor morto; `trace_id`/`X-Request-ID` correlacionados),
  `TelemetryPervasivenessTest` (core emite `module.discovery`; double propaga retorno/exceção) e
  `AuditTrailTest::it_emits_a_telemetry_span_when_auditing` (`audit.persist`).
- Services (`ServicesTelemetryTest`): `media.ingest`, `richtext.render`, `seo.metadata`,
  `security.hash`/`security.encrypt`; atributos só `scalar|null`.
- Módulos (`ModulesTelemetryTest`): runtime (`blueprint.saudacao`) **e** convenção estrutural —
  toda Action sob `Logic/Actions` injecta `Telemetry` e emite `{modulo}.{acao}`.
- Portas/mensageria + saúde (`CoreTelemetryTest`): `core.port`, `core.listen`, `widget.register`,
  `widget.get`, `health.check`; e o estado da telemetria (`TelemetryStatus`) exposto no health da API.
- `EnvironmentConfigTest`: perfil `testing` (debug, sem HTTPS, sem request/query log) e log
  `stack` (`json`+`daily`) com 730 dias.
- Manual no container: request → `{"ts":...,"request_id":"rid-json-123456","trace_id":"<32 hex>",...,"message":"http.request",...}`.
- Manual (perfil `telemetry`): Jaeger exibe o trace `abcd…` com span `http.server` (`url.path=/blueprint`).
- Manual (fail-open): com o coletor fora, 1ª request paga o timeout e as seguintes ficam em ~6ms (breaker APCu).
