ロギングとメトリクス
tracing とメトリクスは障害調査の主経路である。関数名だけのログや、生IDをラベルにしたメトリクスは、原因特定を遅らせるうえ漏洩経路にもなる。
遷移の記録はユースケース境界で行う(状態遷移)。マスキングとID分類は PII 保護、エラーの一度きりの記録は エラーハンドリング と整合させる。
ドメインコンテキストで log する
Section titled “ドメインコンテキストで log する”各ログエントリは次の3点に答える。何が起きたか、どのドメインオブジェクトに関する事象か、なぜ重要か。ログはドメイン不変条件の内部ではなく、ユースケース、アプリケーションサービス、アダプターから出力する。
- 意味のあるメッセージ: 関数名ではなくdomain用語でイベントや判断を述べる。「
assign_driver called」より「driver assigned to waiting request」。 - Domain オブジェクト state: 判断理解に必要なidentifier、現state variant、値。補間文字列よりstructured field。
- 遷移情報: 操作目的がstate遷移なら、source state、target state、トリガー commandまたはevent。
#[derive(Clone, Debug)]pub struct AssignDriverLog { request_id: RequestId, passenger_id: PassengerId, driver_id: DriverId, from: TaxiRequestState, to: TaxiRequestState, triggered_by: CommandId,}
tracing::info!( request_id = %log.request_id, passenger_id = %log.passenger_id, // safe only for opaque surrogate IDs driver_id = %log.driver_id, from = ?log.from, to = ?log.to, triggered_by = %log.triggered_by, "driver assigned to waiting request");構造化ログを優先する
Section titled “構造化ログを優先する”人間可読文から意味をparseせず、key-value fieldを使う。集約はmessageでグループ化し、fieldによるfilterができるようlogテンプレートを安定させる。
// Good: stable template, structured fields.tracing::info!( request_id = %request_id, state = ?state, "request state persisted");
// Avoid: values baked into the message text.tracing::info!("request {} persisted in state {:?}", request_id, state);logレベルを意図的に選ぶ:
ERROR: domain不変条件失敗、ユースケース完了不能、インフラ依存unhealthy。secretを漏らさず再現に足るcontext。WARN: リトライ可能timeoutなど回復可能異常、予期外だが処理済みedge case。INFO: 重要ビジネスイベントまたはライフサイクルstep。DEBUG: 特定問題診断向け詳細state。高コスト値はmatching levelのときだけ評価されるtracing::debug!でguard。
log から PII 漏洩を防ぐ
Section titled “log から PII 漏洩を防ぐ”logは長寿命で広くアクセス可能: 公開境界として扱う。PII 保護 のルールに従う。
- raw氏名、メール、電話、住所、位置、token、資格情報をlogしない。
- newtypeとredacting wrapperで
Debugderiveや値補間のaccidental露出を防ぐ。 - identifierがsensitiveならhashまたはopaque referenceをlog。
分類ルールは どの ID を log に載せるか 参照。
// Good: only non-sensitive identifiers and states appear in logs.// `passenger_id` is safe here only because it is an opaque surrogate, not email/phone.tracing::info!( request_id = %request_id, passenger_id = %passenger_id, state = ?state, "request transitioned to en-route");
// Avoid: a raw email would leak into log storage.tracing::info!("notification sent to {}", email);どの ID を log に載せるか
Section titled “どの ID を log に載せるか”identifierを分類してからlog、span、metrics、errorへ到達させる。フィールド名はsafeを決めない。identifierの意味、 derivation、再識別リスクが決める。
デフォルト: log してよい
Section titled “デフォルト: log してよい”運用相関に役立ちsecretや直接個人identityを露出しないとき:
| 種別 | 例 | 通常安全な理由 |
|---|---|---|
| Correlation / tracing | correlation_id, trace_id, span_id, request_id (HTTP) | 一時的または運用向け。identity ではない |
| Internal aggregate IDs | order_id, request_id, shipment_id, command_id, event_id | サービス内 opaque surrogate key |
| Process / job IDs | job_id, outbox_id, batch_id, transaction_id (internal) | インフラ相関 |
| Tenant / org context | tenant_id, organization_id, fleet_id | アクセス制御下 multi-tenant ops に必要 |
| Bounded domain enums | state, command_name, event_type, error_code | 低 cardinality。個人データではない |
「logしてよい」の要件:
- Opaque surrogate: システム内ランダムまたはsequential。email、phone、氏名、政府ID、カードデータ由来でない。
- Secret ではない: session token、API key、password、signed URL capabilityではない。
- 単体再識別リスク低: 値単体がアプリ制御datastore外で自然人を特定しない。
- Safe
Display/Debug: newtypeのformatting経路がlog向けにreview済みでnested PIIを露出しない。
// Safe: opaque surrogate IDs with explicit logging newtypes.tracing::info!( request_id = %request_id, command_id = %command_id, correlation_id = %correlation_id, state = ?state, "request transitioned to en-route");デフォルト: log しない
Section titled “デフォルト: log しない”一般application log、span、metrics label、error文字列に載せない:
| 種別 | 例 | 理由 |
|---|---|---|
| Secrets / auth material | API keys, passwords, session tokens, refresh tokens, HMAC secrets, signed download URLs | 資格情報漏洩 |
| Government / regulated IDs | SSN, My Number, passport, driver’s license, national health ID | 直接個人 identity |
| Payment identifiers | PAN, CVV, full bank account, raw payment-method tokens from PSPs | PCI / 金融 exposure |
| Contact identity | email, phone, messenger handle when used as account identity | 直接 PII |
| Person descriptors | legal name, birth date, address, free-text notes about a person | 直接 PII |
| Health / special-category data | diagnosis, prescription, patient notes | 規制 sensitive data |
| Precise location | lat/long, full street address, room-level indoor position | 位置プライバシー |
| Network identity | client IP, device fingerprint, advertising ID | 多法域で tracking / PII |
| External IDs that embed PII | user@example.com as key, hashed email in reversible scheme, provider subject that is an email | PII が「ID」として混入 |
インシデントでこれらが必要なら、明示認可付きrestricted audit exportまたはsupportツールへ。一般log retentionを広げて載せない。
条件付き: domain model で分類
Section titled “条件付き: domain model で分類”よくありlogされうるが、プロジェクト明示判断後のみ。型と Display/Debug 契約に符号化。
| 種別 | ログしてよいとき | ログしないとき |
|---|---|---|
user_id, passenger_id, customer_id, patient_id | 自システム発行 opaque surrogate UUID/ULID | 値が email/phone、政府 ID、provider subject、PII 可逆 hash |
account_id, profile_id | login identifier と無関係な内部 account key | login 名や人に紐づく public profile slug と同一 |
driver_id, staff_id, provider_id | 運用向け内部 workforce/resource key | log で直接個人 identity または legal name と 1:1 |
device_id, installation_id | tracking リスク方針が低い opaque app 生成 surrogate | vendor advertising ID または hardware serial |
external_id, partner_ref | 契約上 ops log 可な opaque partner reference | partner 供給値に email、phone、national ID |
| Hashed identifier | セキュリティ review 済み pepper/HMAC pseudonym | システム横 fast hash of email/phone |
条件付きIDをlog可能にするとき PassengerId や CorrelationId 等named newtype。log不可は Redacted<T>、SecretString、approved adapter外で Display 意図的unavailable。
metric と span の ID ルール
Section titled “metric と span の ID ルール”log safeなIDがmetric labelで自動safeではない。
- Log する: backendが許容するrequestあたりcardinalityならlog fieldとtrace attributeにaggregate ID
- metric label にしない: raw user/customer/passenger ID、timestamp、email、phone、IP、無界string。
state、command、error_code、tenant_idなど有界domain label(cardinality既知)
// Good metric labels: bounded domain vocabulary.metrics::counter!("taxi_request.driver_assigned", "fleet" => fleet.as_str()).increment(1);
// Avoid: per-user metric labels explode cardinality and leak identity into TSDB.metrics::counter!("notification.sent", "user_id" => user_id.as_str()).increment(1);クイック判断チェックリスト
Section titled “クイック判断チェックリスト”log行にIDを足す前:
- secretまたはauth tokenか。Yesならlogしない。
- 直接PIIまたは規制identifierか。Yesならlogしない。
- 埋め込みPIIなし自システムopaque surrogateか。Yesなら通常log可。
- このfield(
Display/span/metric label)で意図以上を露出しないか。Yesならredact、approved schemeでhash、restricted auditのみ。 - 型のformattingがsafe logging向けreview済みか。Noならlog前に型を直す。
state 遷移を明示的に log
Section titled “state 遷移を明示的に log”state遷移はdomain振る舞いの中心。before/after stateをlogしtrace、audit、インシデント調査でライフサイクル再構築可能に。
遷移がeventを出すとき、payload全体ではなくevent名またはtype(payloadがsafeでopsに有用な場合を除く)。
let outcome = waiting_request.assign_driver(driver)?;
tracing::info!( request_id = %outcome.state.request_id, from = "waiting", to = "en-route", events = ?outcome.events.iter().map(|e| e.name()).collect::<Vec<_>>(), "driver assignment completed");domainレベルlogはトランザクションを所有するユースケース近く。getterやvalidation helper各所に散らさない。
error を actionable に
Section titled “error を actionable に”domain errorに失敗経路と影響objectを追跡できるcontext。周囲ユースケースのstructured identifierを再利用。ad-hocラベルを作らない。
match repository.find_by_id(&request_id).await { Ok(Some(request)) => request, Ok(None) => { tracing::warn!(request_id = %request_id, "request not found"); return Err(AssignDriverError::RequestNotFound { request_id }); } Err(e) => { tracing::error!(request_id = %request_id, error = %e, "repository lookup failed"); return Err(AssignDriverError::Repository(e)); }}各層で同一失敗をlogしない。ユースケースまたはapplication serviceが権威log行を所有しtyped errorを上へ。
構造化ログと error chain 統合
Section titled “構造化ログと error chain 統合”thiserror source chainと tracing fieldを連携し、1 log行でdomain contextと根因を見せる。
if let Err(error) = self.execute(request_id, driver).await { tracing::error!( request_id = %request_id, driver_id = %driver.id, error = %error, // full Display chain via thiserror error.debug = ?error, // optional: Debug for support tooling "assign driver use case failed" ); return Err(error);}ガイドライン:
thiserrorenumでは%errorで#[source]原因を順序表示- domain field(
request_id、command、error_code)をerrorのDisplay内ではなく横に - ユースケースabort時active spanにerror記録:
tracing::Span::current().record("error", tracing::field::display(&error));- raw client errorを意味論variantへマップしてからendpoint、SQL、secretを漏らさない
- enum variant由来
error_code等bounded labelでmetric increment。full error textではない
error enum設計は エラーハンドリング と照合。ユースケースがricher domain contextで同一失敗をlog済みならrepository adapterで重複logしない。
有用なときだけ tracing
Section titled “有用なときだけ tracing”tracing はガイドラインの便利実装だが必須依存ではない。structured log、span、相関が必要なプロジェクトでは tracing を使う。そうでなければ、同じ原則をプロジェクトのlogging facadeまたはcustom writerに適用する。
tracing 使用時:
- spanはユースケース/application service境界。internal helper各所ではない。spanは操作名とaggregate identifierを運ぶ
- 広いauto-derived fieldより明示field listの
#[instrument]。raw DTOやsensitive payloadを受ける関数はexcludeしない限りinstrumentしない - redaction方針に合うfield値syntax。
%fieldはDisplay、?fieldはDebug。PIIを含むdomain objectでは両方safe表現
#[tracing::instrument( name = "use_case.assign_driver", skip(driver), // skip fields that need manual redaction fields(request_id = %request_id, driver_id = %driver.id))]pub async fn assign_driver( &self, request_id: RequestId, driver: DriverAssignment,) -> Result<Transition<EnRouteRequest, TaxiRequestEvent>, AssignDriverError> { // ...}tracing spanをdomain eventやaudit記録と混同しない。observability補助。耐久性はdomain event型またはoutbox。
domain outcome を計測
Section titled “domain outcome を計測”metricsはruntime機構だけでなくビジネスoutcomeを反映。このskillがモデル化するdomain概念に整合。
- Counters: 遷移、command受理/拒否、published event等
- Histograms: 各aggregate state滞在時間、ユースケース実行latency等意味dimension付きduration/size
- Gauges: 現在waiting request数等point-in-state
metrics::counter!("taxi_request.driver_assigned", "fleet" => fleet.as_str()).increment(1);metrics::histogram!("taxi_request.state_duration_seconds", "from" => "waiting", "to" => "en-route") .record(duration.as_secs_f64());domain型由来の一貫label。TSDB向けcardinality低く。raw IDやtimestampより有界state/command名セット。
OpenTelemetry で telemetry export
Section titled “OpenTelemetry で telemetry export”log、metrics、traceのobservability backend exportにはOpenTelemetryをapplicationレベルdefault。domain/use-caseはfacade API(tracing、metrics)に留め、exporterはapplication startupのみ。
facadeは自動OTel接続しない。startupでbridge crate:
tracingspan/trace:tracing-opentelemetry等metrics:metrics-exporter-otel、metrics-opentelemetry等OTelMeter転送recorder
Prometheus scrape向け /metrics はoptional。デプロイがOTLPを支持すればOTLP export優先。scraping必須時のみPrometheus text exporter。legacy opentelemetry-prometheus はdeprecated。text expositionは opentelemetry-prometheus-text-exporter またはcollector経由OTLP metrics。
domain/application層を特定exporter向けに設計しない。
// Application startup, not domain code.use opentelemetry::global;use opentelemetry::metrics::MeterProvider;use opentelemetry_sdk::metrics::SdkMeterProvider;use opentelemetry_prometheus_text_exporter::PrometheusExporter;
// Bridge `metrics` facade recordings into OpenTelemetry.use metrics_exporter_otel::OpenTelemetryRecorder;
let exporter = PrometheusExporter::builder().build();let provider = SdkMeterProvider::builder() .with_reader(exporter) .build();
let meter = provider.meter(env!("CARGO_PKG_NAME"));let recorder = OpenTelemetryRecorder::new(meter);metrics::set_global_recorder(recorder).expect("install metrics recorder");
global::set_meter_provider(provider.clone());
// For `tracing`, install a `tracing-opentelemetry` layer separately.log と metrics を相関
Section titled “log と metrics を相関”request、command、transaction全体でcorrelation identifierを運ぶ。structured logに含め、実用的ならmetric labelまたはtrace attributeでlog/metrics/trace間pivot。
let correlation_id = CorrelationId::generate();tracing::Span::current().record("correlation_id", correlation_id.as_str());spanはinternal call各所ではなくユースケース境界。操作名とaggregate identifier。実行thread詳細ではない。
レビューでは、意味のないログメッセージ、ドメイン文脈の欠如、遷移ログの不足、非構造化ログ、ドメイン次元のないメトリクス、高カーディナリティラベル、PII漏洩、誤分類ID、重複エラーログを指摘する。
レビュー観点
Section titled “レビュー観点”マスキングルールは PII 保護 も参照。
PII とシークレットはログ、スパン、メトリクスから除外されているか — High
Section titled “PII とシークレットはログ、スパン、メトリクスから除外されているか — High”PII 保護 も照合する。生の機密値を載せるログフィールド、スパン属性、メトリクスラベル、エラー表示文字列を指摘する。
ドメインオブジェクトが可観測性ヘルパへ到達する前に、Debug 実装、マスキングラッパ、許可リストが一貫して適用されているかも確認する。
ログに載せる ID は正しく分類されているか — High
Section titled “ログに載せる ID は正しく分類されているか — High”本文の「ログに載せるID」節も参照。文書化された安全性ではなくフィールド名の仮定でIDをログする箇所を指摘する。
次を含む場合はエスカレートする:
- シークレット、セッショントークン、APIキー
- 政府、決済、健康、連絡先の本人情報
- 不透明サロゲートでない人物紐づきID(メールをキーにしたID、プロバイダsubject、PIIの可逆ハッシュ)
- メトリクスラベル上の生のuser / customer / passenger ID
型の整形がレビュー済みでPII由来でない場合、不透明サロゲート集約ID(request_id、order_id、correlation_id、内部 transaction_id)には指摘しない。
エラーチェーンはドメイン文脈付きで一度だけログされているか — Medium
Section titled “エラーチェーンはドメイン文脈付きで一度だけログされているか — Medium”logging-metrics.md のエラーチェーン統合節も照合する。同一失敗を各アダプタ層で重複 tracing::error! する、または %error / ソースチェーン整形なしにエラーを文字列化するログを指摘する。
メトリクスのカーディナリティは制御されているか — Medium
Section titled “メトリクスのカーディナリティは制御されているか — Medium”生ID、タイムスタンプ、メールアドレス、無制限文字列をラベルに使う箇所を指摘する。高カーディナリティラベルは時系列ストレージを圧迫し、識別子をメトリクスバックエンドへ漏らす。
ログメッセージは意味があるか — Medium
Section titled “ログメッセージは意味があるか — Medium”関数名だけ、またはドメイン文脈のないログメッセージを指摘する。
良いログメッセージはビジネス用語で何が起きたかを述べる: "assign_driver called" ではなく "driver assigned to waiting request"。
各ログに影響を受けたドメインオブジェクトの状態が含まれるか — Medium
Section titled “各ログに影響を受けたドメインオブジェクトの状態が含まれるか — Medium”識別子、現在の状態バリアント、判断に必要な値を欠くログを指摘する。構造化フィールドには集約またはエンティティIDと、イベント再構成に必要な状態を載せる。
文の補間より request_id = %request_id, state = ?state を優先する。
状態遷移は明示的にログされているか — Medium
Section titled “状態遷移は明示的にログされているか — Medium”ソースとターゲットの両方の状態、または遷移を起こしたコマンド / イベントを記録しないライフサイクル変更を指摘する。
from / to フィールドの欠落、イベント名の欠落、インフラ内だけのログで、トランザクションを所有するユースケース境界にログがない場合を探す。
エラーメトリクスは境界のあるラベルを使っているか — Low
Section titled “エラーメトリクスは境界のあるラベルを使っているか — Low”生エラーテキスト、SQL断片、無制限文字列をラベルにするカウンタやヒストグラムを指摘する。列挙バリアント名や安定した error_code を使う。
メトリクスはドメイン結果に結びついているか — Low
Section titled “メトリクスはドメイン結果に結びついているか — Low”HTTPステータスコード、スレッド数、汎用ランタイム値だけを数え、ドメイン次元のないメトリクスを指摘する。状態名やコマンド名など境界のあるドメイン値でラベル付けし、ビジネスイベントと状態継続時間を反映するカウンタとヒストグラムを優先する。
ログは構造化され、レベルは適切か — Low
Section titled “ログは構造化され、レベルは適切か — Low”補間値の tracing::info! や println! を指摘する。ヘルパやループで冗長な INFO は DEBUG にすべき。
ERROR ログが本当の失敗経路を示し、シークレットを漏らさず診断に足りる文脈を含むか確認する。