Imported from nakamori-naoya/go-convention-plugins (
plugins/go-convention/skills/write-logs/SKILL.md). Install upstream withnpx skills add nakamori-naoya/go-convention-plugins --skill write-logs. Copyright stays with the author.
write-logs
工程順序の定義を最初に読み、同じagentが`steps`を宣言順に実行する。YAMLは工程順序を決め、各工程の判断内容と根拠はこの本文と参照資料を実読して評価する。失敗時は成功扱いせず停止して、完了工程、根拠、未決を残し、再開時は最初の未完了工程から続ける。
これは、ログを最外境界に集め、1 回だけ、同じ語彙で出すための規約である。内側はエラーを返し、境界が拾って記録する。
これは、エラーの分類ではない(どの sentinel をどの connect.Code にするかはエラーの規約が決める。この規約は Code からレベルを決めるだけ)。監査記録の一次データでもない(確定・取消の一次データはイベント行で、ログは消えてもよい二次情報)。メトリクス・トレースでもない(数えたいなら msg を定型にする。ログ行を計測の代わりに設計しない)。
前提: Go 1.27・log/slog・connectrpc.com/connect v1.21.0(v2 alpha は採らない)。
| する | しない |
|---|---|
| 最外境界(interceptor / supervisor / 外部受信)で 1 回記録する | error を return する直前に slog.Error を置く |
| 境界へ返らない情報(部分失敗して続行・再試行の各回・成功した業務監査イベント)だけ途中ログを出す | ログを握りつぶしの代替にする(記録して return nil) |
| logger を境界の構造体へ DI する | logger を ctx に入れる。nil を slog.Default() へ丸める |
常に *Context 版 / LogAttrs で ctx を渡す |
ドメイン package で log/slog を import する |
| 属性は snake_case の固定語彙、型付き helper、primitive だけ | 値オブジェクトや集約を slog.Any で丸ごと渡す。msg に値を埋め込む |
業務上の拒否・NotFound を Info にする |
拒否を Error にして対応が要る行と混ぜる |
入力
- 対象のプロセスと、その最外境界(Connectのinterceptor・workerのsupervisor・外部受信境界)。
references: 追加で従う資料の絶対path配列。任意。手順の最初に読み、以降の判断でこの規約と併せて従う。
プロジェクト固有の規約(置き場、命名、追加で従う資料)は、対象repositoryのAGENTS.md / CLAUDE.mdとreferencesで渡される。この入口は既定値を持たず、指示文へ展開もしない。
規約
| # | 柱 | 一言で | 基準資料 |
|---|---|---|---|
| 1 | 出す層と出さない層 | 最外境界が 1 回。usecase は境界へ返らない情報だけ。リポジトリとドメインは出さない。判定式「返すなら書かない、飲み込むなら書く」 | boundaries.md |
| 2 | middleware が拾う | 最外の interceptor handler.Logging が返ったエラーと ctx の属性から 1 回記録し、codeTable で connect.Code へ翻訳する。応答は sentinel の文言だけ。ctx から属性を足す slog.Handler のラッパ。worker は worker.Supervise。組み立ては run が handler.NewMux(Deps) と worker に同じ logger を渡す |
middleware.md |
| 3 | レベルと属性 | Debug / Info / Warn / Error の表。Code → レベル。snake_case の語彙、型付き helper、msg は定型 |
severity-and-attributes.md |
| 4 | 秘匿 | primitive だけ渡す・String() を書かない・JSON handler・ReplaceAttr の安全網。LogValuer をドメインに置かない |
redaction.md |
手順
- 境界を確かめる。
referencesがあれば先に読む。対象のプロセスに、最外の interceptor(handler.Logging)と worker の supervisor(worker.Supervise)があるかを見る。無ければ middleware.md の形で置き、handler.NewMuxの interceptor の並びで先頭(最外)にする。既にあるなら足さない。完了条件: RPC 1 回・worker 1 サイクルにつき、記録する場所が 1 か所に決まっている - 出したい行が境界へ返るかを問う。 その情報は error 鎖に載って境界へ届くか。届くなら書かない(
return errで足りる)。届かない(continueで飲み込む・再試行の各回・1 件ごとの成功)なら途中ログにする。完了条件: 書く行ごとに boundaries.md §4 のどの行に当たるかを言える - 層を確かめる。 途中ログを書く場所が worker の
Runか usecase である。リポジトリ・query service・ドメインなら書かず、エラーを返す形に戻す。1 件 1 tx の command は error を返すだけで、ループと途中ログは worker が持つ。完了条件: ドメイン package にlog/slogの import が無い - レベルと
msgと属性を決める。 severity-and-attributes.md の表からレベルを選び、msgを定型の日本語 1 文にし、属性を語彙表のキーと型付き helper で書く。語彙に無いキーが要るなら表に足す。完了条件:msgに値が無く、属性のキーが全部語彙表にある - 秘匿を確かめる。 属性に渡す値が primitive で、値オブジェクトは getter で取り出している。新しい秘匿キーがあれば
secretKeysとテストに足す。完了条件:slog.Anyに渡しているのがerr/panicだけ - 機械で見られる分を通す。
完了条件: 3 つとも指摘が無いgo vet ./... # slog の key/value 対の不整合 grep -rln '"log/slog"' ./reservation # ドメイン package。0 件であること grep -rnE '\bslog\.(Debug|Info|Warn|Error)\(' --include='*.go' . # ctx を取らない呼び出し。0 件であること - 報告する。 「報告」の項目
停止条件
止まるのは、資料または規約の契約に反する要求、正式な定義に無い決定が要る、利用者の許可が要る、toolが失敗した、のどれかに当たるときで、それ以外の判断の揺れでは止まらない。欠けているのが業務事実(操作・状態・拒む理由・資料が未決と明示した値)なら止まり、命名・分割・定義場所・並び・テストの置き場のような設計判断の揺れなら仮説を明示して進む。
- ドメイン package(値オブジェクト・集約・イベント)にログを求められた → 書かない。sentinel か遷移結果型で外へ出す設計を提案して止まる
- error を記録して
return nilする要求で、boundaries.md §4 の許可基準に当たらない → 書かない。return errにするか、継続を制御フローで明示する設計を返す - ログを監査記録の一次データにする要求(ログから業務状態を復元する前提) → 対象外。イベント行の設計へ返す
- メトリクス・トレース・アラートの配線 → 対象外。計測基盤の規約へ返す
- 出したい値が値オブジェクトの中にあり、primitive を取り出す getter が無い → 書かない。その値が運用に要るかをドメインの規約へ返す
- sentinel と
connect.Codeの対応を変えたい → 対象外。エラーの規約へ返す(この規約が持つのは Code → レベルだけ)
止まるときは、書いた範囲と書かなかった範囲を分け、返す先(資料、実装の規約、利用者)と必要な決定を報告に示す。
判断の揺れでは、その時点の根拠から最も筋の良い形を仮説として採り、仮説であることと採らなかった形を報告に明示して進む。
- レベルや属性名がレベル表・属性語彙のどれに最も近いか迷う: 表に最も近いものを採り、報告に示す。
チェックリスト(機械で言えないことだけ)
- 同じ失敗が 2 行にならない。
returnする error の直前にslog.Error/slog.Warnが無い - 途中ログの各行が「境界へ返らない情報」である(部分失敗して続行・再試行の各回・1 件ごとの成功・完了サマリ)
- 記録して
return nilしている箇所は、継続が意図であることが制御フロー(continue)で読める - handler の server 実装と内側の interceptor が記録していない。記録は最外の interceptor だけ
- worker は
worker.Superviseで起動している。go fn()が無い - logger は構造体へ DI され、ctx に入っていない。
nilを既定へ丸めていない。途中ログを持つ worker のRunは logger を DI で受けている - 業務上の拒否・NotFound が
Info、想定外だけがError -
msgが定型の日本語 1 文で、値を含まない - 属性のキーが語彙表にあり、
duration_msにslog.Durationを渡していない -
slog.Anyに渡しているのがerr/panicだけ。値オブジェクトにString()/LogValueが無い - テストの logger が
slog.DiscardHandlerかt.Output()で、ログの有無を検証していない
報告
- 置いた境界(interceptor / supervisor / HTTP middleware)と、その組み立て位置(
runの中で最外か) - 途中ログを書いた箇所と、それぞれが許可基準のどの行に当たるか
- 語彙表に足したキーと、
secretKeysに足したキー - 停止条件に当たって返した論点(ドメインの getter、エラーの翻訳表、監査記録、計測)