@luanpdd/kit-mcp 1.8.1 → 1.10.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.
- package/CHANGELOG.md +86 -0
- package/README.md +97 -1
- package/gates/golden-signals-coverage.md +133 -0
- package/gates/obs-agents-mcp-supabase.md +86 -0
- package/gates/obs-skills-frontmatter.md +76 -0
- package/gates/omm-no-regression.md +83 -0
- package/gates/postmortem-template-required.md +127 -0
- package/gates/prr-checklist-coverage.md +128 -0
- package/gates/skill-must-include.md +21 -19
- package/kit/agents/burn-rate-forecaster.md +160 -0
- package/kit/agents/golden-signals-instrumenter.md +241 -0
- package/kit/agents/incident-investigator.md +245 -0
- package/kit/agents/observability-instrumenter.md +200 -0
- package/kit/agents/omm-auditor.md +251 -0
- package/kit/agents/postmortem-writer.md +282 -0
- package/kit/agents/prr-conductor.md +288 -0
- package/kit/agents/slo-engineer.md +224 -0
- package/kit/agents/supabase-architect.md +62 -0
- package/kit/agents/supabase-auth-bootstrapper.md +17 -0
- package/kit/agents/supabase-edge-fn-writer.md +124 -0
- package/kit/agents/supabase-migration-writer.md +98 -0
- package/kit/agents/supabase-realtime-implementer.md +23 -0
- package/kit/agents/supabase-rls-writer.md +17 -0
- package/kit/agents/supabase-storage-implementer.md +174 -0
- package/kit/agents/toil-auditor.md +277 -0
- package/kit/commands/auditar-marco.md +102 -1
- package/kit/commands/auditar-observabilidade.md +103 -0
- package/kit/commands/auditar-toil.md +129 -0
- package/kit/commands/burn-rate-status.md +140 -0
- package/kit/commands/concluir-marco.md +73 -1
- package/kit/commands/definir-slo.md +108 -0
- package/kit/commands/discutir-fase.md +26 -0
- package/kit/commands/forense.md +83 -1
- package/kit/commands/golden-signals.md +142 -0
- package/kit/commands/instrumentar-fase.md +200 -0
- package/kit/commands/investigar-producao.md +162 -0
- package/kit/commands/observabilidade.md +116 -0
- package/kit/commands/planejar-fase.md +20 -0
- package/kit/commands/postmortem.md +179 -0
- package/kit/commands/prr.md +205 -0
- package/kit/commands/risk-budget.md +220 -0
- package/kit/commands/sre.md +227 -0
- package/kit/commands/verificar-trabalho.md +26 -0
- package/kit/skills/_shared-observability/glossary.md +396 -0
- package/kit/skills/_shared-sre/glossary.md +573 -0
- package/kit/skills/blameless-postmortems/SKILL.md +340 -0
- package/kit/skills/burn-rate-alerting/SKILL.md +258 -0
- package/kit/skills/core-analysis-loop/SKILL.md +352 -0
- package/kit/skills/distributed-tracing/SKILL.md +362 -0
- package/kit/skills/eliminating-toil/SKILL.md +243 -0
- package/kit/skills/event-based-slos/SKILL.md +296 -0
- package/kit/skills/four-golden-signals/SKILL.md +297 -0
- package/kit/skills/observability-driven-development/SKILL.md +315 -0
- package/kit/skills/observability-maturity-model/SKILL.md +222 -0
- package/kit/skills/opentelemetry-standard/SKILL.md +351 -0
- package/kit/skills/production-readiness-review/SKILL.md +305 -0
- package/kit/skills/sre-risk-management/SKILL.md +221 -0
- package/kit/skills/structured-events/SKILL.md +265 -0
- package/kit/skills/telemetry-pipelines/SKILL.md +259 -0
- package/kit/skills/telemetry-sampling/SKILL.md +256 -0
- package/package.json +1 -1
|
@@ -178,8 +178,132 @@ Test local:
|
|
|
178
178
|
- Função existente que precisa de pequeno ajuste → use Edit direto
|
|
179
179
|
- Lógica que pode rodar em DB function (`security definer`) → considera `supabase-database-functions` (mais barato que Edge)
|
|
180
180
|
|
|
181
|
+
## Observabilidade integrada
|
|
182
|
+
|
|
183
|
+
Edge Function nasce instrumentada com OTel — não é addon. Beneficia mais que qualquer outro agent dado que é entry-point externo.
|
|
184
|
+
|
|
185
|
+
1. **OTel SDK no topo do `index.ts`** (skill [`opentelemetry-standard`](../skills/opentelemetry-standard/SKILL.md)):
|
|
186
|
+
```ts
|
|
187
|
+
import { trace } from 'npm:@opentelemetry/api@1.9.0'
|
|
188
|
+
import { NodeSDK } from 'npm:@opentelemetry/sdk-node@0.55.0'
|
|
189
|
+
import { OTLPTraceExporter } from 'npm:@opentelemetry/exporter-trace-otlp-http@0.55.0'
|
|
190
|
+
const sdk = new NodeSDK({ /* service.name, OTLP endpoint */ })
|
|
191
|
+
sdk.start()
|
|
192
|
+
```
|
|
193
|
+
2. **Span por handler** com kind `SERVER` envolvendo `Deno.serve`. Atributos canônicos: `request.id`, `user.id`, `tenant_id`, `endpoint`, `result.success`, `error.type`, `build_id` (`Deno.env.get('SUPABASE_GIT_SHA')`) — skill [`structured-events`](../skills/structured-events/SKILL.md).
|
|
194
|
+
3. **Context propagation** via header `traceparent` para outbound calls a Postgres/PostgREST/external (skill [`distributed-tracing`](../skills/distributed-tracing/SKILL.md)).
|
|
195
|
+
4. **Sampling head-based** baseado em `customer.tier` ou `feature_flag.<name>` (skill [`telemetry-sampling`](../skills/telemetry-sampling/SKILL.md) *Phase 34*) — 100% errors, 100% enterprise, 10% baseline.
|
|
196
|
+
|
|
197
|
+
**Output adicionado:** template completo de Edge Function inclui SDK setup + span wrapper + propagação outbound + classificador de error.type. ODD-compliant (4 perguntas pré-PR endereçadas).
|
|
198
|
+
|
|
199
|
+
## Four Golden Signals
|
|
200
|
+
|
|
201
|
+
> Cross-ref canônico: [four-golden-signals](../skills/four-golden-signals/SKILL.md) (cap 6 do livro Google SRE — Monitoring Distributed Systems). Para retro-instrumentar Edge Function existente, delegar para [golden-signals-instrumenter](./golden-signals-instrumenter.md).
|
|
202
|
+
|
|
203
|
+
Edge Function user-facing nasce com os 4 sinais dourados — não é addon. O bloco `## Observabilidade integrada` acima cobre OTel SDK + spans + propagation; este bloco especifica os **4 instrumentos canônicos** que o template gerado SEMPRE inclui:
|
|
204
|
+
|
|
205
|
+
| Signal | Instrumento | Dimensão | Valor padrão |
|
|
206
|
+
|---|---|---|---|
|
|
207
|
+
| **Latency** | `meter.createHistogram('http_request_duration_ms')` com `explicitBucketBoundaries: [1,2,5,10,25,50,100,250,500,1000,2500,5000,10000,30000]` | `result=success\|error` (separar success de erro) | Bucketing exponencial captura long tail sem cardinality explosion |
|
|
208
|
+
| **Traffic** | `meter.createCounter('http_requests_total')` | `endpoint`, `http_method` | Incrementado antes de processar request |
|
|
209
|
+
| **Errors** | `meter.createCounter('http_errors_total')` | `error.type` enum (5-15 valores: `timeout\|validation\|auth\|rate_limit\|db\|provider_down\|...`) — **nunca** `error.message` (cardinalidade explode) | Incrementado em catch + path 4xx/5xx |
|
|
210
|
+
| **Saturation** | `meter.createObservableGauge('saturation_pct')` com callback que lê estado real | resource-specific: `connection_pool` (pg) / `concurrency_limit` (Edge runtime) / `egress_bandwidth` / `cache_memory` | % do recurso mais escasso identificado ANTES de instrumentar |
|
|
211
|
+
|
|
212
|
+
### Snippet canônico — adicionado ao topo do `index.ts` gerado
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
// PT-BR: 4 golden signals — instrumentação mínima universal
|
|
216
|
+
import { metrics } from 'npm:@opentelemetry/api@1.9.0'
|
|
217
|
+
const meter = metrics.getMeter('<function_name>')
|
|
218
|
+
|
|
219
|
+
// 1. LATENCY — histogram bucketed exponencial
|
|
220
|
+
const latencyHistogram = meter.createHistogram('http_request_duration_ms', {
|
|
221
|
+
description: 'Edge function latency split by result (success vs error)',
|
|
222
|
+
unit: 'ms',
|
|
223
|
+
advice: { explicitBucketBoundaries: [1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000] }
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
// 2. TRAFFIC — counter de requests recebidos
|
|
227
|
+
const trafficCounter = meter.createCounter('http_requests_total', {
|
|
228
|
+
description: 'Total HTTP requests received by edge function'
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
// 3. ERRORS — counter por error.type (NUNCA error.message — cardinalidade)
|
|
232
|
+
const errorsCounter = meter.createCounter('http_errors_total', {
|
|
233
|
+
description: 'Edge function errors by error.type enum'
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
// 4. SATURATION — gauge do recurso mais escasso (callback lê estado real)
|
|
237
|
+
// PT-BR: para Edge Function default, saturation = concurrency_limit_used %
|
|
238
|
+
// Substituir callback conforme recurso identificado (db pool, queue, cache)
|
|
239
|
+
meter.createObservableGauge('saturation_pct', {
|
|
240
|
+
description: 'Saturation of scarcest resource — function-specific'
|
|
241
|
+
}).addCallback((result) => {
|
|
242
|
+
// PT-BR: callback canônico — ler estado real (ex: SELECT count(*) FROM pg_stat_activity)
|
|
243
|
+
// Aqui placeholder: 0 < value < 1
|
|
244
|
+
result.observe(getSaturationPct()) // implementar conforme resource
|
|
245
|
+
})
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Wrapping no handler
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
Deno.serve(async (req: Request) => {
|
|
252
|
+
const start = performance.now()
|
|
253
|
+
const endpoint = new URL(req.url).pathname
|
|
254
|
+
trafficCounter.add(1, { endpoint, http_method: req.method })
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const response = await handle(req)
|
|
258
|
+
latencyHistogram.record(performance.now() - start, {
|
|
259
|
+
endpoint,
|
|
260
|
+
result: response.ok ? 'success' : 'error',
|
|
261
|
+
})
|
|
262
|
+
if (!response.ok) {
|
|
263
|
+
errorsCounter.add(1, { endpoint, 'error.type': classifyError(response) })
|
|
264
|
+
}
|
|
265
|
+
return response
|
|
266
|
+
} catch (err) {
|
|
267
|
+
latencyHistogram.record(performance.now() - start, { endpoint, result: 'error' })
|
|
268
|
+
errorsCounter.add(1, { endpoint, 'error.type': classifyError(err) })
|
|
269
|
+
throw err
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
// PT-BR: classifyError DEVE retornar enum fechado, não err.message
|
|
274
|
+
function classifyError(e: unknown): string {
|
|
275
|
+
if (e instanceof TimeoutError) return 'timeout'
|
|
276
|
+
if (e instanceof ValidationError) return 'validation'
|
|
277
|
+
if (e instanceof AuthError) return 'auth'
|
|
278
|
+
// ... 5-15 valores no total
|
|
279
|
+
return 'unknown'
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Saturation por tipo de Edge Function
|
|
284
|
+
|
|
285
|
+
| Tipo de função | Recurso mais escasso | Implementação típica |
|
|
286
|
+
|---|---|---|
|
|
287
|
+
| API simples (GET/POST com leitura DB) | `pg_pool` connections used | `select count(*) from pg_stat_activity where state = 'active'` |
|
|
288
|
+
| RAG / embeddings | `concurrency_limit` (provider externo) | counter de requests in-flight |
|
|
289
|
+
| Email / queue consumer (cron → pgmq) | `pgmq.queue_length` | `select msg_count from pgmq.metrics_<queue>` |
|
|
290
|
+
| Storage I/O heavy (uploads grandes) | `egress_bandwidth` | bytes-out tracker em window |
|
|
291
|
+
|
|
292
|
+
### Anti-patterns prevenidos
|
|
293
|
+
|
|
294
|
+
- Errors counter usando `error.type = err.message` → SEMPRE enum fechado (5-15 valores)
|
|
295
|
+
- Latency mistura success + error → SEMPRE `result` dimension separa
|
|
296
|
+
- Mean latency em vez de histogram → SEMPRE histogram com percentis derivados em backend
|
|
297
|
+
- Saturation genérico (CPU%) sem identificar recurso real → SEMPRE escolher recurso scarcest da função
|
|
298
|
+
|
|
181
299
|
## Ver também
|
|
182
300
|
|
|
183
301
|
- [supabase-edge-functions](../skills/supabase-edge-functions/SKILL.md) — base de conhecimento canônica
|
|
184
302
|
- [supabase-cron-queues](../skills/supabase-cron-queues/SKILL.md) — pattern `cron → pgmq → Edge Function`
|
|
185
303
|
- [supabase-auth-ssr](../skills/supabase-auth-ssr/SKILL.md) — clients Supabase
|
|
304
|
+
- [opentelemetry-standard](../skills/opentelemetry-standard/SKILL.md) — SDK setup para Deno
|
|
305
|
+
- [distributed-tracing](../skills/distributed-tracing/SKILL.md) — context propagation
|
|
306
|
+
- [structured-events](../skills/structured-events/SKILL.md) — campos canônicos
|
|
307
|
+
- [observability-driven-development](../skills/observability-driven-development/SKILL.md) — 4 perguntas pré-PR
|
|
308
|
+
- [four-golden-signals](../skills/four-golden-signals/SKILL.md) — 4 sinais canônicos (Latency, Traffic, Errors, Saturation) cap 6 livro Google SRE
|
|
309
|
+
- [golden-signals-instrumenter](./golden-signals-instrumenter.md) — agent que retro-instrumenta Edge Functions existentes com os 4 signals
|
|
@@ -154,3 +154,101 @@ Próximos passos:
|
|
|
154
154
|
- `auth.uid()` sem `(select)` → SEMPRE wrapper
|
|
155
155
|
- Schema-qualifier ausente em DB functions → SEMPRE `public.<name>`
|
|
156
156
|
- Comandos destrutivos sem comentário → BLOQUEIA até user adicionar Risk/Validation/Rollback
|
|
157
|
+
|
|
158
|
+
## Observabilidade integrada
|
|
159
|
+
|
|
160
|
+
Toda migration emite evento estruturado e cria audit hooks por default — não é addon, é parte do contrato (skill [`observability-driven-development`](../skills/observability-driven-development/SKILL.md)).
|
|
161
|
+
|
|
162
|
+
1. **Migration event** (auto-gerado no fim da migration):
|
|
163
|
+
```sql
|
|
164
|
+
-- PT-BR: emite linha em observability.migration_events
|
|
165
|
+
insert into observability.migration_events (
|
|
166
|
+
migration_id, sql_hash, applied_at, build_id, result_success, duration_ms
|
|
167
|
+
) values (
|
|
168
|
+
'20260506120000_create_orders', md5(...), now(), '{{BUILD_ID}}', true, {{ELAPSED_MS}}
|
|
169
|
+
);
|
|
170
|
+
```
|
|
171
|
+
2. **Audit triggers em tabelas sensíveis** (pagamentos, auth, dados pessoais): trigger `after insert/update/delete` que insere `audit_log` com `tenant_id`, `user_id`, `op`, `old_row`, `new_row`, `actor`, `timestamp`.
|
|
172
|
+
3. **Atributos canônicos** em qualquer função criada: `set search_path = ''` + comments com `result.success`, `error.type` enum esperado (skill [`structured-events`](../skills/structured-events/SKILL.md)).
|
|
173
|
+
|
|
174
|
+
**Output adicionado:** seção "## Audit hooks" + "## Migration event emit" no SQL gerado, comentadas em PT-BR.
|
|
175
|
+
|
|
176
|
+
## Alerta toil — automação via pg_cron
|
|
177
|
+
|
|
178
|
+
> Cross-ref canônico: [eliminating-toil](../skills/eliminating-toil/SKILL.md) (cap 5 do livro Google SRE — Eliminating Toil). Para auditoria sistemática de toil em todo o repo, delegar para [toil-auditor](./toil-auditor.md).
|
|
179
|
+
|
|
180
|
+
Migrations SQL executadas **manualmente em cadência regular** (rebuild índice, VACUUM, REFRESH MATERIALIZED VIEW, ANALYZE) são toil canônico — passam todos os 6 critérios: manual, repetitivo, automatizável, tático, sem valor durável, escala linear. Este agent **detecta padrões de toil** ao escrever migration e **alerta proativamente** sugerindo automação via `pg_cron`.
|
|
181
|
+
|
|
182
|
+
### 6 critérios — quando uma migration é toil-prone
|
|
183
|
+
|
|
184
|
+
Migration descreve operação que será re-executada > 1× = toil-prone. Aplicar 6 critérios da skill `eliminating-toil`:
|
|
185
|
+
|
|
186
|
+
| Critério | Pergunta | Sinal de toil |
|
|
187
|
+
|---|---|---|
|
|
188
|
+
| 1. Manual | Operador roda `psql` ou aplica migration "quando lembra"? | Sim |
|
|
189
|
+
| 2. Repetitivo | Já foi executada 3+ vezes em milestones diferentes? | Sim |
|
|
190
|
+
| 3. Automatizável | `pg_cron` consegue agendar sem julgamento humano? | Sim |
|
|
191
|
+
| 4. Tático | Reage a sintoma (lentidão, bloat, stale view) sem planejar? | Sim |
|
|
192
|
+
| 5. Sem valor durável | Não cria asset permanente — só "limpa" estado | Sim |
|
|
193
|
+
| 6. Escala linear | Mais users / mais dados = mais frequência manual | Sim |
|
|
194
|
+
|
|
195
|
+
Se TODOS os 6 = sim → **toil**. Bloquear migration manual recorrente; oferecer alternativa via `pg_cron`.
|
|
196
|
+
|
|
197
|
+
### Padrões SQL canônicos que SEMPRE disparam alerta toil
|
|
198
|
+
|
|
199
|
+
| Operação manual | Por quê é toil | Automação canônica |
|
|
200
|
+
|---|---|---|
|
|
201
|
+
| `REINDEX TABLE x` recorrente (a cada N semanas) | Rebuild de bloat de índice é tático, sem valor durável, repetitivo | `select cron.schedule('reindex_x', '0 3 * * 0', $$reindex table x$$);` (semanal 3am) |
|
|
202
|
+
| `VACUUM ANALYZE x` manual | autovacuum não está acompanhando — sintoma de tuning, não fix manual | Tunar `autovacuum_vacuum_scale_factor` para tabela específica + `pg_cron` se necessário |
|
|
203
|
+
| `REFRESH MATERIALIZED VIEW x` manual | Stale view detectada por user reclamação ou alert | `select cron.schedule('refresh_x', '*/30 * * * * *', $$refresh materialized view concurrently x$$);` |
|
|
204
|
+
| `ANALYZE` em tabela após bulk insert manual | Estatísticas desatualizadas após ETL — bem conhecido | Trigger AFTER INSERT/COPY com `analyze` no fim do batch, ou `pg_cron` pós-ETL |
|
|
205
|
+
| `delete from logs where created_at < now() - interval '90d'` manual recorrente | Retention manual = toil clássico | `select cron.schedule('purge_logs', '0 4 * * *', $$delete from logs where ...$$);` |
|
|
206
|
+
| `dump + restore` periódico para estatísticas / planos cache | Operação repetitiva sem valor permanente | `pg_cron` job ou `pg_stat_reset_*()` calls automatizadas |
|
|
207
|
+
|
|
208
|
+
### Snippet canônico — converter manual em pg_cron
|
|
209
|
+
|
|
210
|
+
```sql
|
|
211
|
+
-- PT-BR: ANTES — toil (operador roda manualmente)
|
|
212
|
+
-- $ psql -c 'reindex table heavy_table;' ← repetir a cada 2 semanas
|
|
213
|
+
|
|
214
|
+
-- PT-BR: DEPOIS — automação via pg_cron (necessita extension pg_cron habilitada)
|
|
215
|
+
create extension if not exists pg_cron;
|
|
216
|
+
|
|
217
|
+
select cron.schedule(
|
|
218
|
+
'reindex_heavy_table_biweekly',
|
|
219
|
+
'0 3 1,15 * *', -- 3am dias 1 e 15
|
|
220
|
+
$$ reindex table public.heavy_table $$
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
-- PT-BR: monitor — falha em job pg_cron emite linha em cron.job_run_details
|
|
224
|
+
-- alimentar alerta SLO se job falha 3+ vezes seguidas
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Quando NÃO automatizar (não é toil)
|
|
228
|
+
|
|
229
|
+
- **Migration de schema (DDL one-shot)** — `create table`, `alter table add column` são project work, não toil. Não recorrentes.
|
|
230
|
+
- **Backfill data único** — `update orders set status = ...` aplicado 1× para corrigir bug é grungy work, não toil.
|
|
231
|
+
- **Rebuild que requer julgamento** — `reindex` que requer escolher hora baseada em load patterns variáveis, ou que precisa coordenação com release. Mantém manual mas documenta runbook.
|
|
232
|
+
|
|
233
|
+
### Output do agent — adicionado ao SQL gerado
|
|
234
|
+
|
|
235
|
+
Quando o agent detecta que a migration descreve operação toil-prone (regex em DDL: `reindex|vacuum|refresh materialized|delete from .* interval`), adiciona comentário-alerta no header do arquivo SQL gerado:
|
|
236
|
+
|
|
237
|
+
```sql
|
|
238
|
+
/*
|
|
239
|
+
⚠ TOIL ALERT — esta operação parece recorrente.
|
|
240
|
+
|
|
241
|
+
Se será executada em cadência regular, considere automação via pg_cron:
|
|
242
|
+
select cron.schedule('<job_name>', '<schedule>', $$ <sql> $$);
|
|
243
|
+
|
|
244
|
+
Cross-ref: kit/skills/eliminating-toil/SKILL.md (6 critérios canônicos)
|
|
245
|
+
kit/agents/toil-auditor.md (audit sistemático para repo todo)
|
|
246
|
+
*/
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Anti-patterns prevenidos
|
|
250
|
+
|
|
251
|
+
- "Roda quando der" runbook → SEMPRE pg_cron + monitoring de falha do job
|
|
252
|
+
- `pg_cron` schedule mas sem alerta de falha → SEMPRE incluir SLO em `cron.job_run_details` (% sucesso 30d)
|
|
253
|
+
- Automação parcial (script humano-iniciado) → ainda é toil (humano pressiona botão); preferir cron.schedule completo
|
|
254
|
+
- Migration manual recorrente "porque é só uma vez por mês" → 12×/ano = toil, regra ≤ 50% se acumular vários "só um por mês"
|
|
@@ -245,8 +245,31 @@ PRÓXIMOS PASSOS
|
|
|
245
245
|
- Presence para listas de objetos → ALERTA explícito (use queries normais)
|
|
246
246
|
- Naming inconsistente → SEMPRE `scope:entity:id`
|
|
247
247
|
|
|
248
|
+
## Observabilidade integrada
|
|
249
|
+
|
|
250
|
+
Realtime é tipicamente fora-de-trace porque WebSocket não usa header `traceparent` por default. Patches:
|
|
251
|
+
|
|
252
|
+
1. **Trace context no payload do broadcast** (skill [`distributed-tracing`](../skills/distributed-tracing/SKILL.md)):
|
|
253
|
+
```ts
|
|
254
|
+
// PT-BR: producer — anexa traceparent ao payload do broadcast
|
|
255
|
+
const carrier: Record<string, string> = {}
|
|
256
|
+
propagation.inject(context.active(), carrier)
|
|
257
|
+
await channel.send({
|
|
258
|
+
type: 'broadcast',
|
|
259
|
+
event: 'message_inserted',
|
|
260
|
+
payload: { ...originalPayload, _trace_context: carrier }
|
|
261
|
+
})
|
|
262
|
+
```
|
|
263
|
+
2. **Consumer extrai contexto** ao receber broadcast e abre span filho — stitching cross-WebSocket fica completo.
|
|
264
|
+
3. **Atributos canônicos** em todo span de subscribe/unsubscribe (skill [`structured-events`](../skills/structured-events/SKILL.md)): `channel.name`, `channel.private`, `subscribe.status` (`SUBSCRIBED` | `CHANNEL_ERROR` | `TIMED_OUT`), `user.id`, `tenant_id`.
|
|
265
|
+
4. **Trigger DB** (`realtime.broadcast_changes`) emite evento estruturado em `observability.events` com `event_name = 'realtime_broadcast'`, `result_success`, `tenant_id`.
|
|
266
|
+
|
|
267
|
+
**Output adicionado:** template inclui propagation.inject no payload + span wrapper em subscribe + atributos canônicos no callback.
|
|
268
|
+
|
|
248
269
|
## Ver também
|
|
249
270
|
|
|
250
271
|
- [supabase-realtime](../skills/supabase-realtime/SKILL.md) — base de conhecimento canônica
|
|
251
272
|
- [supabase-rls-writer](./supabase-rls-writer.md) — invocar para policies adicionais em tabelas do app
|
|
252
273
|
- [supabase-database-functions](../skills/supabase-database-functions/SKILL.md) — trigger function pattern
|
|
274
|
+
- [distributed-tracing](../skills/distributed-tracing/SKILL.md) — context propagation cross-WebSocket
|
|
275
|
+
- [structured-events](../skills/structured-events/SKILL.md) — atributos canônicos para channels
|
|
@@ -212,7 +212,24 @@ NOTAS
|
|
|
212
212
|
- Tabela já tem policies estabelecidas e user só quer 1 ajuste pequeno → use Edit direto
|
|
213
213
|
- Tabela é puramente read-only para `anon` (ex: catalog público) → policy trivial, overhead
|
|
214
214
|
|
|
215
|
+
## Observabilidade integrada
|
|
216
|
+
|
|
217
|
+
RLS denials são sinal de segurança e debug — emite evento estruturado SEMPRE.
|
|
218
|
+
|
|
219
|
+
1. **RLS deny logging**: no entry-point do app (Edge Function ou backend), capturar `42501 insufficient_privilege` errors e emitir span com:
|
|
220
|
+
- `policy.name` (qual policy negou)
|
|
221
|
+
- `attempted_op` (`select` | `insert` | `update` | `delete`)
|
|
222
|
+
- `user.id` (de `auth.uid()` na sessão)
|
|
223
|
+
- `tenant_id` (de `app_metadata` quando aplicável)
|
|
224
|
+
- `resource.table` (qual tabela/view tentada)
|
|
225
|
+
- `error.type = 'authz'` (skill [`structured-events`](../skills/structured-events/SKILL.md))
|
|
226
|
+
2. **Investigação via Core Analysis Loop** (skill [`core-analysis-loop`](../skills/core-analysis-loop/SKILL.md)): pergunta canônica "qual policy + qual tenant + qual op + quando começou?" → query agrupando por essas 4 dimensões para identificar pattern.
|
|
227
|
+
|
|
228
|
+
**Output adicionado:** seção "## Observability hooks" com snippet de error handler que classifica RLS denial e emite span.
|
|
229
|
+
|
|
215
230
|
## Ver também
|
|
216
231
|
|
|
217
232
|
- [supabase-rls-policies](../skills/supabase-rls-policies/SKILL.md) — base de conhecimento canônica das regras
|
|
218
233
|
- [supabase-migration-writer](./supabase-migration-writer.md) — invocar quando user quer policies dentro de migration nova
|
|
234
|
+
- [structured-events](../skills/structured-events/SKILL.md) — campos canônicos para RLS denial logging
|
|
235
|
+
- [core-analysis-loop](../skills/core-analysis-loop/SKILL.md) — investigar denial patterns
|
|
@@ -233,8 +233,182 @@ ALERTAS
|
|
|
233
233
|
- **Vector Buckets / Analytics Buckets** ainda alpha em 2026-05-06 — não detalhar
|
|
234
234
|
- **Smart CDN** para egress optimization — fora deste agent (config no Dashboard)
|
|
235
235
|
|
|
236
|
+
## Observabilidade integrada
|
|
237
|
+
|
|
238
|
+
Upload events são quentes em custo (egress + storage) e em UX (lentidão de upload = abandono). Instrumentar SEMPRE.
|
|
239
|
+
|
|
240
|
+
1. **Span por upload/download** (skill [`structured-events`](../skills/structured-events/SKILL.md)) com atributos:
|
|
241
|
+
- `bucket.name`, `bucket.public` (bool)
|
|
242
|
+
- `file.size_bytes`, `file.mime_type`, `file.path`
|
|
243
|
+
- `operation`: `upload` | `download` | `signed_url` | `delete`
|
|
244
|
+
- `result.success`, `error.type` (enum: `quota_exceeded`, `unauthorized`, `mime_blocked`, `size_exceeded`, `network`)
|
|
245
|
+
- `duration_ms`, `transfer.bytes_per_second` (calculado)
|
|
246
|
+
- `user.id`, `tenant_id` (do `auth.uid()`)
|
|
247
|
+
2. **Sampling** (skill [`telemetry-sampling`](../skills/telemetry-sampling/SKILL.md) *Phase 34*): 100% errors, 100% uploads > 10 MB (cardinalidade baixa, valor alto), 5% baseline para downloads pequenos (alto volume).
|
|
248
|
+
3. **Audit log** para uploads em buckets sensíveis (`audit_log` table com `actor`, `op`, `resource`, `geo`, `user_agent`).
|
|
249
|
+
|
|
250
|
+
**Output adicionado:** seção "## Observability hooks" com snippet de upload/download wrapper.
|
|
251
|
+
|
|
252
|
+
## Saturation signal — bucket size + quota
|
|
253
|
+
|
|
254
|
+
> Cross-ref canônico: [four-golden-signals](../skills/four-golden-signals/SKILL.md) (cap 6 do livro Google SRE — Monitoring Distributed Systems). Para retro-instrumentar storage existente com os 4 signals, delegar para [golden-signals-instrumenter](./golden-signals-instrumenter.md).
|
|
255
|
+
|
|
256
|
+
Storage tem o **recurso mais escasso explícito**: o quota do plano (Free 1 GB, Pro 100 GB, Team 1 TB, etc.). Sem signal de saturation, time descobre quota exhaustion via incident (uploads falham silenciosamente em UX) — **anti-pattern clássico** de white-box monitoring sem detecção precoce. O bloco `## Observabilidade integrada` acima cobre Latency / Traffic / Errors (3 signals); este bloco completa com **Saturation** — o 4º signal canônico.
|
|
257
|
+
|
|
258
|
+
### Saturation = bucket size ÷ quota plan
|
|
259
|
+
|
|
260
|
+
| Plano | Quota total | Threshold ALERT (yellow) | Threshold PAGE (red) |
|
|
261
|
+
|---|---|---|---|
|
|
262
|
+
| Free | 1 GB | 80% (800 MB) | 95% (950 MB) |
|
|
263
|
+
| Pro | 100 GB | 80% (80 GB) | 95% (95 GB) |
|
|
264
|
+
| Team | 1 TB | 80% (800 GB) | 95% (950 GB) |
|
|
265
|
+
| Enterprise | custom | custom | custom |
|
|
266
|
+
|
|
267
|
+
### Signal 1 — Gauge: bucket size atual (bytes)
|
|
268
|
+
|
|
269
|
+
`ObservableGauge` (push periódico via callback) mede tamanho real de cada bucket. Callback consulta `storage.objects` agregado:
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
// PT-BR: 4º signal — saturation (gauge de bucket size em bytes)
|
|
273
|
+
import { metrics } from 'npm:@opentelemetry/api@1.9.0'
|
|
274
|
+
const meter = metrics.getMeter('supabase-storage')
|
|
275
|
+
|
|
276
|
+
meter.createObservableGauge('storage_bucket_bytes', {
|
|
277
|
+
description: 'Tamanho atual em bytes por bucket — saturation signal',
|
|
278
|
+
unit: 'bytes',
|
|
279
|
+
}).addCallback(async (result) => {
|
|
280
|
+
// PT-BR: query agregada (rodar via service-role client em cron)
|
|
281
|
+
const sizes = await supabaseAdmin.rpc('storage_bucket_sizes_bytes')
|
|
282
|
+
// expected: [{ bucket_id: 'avatars', total_bytes: 12345678 }, ...]
|
|
283
|
+
for (const row of sizes ?? []) {
|
|
284
|
+
result.observe(row.total_bytes, { 'bucket.id': row.bucket_id })
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
meter.createObservableGauge('storage_saturation_pct', {
|
|
289
|
+
description: 'Saturation = bucket size / quota plan — % do quota usado',
|
|
290
|
+
unit: '1', // ratio (0..1)
|
|
291
|
+
}).addCallback(async (result) => {
|
|
292
|
+
const sizes = await supabaseAdmin.rpc('storage_bucket_sizes_bytes')
|
|
293
|
+
const QUOTA_BYTES = Number(Deno.env.get('SUPABASE_PLAN_QUOTA_BYTES') ?? 1_000_000_000) // default Free
|
|
294
|
+
for (const row of sizes ?? []) {
|
|
295
|
+
result.observe(row.total_bytes / QUOTA_BYTES, { 'bucket.id': row.bucket_id })
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
SQL helper para o callback:
|
|
301
|
+
|
|
302
|
+
```sql
|
|
303
|
+
-- PT-BR: function que retorna bytes por bucket — chamada por callback OTel
|
|
304
|
+
create or replace function public.storage_bucket_sizes_bytes()
|
|
305
|
+
returns table (bucket_id text, total_bytes bigint)
|
|
306
|
+
language sql
|
|
307
|
+
security definer
|
|
308
|
+
set search_path = ''
|
|
309
|
+
as $$
|
|
310
|
+
select bucket_id, coalesce(sum((metadata->>'size')::bigint), 0) as total_bytes
|
|
311
|
+
from storage.objects
|
|
312
|
+
group by bucket_id;
|
|
313
|
+
$$;
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Signal 2 — Counter: quota near-exhaustion events
|
|
317
|
+
|
|
318
|
+
`Counter` incrementa a cada upload que **detecta** approach a quota threshold (80%, 95%). Permite contar eventos críticos para alerting:
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
// PT-BR: counter incrementado em cada upload
|
|
322
|
+
const quotaWarnings = meter.createCounter('storage_quota_warnings_total', {
|
|
323
|
+
description: 'Counter de eventos onde upload aproxima quota — alimentar alert SLO',
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
export async function uploadInstrumented(file: File, filename: string) {
|
|
327
|
+
const supabase = createClient()
|
|
328
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
329
|
+
if (!user) throw new Error('not authenticated')
|
|
330
|
+
|
|
331
|
+
const path = `${user.id}/${filename}`
|
|
332
|
+
|
|
333
|
+
// PT-BR: pre-check — saturation atual antes de upload
|
|
334
|
+
const sizes = await supabaseAdmin.rpc('storage_bucket_sizes_bytes')
|
|
335
|
+
const bucketSize = sizes?.find(s => s.bucket_id === '<bucket_name>')?.total_bytes ?? 0
|
|
336
|
+
const QUOTA = Number(Deno.env.get('SUPABASE_PLAN_QUOTA_BYTES') ?? 1_000_000_000)
|
|
337
|
+
const saturation = bucketSize / QUOTA
|
|
338
|
+
|
|
339
|
+
if (saturation >= 0.95) {
|
|
340
|
+
quotaWarnings.add(1, { 'bucket.id': '<bucket_name>', threshold: '95pct' })
|
|
341
|
+
} else if (saturation >= 0.80) {
|
|
342
|
+
quotaWarnings.add(1, { 'bucket.id': '<bucket_name>', threshold: '80pct' })
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const { data, error } = await supabase.storage
|
|
346
|
+
.from('<bucket_name>')
|
|
347
|
+
.upload(path, file, { upsert: true })
|
|
348
|
+
|
|
349
|
+
if (error) throw error
|
|
350
|
+
return data.path
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Cron schedule sugerido
|
|
355
|
+
|
|
356
|
+
Saturation gauge não precisa rodar em cada request — agendar leitura via `pg_cron` (ou OTel SDK polling interval = 60s) é suficiente:
|
|
357
|
+
|
|
358
|
+
```sql
|
|
359
|
+
-- PT-BR: refresh saturation cache a cada 60s para gauge OTel
|
|
360
|
+
create materialized view if not exists obs.storage_saturation as
|
|
361
|
+
select bucket_id, sum((metadata->>'size')::bigint) as total_bytes, now() as captured_at
|
|
362
|
+
from storage.objects
|
|
363
|
+
group by bucket_id;
|
|
364
|
+
|
|
365
|
+
select cron.schedule(
|
|
366
|
+
'refresh_storage_saturation',
|
|
367
|
+
'* * * * *', -- a cada 1 min
|
|
368
|
+
$$ refresh materialized view concurrently obs.storage_saturation $$
|
|
369
|
+
);
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Alert SLO sobre saturation
|
|
373
|
+
|
|
374
|
+
Saturation alimenta SLO event-based — não threshold direto:
|
|
375
|
+
|
|
376
|
+
```yaml
|
|
377
|
+
# PT-BR: SLO sobre quota — % de tempo em yellow ou worse
|
|
378
|
+
slo:
|
|
379
|
+
name: storage_quota_healthy
|
|
380
|
+
target: 0.99 # 99% do tempo em < 80% quota
|
|
381
|
+
window: 30d_sliding
|
|
382
|
+
sli:
|
|
383
|
+
type: event_based
|
|
384
|
+
good_event:
|
|
385
|
+
saturation_pct: { lt: 0.80 }
|
|
386
|
+
bad_event:
|
|
387
|
+
saturation_pct: { gte: 0.80 }
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Output do agent — adicionado ao SQL/código gerado
|
|
391
|
+
|
|
392
|
+
Quando agent gera bucket privado novo, **sempre inclui**:
|
|
393
|
+
1. Function SQL `storage_bucket_sizes_bytes()` (uma vez por projeto)
|
|
394
|
+
2. Materialized view `obs.storage_saturation` + pg_cron refresh job
|
|
395
|
+
3. Snippet OTel ObservableGauge no código client wrapper
|
|
396
|
+
4. Counter `storage_quota_warnings_total` no upload wrapper
|
|
397
|
+
5. SLO `storage_quota_healthy` em `.planning/slos/<bucket>.yaml`
|
|
398
|
+
|
|
399
|
+
### Anti-patterns prevenidos
|
|
400
|
+
|
|
401
|
+
- Saturation = "% disco do servidor" → SEMPRE saturation = % quota plan (recurso correto)
|
|
402
|
+
- Threshold direto em alerta CPU/memory para capacity → SEMPRE SLO event-based sobre saturation_pct
|
|
403
|
+
- Polling de bucket size em cada request → SEMPRE materialized view + pg_cron refresh + OTel polling 60s
|
|
404
|
+
- Plan quota hardcoded → SEMPRE env var `SUPABASE_PLAN_QUOTA_BYTES` (varia por plano, pode ser sobrescrita em test)
|
|
405
|
+
|
|
236
406
|
## Ver também
|
|
237
407
|
|
|
238
408
|
- [supabase-storage](../skills/supabase-storage/SKILL.md) — base de conhecimento canônica
|
|
239
409
|
- [supabase-rls-writer](./supabase-rls-writer.md) — invocar para policies adicionais
|
|
240
410
|
- [supabase-auth-ssr](../skills/supabase-auth-ssr/SKILL.md) — usuário autenticado obtém `auth.uid()`
|
|
411
|
+
- [structured-events](../skills/structured-events/SKILL.md) — campos canônicos para upload/download events
|
|
412
|
+
- [telemetry-sampling](../skills/telemetry-sampling/SKILL.md) *(Phase 34)* — head-based sampling por size_bytes
|
|
413
|
+
- [four-golden-signals](../skills/four-golden-signals/SKILL.md) — 4 sinais canônicos (Latency, Traffic, Errors, Saturation) cap 6 livro Google SRE — saturation = bucket size / quota plan
|
|
414
|
+
- [golden-signals-instrumenter](./golden-signals-instrumenter.md) — agent que retro-instrumenta storage existente com os 4 signals
|