# 設定

Source: https://gortexa.sheng.page/configuration/

設定檔預設在 `etc/config.yaml`；檔案不存在時該層被跳過，改用內建預設值加環境變數。其餘載入是 fail-loud：欄位型別錯誤或 secret 不合規，啟動直接失敗而不是帶病上線。

## 設定檔怎麼載入

設定由三層疊起來，後者覆寫前者：

1. 內建預設值（程式碼裡；例如 `server.addr` 是 `:8080`、`cache.driver` 是 `memory`）。
2. `etc/config.yaml`——用 `GORTEXA_CONFIG=/path/to/config.yaml` 指到別處；檔案不存在時整層跳過。`GORTEXA_CONFIG` 與跳過邏輯由伺服器的 `main.go`（`cmd/server`）處理，不在 `config` 套件裡：自己呼叫 `config.WithConfigFile` 的程式碼遇到檔案不存在會失敗。
3. 環境變數，格式 `GORTEXA_<SECTION>__<FIELD>`（區塊與欄位之間兩條底線）。

同一個欄位若在多層都設，最後生效的是環境變數。真實 secret 只走環境變數注入，不寫進 `config.yaml`、不進版控。

## config.yaml

```yaml
server:
  addr: ":8080"
  shutdown_timeout: 20s
  enable_cors: true
  cors_origins: []           # CORS allowlist；enable_cors 只是總開關，沒列在這裡的 origin 拿不到 CORS headers
  openapi: true
  reflection: false          # gRPC reflection 預設關

auth:
  jwt_secret: "dev-only-insecure-secret-change-me-please"
  issuer: "gortexa"
  # audience: "myapp"        # 選配：設了之後簽出的 token 帶 aud、驗證時要求 aud——隔離共用 secret 的多個服務
  ttl: 1h

db:
  dsn: ""                    # PostgreSQL DSN（Secret，日誌遮罩）；走環境變數注入：GORTEXA_DB__DSN
  max_conns: 10

cache:
  driver: "memory"           # "memory"（預設，process-local）或 "redis"

mq:
  driver: "nats"             # "nats"（core，at-most-once）或 "jetstream"（durable，at-least-once）
  url: ""                    # 逗號分隔多 server，例 "nats://a:4222,nats://b:4222"
  group_id: ""               # 空＝fanout；非空＝load-balance

log:
  level: "info"
  format: "json"

observ:
  service_name: "gortexa"
  service_version: "dev"
  # tracing_otlp: "localhost:4317"   # OTLP trace endpoint；空值停用
  # metrics_otlp: "localhost:4317"   # OTLP metrics endpoint；空值停用（rate-limit／circuit-breaker／health 指標靠它）
  # logs_otlp: "localhost:4317"      # OTLP log endpoint；空值停用
  otlp_insecure: true                # 只限本機 docker compose；連遠端一律 false
  sample_ratio: 1.0
  genai_capture_content: false       # 開啟後 MCP tool 呼叫的參數內容會進 telemetry
  # genai_mask_fields: ["password", "token", "secret", "authorization", "api_key"]
```

`genai_capture_content: true` 會把 AI tool 呼叫的參數送進 telemetry；預設遮罩清單只涵蓋常見欄位名。若同時 `otlp_insecure: true`，這些內容是明文外送——兩者不要同時在生產開。

## 快取：memory 與 redis

`cache.driver` 決定後端。預設 `memory` 不需要任何外部服務；`redis` 才讀 `addr` 以下的欄位。填了 `memory` 或 `redis` 以外的值，`cache.New` 會回傳 `cache: unsupported driver`。伺服器本身不建立快取，所以 driver 打錯要等到你的程式碼呼叫 `cache.New` 才會出錯，啟動時不會發現。

#### memory（預設）
```yaml
cache:
  driver: "memory"
```

Process-local，零外部依賴。適合單機部署與本機開發。
#### redis
```yaml
cache:
  driver: "redis"
  addr: "localhost:6379"
  password: ""            # 走環境變數注入：GORTEXA_CACHE__PASSWORD
  db: 0

  # 以下 client tunables 皆可省略，省略即用內建預設值
  dial_timeout: 5s        # 預設 5s
  read_timeout: 3s        # 預設 3s
  write_timeout: 3s       # 預設 3s
  pool_size: 10           # 預設 10；不可為負
```

切到 redis 後由框架內建的零依賴 RESP client 連線，沒有第三方 redis 套件。

四個 tunable 的零值都代表「用內建預設」，所以只調你要動的那個就好：

| 欄位 | 預設 | 用途 |
| --- | --- | --- |
| `dial_timeout` | 5s | 建立連線的逾時 |
| `read_timeout` | 3s | 單次讀取回應的逾時 |
| `write_timeout` | 3s | 單次送出命令的逾時 |
| `pool_size` | 10 | 連線池大小；`0` 用預設，負值被拒 |

放在 load balancer 後面時，把 timeout 調短可以更快 fail 而不是卡住整個請求；busy 的服務可把 `pool_size` 加大。呼叫端的 context 取消會直接中止進行中的指令（不再等滿 `read_timeout`）；從 idle pool 取出的連線若第一個指令就失敗，會換一條新連線重試一次——LB／NAT 回收 idle 連線、Redis `timeout`、failover 不再變成假的首次錯誤；逾時刻意不重試。

## 資料庫（PostgreSQL）

`db.dsn` 是 pgx 連線字串（Secret 欄位，日誌一律遮罩）；`db.max_conns` 預設 10。真實 DSN 走環境變數注入：

```bash
GORTEXA_DB__DSN=postgres://user:pass@pgbouncer:6432/app
```

連線池是 PgBouncer-safe 的：`QueryExecModeExec`、statement 與 description cache 皆關閉，放在 transaction-pooling 模式的 PgBouncer 後面不會因 prepared statement 出錯。

開發流程：schema 放 `db/migrations/`、SQL 放 `db/queries/`，跑 `make sqlc` 產生 type-safe 查詢碼到 `internal/storage/db/`。`deploy/docker-compose.yaml` 已含 postgres 與 PgBouncer，可供本機驗證。

## 環境變數覆寫

任何欄位都能用環境變數覆寫，格式 `GORTEXA_<SECTION>__<FIELD>`：

```bash
GORTEXA_SERVER__ADDR=:9090
GORTEXA_AUTH__JWT_SECRET=<real-secret>
GORTEXA_CACHE__DRIVER=redis
GORTEXA_CACHE__ADDR=redis.internal:6379
GORTEXA_SERVER__REFLECTION=true
```

## Secret 規則

- config 中的 placeholder `dev-only-insecure-secret-change-me-please` 會被伺服器拒絕啟動，不可能誤帶上線。
- JWT secret 長度不足 32 bytes 也會被拒絕。
- 多個服務共用同一把 secret 與預設 issuer 時，彼此的 token 可以互通——要隔離就各自設 `auth.audience`（為 A 簽的 token 在 B 會被拒）。
- redis password、MQ URL（可能內嵌帳密）等 secret 欄位在日誌與錯誤輸出一律遮罩。
- 真實 secret 走環境變數注入，不進版控。

## MQ 語意

`mq.group_id` 決定訊息分派方式，兩個 driver 一致：

| group_id | NATS（core） | JetStream | 語意 |
| --- | --- | --- | --- |
| 空（預設） | Subscribe | ephemeral consumer | fanout：每個訂閱者都收到 |
| 非空 | QueueSubscribe | durable consumer（名稱＝group_id，handler 成功才 ack） | load-balance：一則訊息一個處理者 |

driver 的差別在投遞保證：core NATS 是 at-most-once（無重投遞，handler 失敗訊息即消失）；`driver: "jetstream"` 是 at-least-once——publish 等 server 落盤確認，handler 回錯誤會延遲重投遞，因此 handler 必須冪等。JetStream 需要 server 以 `-js` 啟動；框架對每個 topic 惰性自建 stream（保留 24h），operator 也可預建 stream 自訂保留策略——框架只採用、不改寫既有 stream。

兩個 driver 共通的保證：`Headers` 名稱以 `gortexa-key` 與整個 `nats-*` 命名空間（不分大小寫，如 `Nats-Msg-Id`）為保留字，Publish 時一律拒為 `InvalidArgument`——這些標頭在 JetStream 上會被 broker 解讀（例如觸發伺服端去重而默默丟訊息）；handler 內的 panic 會被復原成 `mq: handler panic` 錯誤、走與回傳錯誤相同的 Nak／重投遞路徑，不會讓整個行程崩潰；stream 若被外部刪除（`nats stream rm`、叢集重建），下一次 Publish／Subscribe 會重跑 ensure 自動重建，不會一直失敗到行程重啟；連線對 broker 斷線**無限重連**（不像 SDK 預設約 120 秒後永久放棄），斷線／重連／關閉／非同步錯誤都經預設 `slog` logger 記錄；`Close(ctx)` 先等 watcher goroutine 與進行中的 handler 結束再關連線，所以 shutdown 當下還在跑的 handler 仍能 Publish。

## 檢查工具

```bash
gortexa doctor    # 檢查 Go toolchain 與 proto 工具
```

## 延伸閱讀

- [部署](https://gortexa.sheng.page/deployment/)：正式環境必須自己接的三件事
- [內建元件](https://gortexa.sheng.page/components/batteries/)：這些設定鍵背後的 package
