# Cloudflare Logpush セットアップ手順

Workers の構造化ログ (`createLogger` が出す JSON) を Cloudflare 外の永続ストレージに
ストリーミングするための設定。**Workers Observability Dashboard だけでは過去 7 日しか
保管されない** ため、本番運用には Logpush が必須。

> **状況** (2026-04-27): R2 bucket + lifecycle (dev 30d / prod 90d) + Logpush job 配信済み。
> - dev job: id=1606777 / `parky-workers-logs-dev` → `r2://parky-logs-dev/dev/{DATE}/`
> - prod job: id=1606779 / `parky-workers-logs-prod` → `r2://parky-logs-prod/prod/{DATE}/`
> - lifecycle rules は [infra/r2/lifecycle-logs-dev.json](../../infra/r2/lifecycle-logs-dev.json) /
>   [lifecycle-logs-prod.json](../../infra/r2/lifecycle-logs-prod.json) で管理

## 推奨構成

| 環境 | Destination | 保持 | 用途 |
|---|---|---|---|
| dev  | R2 (`parky-logs-dev`)   | 30 日 | デバッグ・障害分析 |
| prod | R2 (`parky-logs-prod`)  | 90 日 | インシデント・SLO 計測 |
| prod | BigQuery (任意)         | 365 日 | 長期分析・ダッシュボード |

R2 を主、BigQuery は将来必要になったら追加 (parky の現フェーズでは R2 だけで十分)。

## 1. R2 bucket 作成

```bash
# dev
wrangler r2 bucket create parky-logs-dev

# prod
wrangler r2 bucket create parky-logs-prod
```

R2 の lifecycle rule で 30 / 90 日後 auto-delete を設定:

```bash
wrangler r2 bucket lifecycle set parky-logs-dev  --file infra/r2/lifecycle-logs-dev.json
wrangler r2 bucket lifecycle set parky-logs-prod --file infra/r2/lifecycle-logs-prod.json
```

JSON 定義は [infra/r2/lifecycle-logs-dev.json](../../infra/r2/lifecycle-logs-dev.json) (30 日) と
[lifecycle-logs-prod.json](../../infra/r2/lifecycle-logs-prod.json) (90 日)。

## 2. Logpush job 作成 (Cloudflare API)

Workers の Logpush は **Account 単位** の設定。R2 destination は ownership challenge を経て
作成する 2 ステップフロー。

```bash
ACCOUNT_ID=5d8f6201999f8965395396c4674cbe2d
CF_TOKEN=$(op item get fckmphwmq7pccoyg6ye3vf4f34 --vault "PJ｜Parky" --fields credential --reveal)
ACCESS_KEY=$(op item get rhxd2jnzp5dqbetn7kiky6aala --vault "PJ｜Parky" --fields access_key_id --reveal)
SECRET_KEY=$(op item get rhxd2jnzp5dqbetn7kiky6aala --vault "PJ｜Parky" --fields secret_access_key --reveal)

# Step 1: ownership challenge — CF が R2 にチャレンジファイルを書き込む
DEST="r2://parky-logs-dev/dev/{DATE}?account-id=${ACCOUNT_ID}&access-key-id=${ACCESS_KEY}&secret-access-key=${SECRET_KEY}"
CHALLENGE_FILE=$(curl -sS -X POST -H "Authorization: Bearer ${CF_TOKEN}" \
  "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/logpush/ownership" \
  --data "{\"destination_conf\":\"${DEST}\"}" \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['result']['filename'])")

# Step 2: チャレンジファイルから token を読む
npx wrangler r2 object get "parky-logs-dev/${CHALLENGE_FILE}" --file /tmp/ownership.txt
OWNERSHIP_TOKEN=$(cat /tmp/ownership.txt)

# Step 3: Job 作成 — kind は付けない (workers_trace_events は kind=edge と非対応)
curl -X POST "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/logpush/jobs" \
  -H "Authorization: Bearer ${CF_TOKEN}" -H "Content-Type: application/json" \
  --data "{
    \"name\": \"parky-workers-logs-dev\",
    \"destination_conf\": \"${DEST}\",
    \"dataset\": \"workers_trace_events\",
    \"filter\": \"{\\\"where\\\":{\\\"and\\\":[{\\\"key\\\":\\\"ScriptName\\\",\\\"operator\\\":\\\"startsWith\\\",\\\"value\\\":\\\"parky-\\\"},{\\\"key\\\":\\\"ScriptName\\\",\\\"operator\\\":\\\"contains\\\",\\\"value\\\":\\\"-dev\\\"}]}}\",
    \"output_options\": {
      \"field_names\": [\"ScriptName\",\"EventType\",\"Outcome\",\"Logs\",\"Exceptions\",\"DispatchNamespace\",\"Event\",\"EventTimestampMs\"],
      \"timestamp_format\": \"rfc3339\"
    },
    \"frequency\": \"high\",
    \"enabled\": true,
    \"ownership_challenge\": \"${OWNERSHIP_TOKEN}\"
  }"
```

R2 access key は既存の **`Parky R2 API Token`** (PJ｜Parky vault, item id `rhxd2jnzp5dqbetn7kiky6aala`) が
account-scoped で全 bucket 書込み可。新規 token 作成不要 (本セットアップで検証済み)。

`filter` の `ScriptName contains -dev` / `-prod` で dev/prod の Worker logs を分離 (例:
`parky-api-dev` は dev job、`parky-api-prod` は prod job)。

## 3. ログのクエリ

R2 に NDJSON 形式で蓄積されるので、解析は以下のいずれか:

- **DuckDB ローカル**: `r2 cp s3://parky-logs-dev/dev/2026-04-26/ /tmp/ --recursive` 後
  `duckdb -c "select * from read_ndjson_auto('/tmp/*.json.gz') where Outcome != 'ok'"`
- **Cloudflare R2 SQL** (公開待ち): R2 上で直接 SQL クエリ
- **BigQuery 連携**: 後追いで Logpush job を増やす

クエリ例 (5xx を時系列で):

```sql
SELECT
  EventTimestampMs,
  Logs[0].Message AS log,
  Outcome,
  Event.RequestUrl AS url
FROM read_ndjson_auto('parky-logs-prod/prod/2026-04-26/*.json.gz')
WHERE Outcome != 'ok'
ORDER BY EventTimestampMs DESC
LIMIT 100
```

## 4. Tail Workers (リアルタイム転送)

リアルタイム性が必要な signal (例: 5xx burst の即時 Slack 通知) は Logpush では
遅すぎるため、別途 **Tail Worker** を作って `wrangler.toml` の `tail_consumers` で
配線する。本書の対象外 (B12 の DLQ monitor と統合する別タスク)。

## 5. PII / GDPR 注意

- `RequestHeaders` フィールドには `Authorization` / `Cookie` ヘッダが含まれうる。
  Logpush の filter で除外する: `"logpull_options": "fields=...&exclude=RequestHeaders.Authorization,RequestHeaders.Cookie"`
- structured log の `error.message` に email / phone が混入しないよう、
  `lib/logger.ts` 側で redact する責務 (S7 / 別タスク)。

## 6. SLO / Alert 連携

Logpush の R2 dump を Workers Cron で 5 分毎に集計して `analytics.error_reports` に書き
戻す案がある (P1 / project_parky_log_aggregation)。実装は本書の対象外。

## 7. コスト目安

- Logpush: $0.05 / 1M log lines (Workers Paid Plan に含まれる)
- R2 storage: 30 日 × 1GB/day × $0.015/GB = $0.45/月
- R2 egress: 0 (Cloudflare 内なら 0、外部 destination で発生)

## 関連

- [parky/api/wrangler.toml](../../api/wrangler.toml) — `[observability] head_sampling_rate`
- [parky/api/src/lib/logger.ts](../../api/src/lib/logger.ts) — JSON ログスキーマ
- [parky/docs/ops/logging.md](./logging.md) — 構造化ログ仕様 (level/scope/resource)
- [parky/docs/ops/sentry-setup.md](./sentry-setup.md) — Sentry (Issue / Alert) 連携
