@luanpdd/kit-mcp 1.8.1 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +39 -1
  2. package/gates/obs-agents-mcp-supabase.md +86 -0
  3. package/gates/obs-skills-frontmatter.md +76 -0
  4. package/gates/omm-no-regression.md +83 -0
  5. package/gates/skill-must-include.md +21 -19
  6. package/kit/agents/burn-rate-forecaster.md +160 -0
  7. package/kit/agents/incident-investigator.md +245 -0
  8. package/kit/agents/observability-instrumenter.md +200 -0
  9. package/kit/agents/omm-auditor.md +199 -0
  10. package/kit/agents/slo-engineer.md +224 -0
  11. package/kit/agents/supabase-architect.md +13 -0
  12. package/kit/agents/supabase-auth-bootstrapper.md +17 -0
  13. package/kit/agents/supabase-edge-fn-writer.md +22 -0
  14. package/kit/agents/supabase-migration-writer.md +18 -0
  15. package/kit/agents/supabase-realtime-implementer.md +23 -0
  16. package/kit/agents/supabase-rls-writer.md +17 -0
  17. package/kit/agents/supabase-storage-implementer.md +18 -0
  18. package/kit/commands/auditar-marco.md +22 -1
  19. package/kit/commands/auditar-observabilidade.md +103 -0
  20. package/kit/commands/burn-rate-status.md +140 -0
  21. package/kit/commands/concluir-marco.md +19 -1
  22. package/kit/commands/definir-slo.md +108 -0
  23. package/kit/commands/discutir-fase.md +26 -0
  24. package/kit/commands/forense.md +20 -1
  25. package/kit/commands/instrumentar-fase.md +200 -0
  26. package/kit/commands/investigar-producao.md +162 -0
  27. package/kit/commands/observabilidade.md +116 -0
  28. package/kit/commands/planejar-fase.md +20 -0
  29. package/kit/commands/verificar-trabalho.md +26 -0
  30. package/kit/skills/_shared-observability/glossary.md +396 -0
  31. package/kit/skills/burn-rate-alerting/SKILL.md +258 -0
  32. package/kit/skills/core-analysis-loop/SKILL.md +352 -0
  33. package/kit/skills/distributed-tracing/SKILL.md +362 -0
  34. package/kit/skills/event-based-slos/SKILL.md +274 -0
  35. package/kit/skills/observability-driven-development/SKILL.md +315 -0
  36. package/kit/skills/observability-maturity-model/SKILL.md +222 -0
  37. package/kit/skills/opentelemetry-standard/SKILL.md +351 -0
  38. package/kit/skills/structured-events/SKILL.md +265 -0
  39. package/kit/skills/telemetry-pipelines/SKILL.md +259 -0
  40. package/kit/skills/telemetry-sampling/SKILL.md +256 -0
  41. package/package.json +1 -1
@@ -0,0 +1,362 @@
1
+ ---
2
+ name: distributed-tracing
3
+ description: Use ao instrumentar tracing — trace_id/span_id/parent_id, propagar W3C TraceContext via header traceparent, stitching além de RPCs (batch, lambda, queue).
4
+ ---
5
+
6
+ # Observabilidade — Distributed Tracing
7
+
8
+ ## Quando usar
9
+
10
+ LLM carrega esta skill ao instrumentar tracing distribuído ou stitching de spans. Trigger phrases:
11
+
12
+ - "distributed tracing", "traces", "spans"
13
+ - "propagar contexto entre serviços", "trace cross-service"
14
+ - "W3C TraceContext", "traceparent header"
15
+ - "trace_id span_id parent_span_id"
16
+ - "ligar lambda batch job ao trace"
17
+ - "stitching de eventos"
18
+
19
+ ## Regras absolutas
20
+
21
+ - **trace_id é compartilhado** entre todos os spans de um único request distribuído. **NÃO** mude por hop.
22
+ - **span_id é único por span** — gere novo a cada `startSpan()`. 16 hex chars (8 bytes).
23
+ - **parent_span_id aponta para span pai** — null no root span. Define a árvore.
24
+ - **W3C TraceContext é o padrão** — header HTTP `traceparent: 00-{trace_id}-{span_id}-{flags}`. Adote sempre. B3 é fallback para legacy.
25
+ - **Propague ANTES de fazer call cross-service** — extrair contexto do request inbound, propagar no request outbound. Sem isso, trace quebra.
26
+ - **Stitching ≠ apenas RPC** — também batch jobs, queue messages, lambda invocations, S3 uploads. Carregue `traceparent` em metadata da queue, env var do lambda, header da Step Function.
27
+ - **Sample decision propaga** — bit `01` em flags de `traceparent` significa "sample=true". Decisão tomada no head propaga downstream.
28
+ - **Não invente trace_id** — sempre derive do contexto inbound ou gere via SDK (não `crypto.randomUUID()`).
29
+ - **Spans devem ter `kind`** — `SERVER` (handler de inbound), `CLIENT` (call outbound), `PRODUCER`/`CONSUMER` (queue), `INTERNAL` (subspan dentro do mesmo process).
30
+
31
+ ## Patterns canônicos
32
+
33
+ ### Pattern: extrair contexto inbound + propagar outbound (Node)
34
+
35
+ ```ts
36
+ // PT-BR: handler HTTP — extrai traceparent do request inbound, propaga em call outbound
37
+ import { trace, context, propagation } from '@opentelemetry/api'
38
+
39
+ const tracer = trace.getTracer('orders-service')
40
+
41
+ export async function placeOrder(req: Request) {
42
+ // PT-BR: 1 — extrair contexto inbound do header traceparent
43
+ const inboundContext = propagation.extract(context.active(), req.headers)
44
+
45
+ return tracer.startActiveSpan(
46
+ 'place_order',
47
+ { kind: SpanKind.SERVER },
48
+ inboundContext,
49
+ async (span) => {
50
+ span.setAttribute('user.id', req.user.id)
51
+
52
+ // PT-BR: 2 — fazer call outbound — propagation injeta traceparent automaticamente
53
+ // se você usar fetch/grpc instrumentados (ver skill opentelemetry-standard)
54
+ const outboundHeaders: Record<string, string> = {}
55
+ propagation.inject(context.active(), outboundHeaders)
56
+
57
+ const inventoryRes = await fetch('http://inventory/check', {
58
+ headers: outboundHeaders, // PT-BR: traceparent injetado aqui
59
+ body: JSON.stringify({ items: req.items })
60
+ })
61
+
62
+ span.end()
63
+ return inventoryRes.json()
64
+ }
65
+ )
66
+ }
67
+ ```
68
+
69
+ ### Pattern: traceparent format
70
+
71
+ ```text
72
+ traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
73
+ ^ ^ ^ ^
74
+ | | | |
75
+ version | flags (sampled bit)
76
+ trace_id (32 hex / 16 bytes) |
77
+ span_id (16 hex / 8 bytes)
78
+ ```
79
+
80
+ ```text
81
+ flags:
82
+ 01 = sampled (decisão upstream: capture este trace)
83
+ 00 = not sampled (decisão upstream: skip)
84
+ ```
85
+
86
+ ### Pattern: trace cross-service via Supabase Edge Function
87
+
88
+ ```ts
89
+ // PT-BR: Edge Function recebe request → propaga para outro service
90
+ import { trace, context, propagation } from 'npm:@opentelemetry/api@1.9.0'
91
+ import { W3CTraceContextPropagator } from 'npm:@opentelemetry/core@1.27.0'
92
+
93
+ propagation.setGlobalPropagator(new W3CTraceContextPropagator())
94
+
95
+ const tracer = trace.getTracer('edge-orders')
96
+
97
+ Deno.serve(async (req) => {
98
+ // PT-BR: extrair traceparent inbound
99
+ const inboundCtx = propagation.extract(context.active(), {
100
+ traceparent: req.headers.get('traceparent') ?? '',
101
+ })
102
+
103
+ return tracer.startActiveSpan(
104
+ 'edge_handler',
105
+ { kind: 1 /* SERVER */ },
106
+ inboundCtx,
107
+ async (span) => {
108
+ span.setAttribute('endpoint', new URL(req.url).pathname)
109
+
110
+ // PT-BR: call outbound para Postgres via PostgREST — injeta traceparent
111
+ const outHeaders: Record<string, string> = {}
112
+ propagation.inject(context.active(), outHeaders)
113
+
114
+ const dbRes = await fetch(Deno.env.get('SUPABASE_URL') + '/rest/v1/orders', {
115
+ method: 'POST',
116
+ headers: {
117
+ ...outHeaders,
118
+ 'apikey': Deno.env.get('SUPABASE_ANON_KEY')!,
119
+ 'content-type': 'application/json',
120
+ },
121
+ body: await req.text(),
122
+ })
123
+
124
+ span.setAttribute('db.status_code', dbRes.status)
125
+ span.end()
126
+ return dbRes
127
+ }
128
+ )
129
+ })
130
+ ```
131
+
132
+ ### Pattern: stitching além de RPC — queue message (não-RPC)
133
+
134
+ ```ts
135
+ // PT-BR: producer — anexa traceparent ao payload da queue (pgmq, SQS, RabbitMQ)
136
+ import { trace, context, propagation } from '@opentelemetry/api'
137
+
138
+ const tracer = trace.getTracer('producer')
139
+
140
+ export async function enqueueEmail(emailJob: EmailJob) {
141
+ return tracer.startActiveSpan(
142
+ 'enqueue_email',
143
+ { kind: SpanKind.PRODUCER },
144
+ async (span) => {
145
+ span.setAttribute('queue.name', 'emails')
146
+ span.setAttribute('email.recipient', emailJob.to)
147
+
148
+ // PT-BR: serializar contexto no payload da mensagem
149
+ const carrier: Record<string, string> = {}
150
+ propagation.inject(context.active(), carrier)
151
+
152
+ await pgmqEnqueue('emails', {
153
+ ...emailJob,
154
+ _trace_context: carrier, // PT-BR: viaja com o job
155
+ })
156
+
157
+ span.end()
158
+ }
159
+ )
160
+ }
161
+
162
+ // PT-BR: consumer — extrai traceparent do payload, continua o trace
163
+ export async function processEmailJob(job: EmailJobWithContext) {
164
+ const inboundCtx = propagation.extract(
165
+ context.active(),
166
+ job._trace_context ?? {} // PT-BR: se vazio, novo trace
167
+ )
168
+
169
+ return tracer.startActiveSpan(
170
+ 'process_email',
171
+ { kind: SpanKind.CONSUMER },
172
+ inboundCtx,
173
+ async (span) => {
174
+ span.setAttribute('email.recipient', job.to)
175
+ // PT-BR: agora o span do worker faz parte do mesmo trace do producer
176
+ await sendEmail(job)
177
+ span.end()
178
+ }
179
+ )
180
+ }
181
+ ```
182
+
183
+ ### Pattern: stitching de batch job (não-RPC)
184
+
185
+ ```ts
186
+ // PT-BR: cron job processa N items — 1 span por item, todos com mesmo trace_id
187
+ const tracer = trace.getTracer('billing-cron')
188
+
189
+ export async function dailyBillingJob() {
190
+ return tracer.startActiveSpan('daily_billing', async (rootSpan) => {
191
+ rootSpan.setAttribute('job.type', 'cron')
192
+ rootSpan.setAttribute('build_id', BUILD_ID)
193
+
194
+ const customers = await db.getCustomersDueForBilling()
195
+ rootSpan.setAttribute('customers.count', customers.length)
196
+
197
+ // PT-BR: cada customer vira span filho com mesmo trace_id
198
+ for (const customer of customers) {
199
+ await tracer.startActiveSpan(
200
+ 'bill_customer',
201
+ { kind: SpanKind.INTERNAL },
202
+ async (span) => {
203
+ span.setAttribute('customer.id', customer.id)
204
+ span.setAttribute('customer.tier', customer.tier)
205
+ try {
206
+ await chargeCustomer(customer)
207
+ span.setAttribute('result.success', true)
208
+ } catch (e) {
209
+ span.setAttribute('result.success', false)
210
+ span.setAttribute('error.type', classify(e))
211
+ } finally {
212
+ span.end()
213
+ }
214
+ }
215
+ )
216
+ }
217
+
218
+ rootSpan.end()
219
+ })
220
+ }
221
+ ```
222
+
223
+ ### Pattern: span kinds
224
+
225
+ | Kind | Quando usar | Exemplo |
226
+ |---|---|---|
227
+ | `SERVER` | Recebendo request inbound | Handler HTTP, gRPC server method |
228
+ | `CLIENT` | Fazendo call outbound | `fetch()`, gRPC client call, DB query |
229
+ | `PRODUCER` | Enviando msg para queue | `pgmq.enqueue()`, SQS publish |
230
+ | `CONSUMER` | Processando msg de queue | Worker recebendo job |
231
+ | `INTERNAL` | Subdivisão dentro do mesmo process | "json_parse", "validation_step" |
232
+
233
+ ### Pattern: query traces — montar waterfall
234
+
235
+ ```sql
236
+ -- PT-BR: pegar todos os spans de um trace em ordem cronológica
237
+ select
238
+ span_id,
239
+ parent_span_id,
240
+ span_name,
241
+ span_kind,
242
+ service_name,
243
+ duration_ms,
244
+ start_time
245
+ from observability.spans
246
+ where trace_id = '4bf92f3577b34da6a3ce929d0e0e4736'
247
+ order by start_time asc;
248
+
249
+ -- PT-BR: encontrar root span — parent_span_id IS NULL ou span sem parent no mesmo trace
250
+ select *
251
+ from observability.spans
252
+ where trace_id = '4bf92f3577b34da6a3ce929d0e0e4736'
253
+ and parent_span_id is null;
254
+
255
+ -- PT-BR: spans mais lentos cross-trace, último 1h
256
+ select
257
+ service_name,
258
+ span_name,
259
+ percentile_cont(0.99) within group (order by duration_ms) as p99,
260
+ count(*) as samples
261
+ from observability.spans
262
+ where start_time > now() - interval '1 hour'
263
+ group by service_name, span_name
264
+ having count(*) > 100
265
+ order by p99 desc
266
+ limit 20;
267
+ ```
268
+
269
+ ## Anti-patterns
270
+
271
+ ### ANTI: gerar trace_id por hop
272
+
273
+ ```ts
274
+ // PT-BR: BAD — quebra a cadeia, cada service vê trace diferente
275
+ const traceId = crypto.randomUUID().replace(/-/g, '').slice(0, 32)
276
+
277
+ // PT-BR: GOOD — extrair do header inbound; deixar SDK gerar root
278
+ const inboundCtx = propagation.extract(context.active(), req.headers)
279
+ tracer.startActiveSpan('handler', {}, inboundCtx, ...)
280
+ ```
281
+
282
+ ### ANTI: esquecer de propagar em call outbound
283
+
284
+ ```ts
285
+ // PT-BR: BAD — outbound call sem traceparent — trace quebra no service B
286
+ await fetch('http://service-b/api', { body: ... })
287
+
288
+ // PT-BR: GOOD — injetar traceparent
289
+ const headers: Record<string, string> = {}
290
+ propagation.inject(context.active(), headers)
291
+ await fetch('http://service-b/api', { headers, body: ... })
292
+ ```
293
+
294
+ ### ANTI: trace só de RPCs, não de batch/queue
295
+
296
+ ```ts
297
+ // PT-BR: BAD — producer/consumer não compartilham trace, debug fica fragmentado
298
+ await pgmqEnqueue('emails', payload) // sem trace context
299
+ // ... depois worker processa sem saber que veio do request X
300
+
301
+ // PT-BR: GOOD — propagar contexto via metadata da queue
302
+ const carrier = {}
303
+ propagation.inject(context.active(), carrier)
304
+ await pgmqEnqueue('emails', { ...payload, _trace_context: carrier })
305
+ ```
306
+
307
+ ### ANTI: span sem `end()`
308
+
309
+ ```ts
310
+ // PT-BR: BAD — span fica aberto forever, duration_ms não calculado, memory leak
311
+ const span = tracer.startSpan('handler')
312
+ // ... handler logic
313
+ return result // PT-BR: ESQUECEU span.end()
314
+
315
+ // PT-BR: GOOD — sempre `try/finally`
316
+ const span = tracer.startSpan('handler')
317
+ try {
318
+ // ... logic
319
+ } finally {
320
+ span.end()
321
+ }
322
+ ```
323
+
324
+ ### ANTI: span hierarchy errada
325
+
326
+ ```ts
327
+ // PT-BR: BAD — usar startSpan sem startActiveSpan, parent não é settado automático
328
+ const parent = tracer.startSpan('parent')
329
+ const child = tracer.startSpan('child') // PT-BR: parent_span_id ficou null
330
+ parent.end()
331
+ child.end()
332
+
333
+ // PT-BR: GOOD — startActiveSpan empurra contexto, child herda parent
334
+ tracer.startActiveSpan('parent', (parent) => {
335
+ tracer.startActiveSpan('child', (child) => {
336
+ // PT-BR: child.parent_span_id === parent.span_id
337
+ child.end()
338
+ })
339
+ parent.end()
340
+ })
341
+ ```
342
+
343
+ ## Verificação
344
+
345
+ 1. **1 trace_id por request** — enviar 1 request, queryar `SELECT DISTINCT trace_id FROM spans WHERE request_id = X` → 1 resultado.
346
+ 2. **Cross-service stitching** — request HTTP service A → service B → DB. Queryar `SELECT count(distinct service_name) FROM spans WHERE trace_id = X` → ≥ 3.
347
+ 3. **Root span identificável** — `SELECT * FROM spans WHERE trace_id = X AND parent_span_id IS NULL` → 1 row (o root).
348
+ 4. **Span hierarchy correta** — graficar via tool (Jaeger UI, Honeycomb, etc.) ou recursivo SQL — deve formar árvore válida (sem ciclos).
349
+ 5. **Duration não-zero** — `SELECT min(duration_ms), max(duration_ms) FROM spans` — min ≥ 0, max razoável.
350
+ 6. **Sampled flag respeitado** — verificar que se traceparent inbound = `01`, downstream também sample=true.
351
+ 7. **Queue stitching funciona** — enqueue + consume → mesmo `trace_id` em ambos os spans.
352
+
353
+ ---
354
+
355
+ ## Ver também
356
+
357
+ - `kit/skills/_shared-observability/glossary.md` — W3C TraceContext, B3, span kinds
358
+ - `kit/skills/structured-events/SKILL.md` — atributos canônicos por span
359
+ - `kit/skills/opentelemetry-standard/SKILL.md` — SDK que faz extract/inject
360
+ - `kit/skills/telemetry-sampling/SKILL.md` *(Phase 34)* — head vs tail sampling decisão
361
+
362
+ *Material-fonte: Observability Engineering (O'Reilly, 2022) — Cap 6: "Stitching Events into Traces".*
@@ -0,0 +1,274 @@
1
+ ---
2
+ name: event-based-slos
3
+ description: Use ao definir SLO — SLI event-based (não time-based), sliding window 30d, decouple what/why. SLO-based alerts substituem thresholds brutos como CPU/memória.
4
+ ---
5
+
6
+ # Observabilidade — Event-Based SLOs
7
+
8
+ ## Quando usar
9
+
10
+ LLM carrega esta skill ao definir/avaliar SLOs ou substituir alertas threshold por SLO-based. Trigger phrases:
11
+
12
+ - "definir SLO", "criar SLI"
13
+ - "alertas confiáveis", "alert fatigue"
14
+ - "error budget", "sliding window"
15
+ - "como medir saúde do serviço"
16
+ - "decouple what from why"
17
+
18
+ ## Regras absolutas
19
+
20
+ - **SLI sempre event-based, nunca time-based** — "% de eventos com `result.success=true` em 30d" > "% de janelas de 5min com p99 < 300ms"
21
+ - **Sliding window 30d por default** — fixed window (calendário) gera comportamento perverso (cliente não esquece bug por causa de reset).
22
+ - **Target ≤ 99.95%** — para SLO 99.99%+ você não tem tempo de reagir antes do budget acabar; use métricas/dashboards informativos em vez de SLO.
23
+ - **Decouple "what" do "why"** — SLO alert diz que tem dor (sintoma); investigation descobre porquê (use [`core-analysis-loop`](../skills/core-analysis-loop/SKILL.md)). NUNCA misturar (anti-pattern: "alert se memória > 80% AND p99 > 300ms").
24
+ - **Customer-facing journey, não system metric** — SLI mede o que o cliente sente ("login funcionou em < 800ms"), não estado interno ("threads ativas").
25
+ - **Granular por endpoint/feature** — 1 SLO por jornada crítica do user. Não SLO global "site availability" — específico demais para ser ignorável.
26
+ - **Owner explícito** — cada SLO tem dono nomeado. Sem owner = sem ação = sem valor.
27
+ - **Substituir alertas threshold gradualmente** — após SLO comprovar valor (1+ incident detectado por SLO antes de threshold), DELETAR threshold antigo.
28
+
29
+ ## Patterns canônicos
30
+
31
+ ### Pattern: SLI event-based vs time-based
32
+
33
+ ```sql
34
+ -- PT-BR: BAD — SLI time-based (anti-pattern)
35
+ -- "99% das janelas de 5 min têm p99 < 300ms"
36
+ -- Problema: pre-aggregation perde fidelidade; janela com 1 outlier puxa p99.
37
+
38
+ -- PT-BR: GOOD — SLI event-based
39
+ -- "99.9% dos eventos individuais têm duration < 300ms e result_success = true"
40
+ select
41
+ count(*) filter (where duration_ms < 300 and result_success = true) as good,
42
+ count(*) as total,
43
+ count(*) filter (where duration_ms < 300 and result_success = true)::float / count(*) as compliance
44
+ from observability.events
45
+ where
46
+ event_name = 'http_request'
47
+ and endpoint = '/api/v1/orders'
48
+ and timestamp > now() - interval '30 days';
49
+ ```
50
+
51
+ ### Pattern: SLO definition canônico
52
+
53
+ ```yaml
54
+ # PT-BR: SLO documentado em formato YAML — alimenta agent slo-engineer
55
+ slo:
56
+ name: checkout_success
57
+ description: "Checkout completes successfully within 800ms (customer perception)"
58
+ owner: orders-team@company.com # PT-BR: dono nomeado
59
+
60
+ sli:
61
+ type: event_based # PT-BR: NUNCA time-based
62
+ event_filter:
63
+ service: orders-api
64
+ endpoint: /api/v1/checkout
65
+ http_method: POST
66
+ good_event: # PT-BR: predicate booleano
67
+ result_success: true
68
+ duration_ms: { lt: 800 }
69
+ bad_event: # PT-BR: complemento
70
+ operator: not_good # qualquer evento que não é "good"
71
+
72
+ target: 0.999 # PT-BR: 99.9% — não 99.99%+ por design
73
+ window: 30d_sliding # PT-BR: nunca fixed/calendar
74
+
75
+ alerts: # PT-BR: ver skill burn-rate-alerting
76
+ - name: page_short_term
77
+ lookahead: 4h
78
+ baseline: 1h
79
+ severity: page
80
+ - name: ticket_long_term
81
+ lookahead: 3d
82
+ baseline: 18h
83
+ severity: ticket
84
+ ```
85
+
86
+ ### Pattern: SLI materialized view (Postgres)
87
+
88
+ ```sql
89
+ -- PT-BR: view materializa SLI events para queries baratas
90
+ -- Refresh agendado (pg_cron) ou em tempo real (trigger)
91
+ create materialized view obs.sli_checkout_success as
92
+ select
93
+ date_trunc('minute', timestamp) as bucket,
94
+ count(*) filter (where result_success = true and duration_ms < 800) as good,
95
+ count(*) filter (where not (result_success = true and duration_ms < 800)) as bad,
96
+ count(*) as total
97
+ from observability.events
98
+ where
99
+ service = 'orders-api'
100
+ and endpoint = '/api/v1/checkout'
101
+ and http_method = 'POST'
102
+ and timestamp > now() - interval '35 days' -- 30d + buffer
103
+ group by 1
104
+ with no data;
105
+
106
+ -- PT-BR: índice para queries de burn rate
107
+ create index on obs.sli_checkout_success (bucket);
108
+
109
+ -- PT-BR: refresh schedule via pg_cron — a cada 30s
110
+ select cron.schedule(
111
+ 'refresh_sli_checkout_success',
112
+ '*/30 * * * * *',
113
+ $$ refresh materialized view concurrently obs.sli_checkout_success $$
114
+ );
115
+ ```
116
+
117
+ ### Pattern: SLO compliance query (atual e histórico)
118
+
119
+ ```sql
120
+ -- PT-BR: compliance atual — % good no último 30d
121
+ select
122
+ sum(good)::float / nullif(sum(total), 0) as compliance_30d,
123
+ 0.999 as target,
124
+ case
125
+ when sum(good)::float / nullif(sum(total), 0) >= 0.999 then 'IN_BUDGET'
126
+ else 'OUT_OF_BUDGET'
127
+ end as status
128
+ from obs.sli_checkout_success
129
+ where bucket > now() - interval '30 days';
130
+
131
+ -- PT-BR: error budget remaining
132
+ -- Budget = (1 - target) × total_events
133
+ -- Remaining = budget - bad_events_so_far
134
+ select
135
+ (1 - 0.999) * sum(total) as budget,
136
+ sum(bad) as burned,
137
+ (1 - 0.999) * sum(total) - sum(bad) as remaining,
138
+ 100.0 * (1 - sum(bad) / nullif((1 - 0.999) * sum(total), 0)) as remaining_pct
139
+ from obs.sli_checkout_success
140
+ where bucket > now() - interval '30 days';
141
+ ```
142
+
143
+ ### Pattern: SLO replacing thresholds (case study Honeycomb)
144
+
145
+ ```text
146
+ ANTES (cap 12 do livro):
147
+ - Alert: CPU > 80% (false positive: garbage collector)
148
+ - Alert: memory > 90% (false positive: cache warming)
149
+ - Alert: 5xx > 1% in 5min (false negative: 0.5% por 1h burns 60% do budget)
150
+ - Alert: p99 latency > 500ms in 5min (false positive: 1 spike isolado)
151
+
152
+ DEPOIS:
153
+ - 1 SLO: checkout_success em 30d sliding
154
+ - 1 alert preditivo: burn rate sustained 4h+
155
+ - 1 alert ticket: burn rate sustained 3d+
156
+
157
+ RESULTADO: 60% menos paginations, 100% dos incidents reais detectados.
158
+ ```
159
+
160
+ ### Pattern: customer-facing SLI dimensions
161
+
162
+ | Dimensão | Valor | Por quê SLI deve incluir |
163
+ |---|---|---|
164
+ | `endpoint` | `/api/v1/checkout` | Granular — não SLO global |
165
+ | `customer.tier` | `'enterprise'` | Diferentes targets por tier (Pro = 99.95% vs Free = 99.5%) |
166
+ | `region` | `us-east-1` | Identificar problema regional vs global |
167
+ | `feature_flag.<name>` | `true`/`false` | SLO durante rollout incremental |
168
+ | `tenant_id` | `'acme'` | Big customers podem ter SLOs próprios |
169
+
170
+ ## Anti-patterns
171
+
172
+ ### ANTI: SLO 99.99% ou 99.999%
173
+
174
+ ```text
175
+ ANTI: target 99.99% em SLO de 30d sliding
176
+ - 30d × 24h × 60min × (1-0.9999) = 4.3 minutos de tolerância
177
+ - Sem tempo para reagir antes do budget esgotar
178
+ - Burn rate alerts disparam após o budget acabar (zero-level)
179
+
180
+ CERTO: target ≤ 99.95% para SLO real
181
+ Para 99.99%+, use métricas/dashboards informativos (não alerta)
182
+ ```
183
+
184
+ ### ANTI: SLO global "site up"
185
+
186
+ ```text
187
+ ANTI: 1 SLO "site availability" para tudo
188
+ - Falha em /api/v1/admin não conta para 99% dos clientes
189
+ - Falha em /api/v1/checkout = catastrófico
190
+ - Misturar = alarmes confusos, ações vagas
191
+
192
+ CERTO: 1 SLO por jornada crítica do user
193
+ - checkout_success: 99.9%
194
+ - login_success: 99.95%
195
+ - search_p95_latency: 99% < 200ms
196
+ - admin_panel: SEM SLO (uso baixo, latência aceitável)
197
+ ```
198
+
199
+ ### ANTI: SLO sem owner
200
+
201
+ ```text
202
+ ANTI: SLO definido em retrospectiva, sem dono nomeado
203
+ - Burn alert dispara → ninguém atende → escalation
204
+ - Sem follow-up no fim do mês
205
+
206
+ CERTO: SLO tem owner em arquivo (yaml ou tabela DB)
207
+ owner = team email ou pessoa específica
208
+ Burn alert roteia direto para owner antes de escalation
209
+ ```
210
+
211
+ ### ANTI: SLO == SLA externo
212
+
213
+ ```text
214
+ ANTI: usar SLA do contrato (99.9% uptime) como SLO interno
215
+
216
+ PROBLEMA: 0 margem de segurança. Atinge SLA mínimo no fio = 1 incident e quebra.
217
+
218
+ CERTO: SLO interno mais rígido que SLA externo
219
+ SLA externo: 99.9% (compromisso com cliente)
220
+ SLO interno: 99.95% (margem de 5× para reagir)
221
+ ```
222
+
223
+ ### ANTI: alterar SLI quando burn ocorre
224
+
225
+ ```text
226
+ ANTI: SLO está queimando → "vamos relaxar SLI para reduzir false positives"
227
+ (definir bad_event mais frouxo)
228
+
229
+ PROBLEMA: você está mascarando dor real. Próximo incident similar passa silencioso.
230
+
231
+ CERTO: SLI é compromisso com customer experience. Se está queimando, fixar
232
+ o problema (root cause via core-analysis-loop), não o SLI.
233
+ Se SLI sistematicamente errado (mede coisa errada): substituir, não relaxar.
234
+ ```
235
+
236
+ ### ANTI: fixed window (mensal/calendário)
237
+
238
+ ```text
239
+ ANTI: error budget reseta dia 1 do mês
240
+
241
+ PROBLEMA:
242
+ - "Tivemos outage dia 31, reseta amanhã" — cliente NÃO esquece
243
+ - Pressão para postergar fixes para "depois do reset"
244
+ - Behavioral hazard: deploy arriscado dia 30
245
+
246
+ CERTO: sliding window 30d
247
+ Outage dia 31 fica no budget até dia 30 do mês seguinte (sai gradualmente)
248
+ Sem incentivo perverso, comportamento humano realista
249
+ ```
250
+
251
+ ## Verificação
252
+
253
+ Antes de marcar SLO como produção-pronto:
254
+
255
+ 1. **Owner nomeado** — email/team em `slo.owner`
256
+ 2. **SLI event-based** — pred boolean, não time-bucket
257
+ 3. **Target ≤ 99.95%** — > 99.95% sinaliza informativo, não SLO
258
+ 4. **Window 30d sliding** — não fixed
259
+ 5. **Customer-facing journey** — SLI mede o que o cliente sente
260
+ 6. **Materialized view** existe e é refreshable (pg_cron)
261
+ 7. **Burn alerts** configurados (ver skill `burn-rate-alerting`)
262
+ 8. **Quero SLO ou metric?** — se a resposta é "informativo", crie metric, não SLO
263
+
264
+ ---
265
+
266
+ ## Ver também
267
+
268
+ - `kit/skills/_shared-observability/glossary.md` — termos canônicos SLI/SLO/error budget
269
+ - `kit/skills/structured-events/SKILL.md` — eventos canônicos para alimentar SLI
270
+ - `kit/skills/burn-rate-alerting/SKILL.md` — lookahead/baseline windows
271
+ - `kit/skills/core-analysis-loop/SKILL.md` — investigar quando SLO queima
272
+ - `kit/agents/slo-engineer.md` — gera SLO.md + SQL para materializar SLI
273
+
274
+ *Material-fonte: Observability Engineering (O'Reilly, 2022) — Cap 12: "Using Service-Level Objectives for Reliability".*