コンテンツにスキップ
検索語を入力してください

    ロギングとメトリクス

    tracing とメトリクスは障害調査の主経路である。関数名だけのログや、生IDをラベルにしたメトリクスは、原因特定を遅らせるうえ漏洩経路にもなる。

    遷移の記録はユースケース境界で行う(状態遷移)。マスキングとID分類は PII 保護、エラーの一度きりの記録は エラーハンドリング と整合させる。

    ドメインコンテキストで log する

    Section titled “ドメインコンテキストで log する”

    各ログエントリは次の3点に答える。何が起きたか、どのドメインオブジェクトに関する事象か、なぜ重要か。ログはドメイン不変条件の内部ではなく、ユースケース、アプリケーションサービス、アダプターから出力する。

    1. 意味のあるメッセージ: 関数名ではなくdomain用語でイベントや判断を述べる。「assign_driver called」より「driver assigned to waiting request」。
    2. Domain オブジェクト state: 判断理解に必要なidentifier、現state variant、値。補間文字列よりstructured field。
    3. 遷移情報: 操作目的が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"
    );

    人間可読文から意味を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 保護 のルールに従う。

    • raw氏名、メール、電話、住所、位置、token、資格情報をlogしない。
    • newtypeとredacting wrapperで Debug deriveや値補間の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);

    identifierを分類してからlog、span、metrics、errorへ到達させる。フィールド名はsafeを決めない。identifierの意味、 derivation、再識別リスクが決める。

    運用相関に役立ちsecretや直接個人identityを露出しないとき:

    種別通常安全な理由
    Correlation / tracingcorrelation_id, trace_id, span_id, request_id (HTTP)一時的または運用向け。identity ではない
    Internal aggregate IDsorder_id, request_id, shipment_id, command_id, event_idサービス内 opaque surrogate key
    Process / job IDsjob_id, outbox_id, batch_id, transaction_id (internal)インフラ相関
    Tenant / org contexttenant_id, organization_id, fleet_idアクセス制御下 multi-tenant ops に必要
    Bounded domain enumsstate, command_name, event_type, error_code低 cardinality。個人データではない

    「logしてよい」の要件:

    1. Opaque surrogate: システム内ランダムまたはsequential。email、phone、氏名、政府ID、カードデータ由来でない。
    2. Secret ではない: session token、API key、password、signed URL capabilityではない。
    3. 単体再識別リスク低: 値単体がアプリ制御datastore外で自然人を特定しない。
    4. 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"
    );

    一般application log、span、metrics label、error文字列に載せない:

    種別理由
    Secrets / auth materialAPI keys, passwords, session tokens, refresh tokens, HMAC secrets, signed download URLs資格情報漏洩
    Government / regulated IDsSSN, My Number, passport, driver’s license, national health ID直接個人 identity
    Payment identifiersPAN, CVV, full bank account, raw payment-method tokens from PSPsPCI / 金融 exposure
    Contact identityemail, phone, messenger handle when used as account identity直接 PII
    Person descriptorslegal name, birth date, address, free-text notes about a person直接 PII
    Health / special-category datadiagnosis, prescription, patient notes規制 sensitive data
    Precise locationlat/long, full street address, room-level indoor position位置プライバシー
    Network identityclient IP, device fingerprint, advertising ID多法域で tracking / PII
    External IDs that embed PIIuser@example.com as key, hashed email in reversible scheme, provider subject that is an emailPII が「ID」として混入

    インシデントでこれらが必要なら、明示認可付きrestricted audit exportまたはsupportツールへ。一般log retentionを広げて載せない。

    よくあり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_idlogin identifier と無関係な内部 account keylogin 名や人に紐づく public profile slug と同一
    driver_id, staff_id, provider_id運用向け内部 workforce/resource keylog で直接個人 identity または legal name と 1:1
    device_id, installation_idtracking リスク方針が低い opaque app 生成 surrogatevendor advertising ID または hardware serial
    external_id, partner_ref契約上 ops log 可な opaque partner referencepartner 供給値に email、phone、national ID
    Hashed identifierセキュリティ review 済み pepper/HMAC pseudonymシステム横 fast hash of email/phone

    条件付きIDをlog可能にするとき PassengerIdCorrelationId 等named newtype。log不可は Redacted<T>SecretString、approved adapter外で Display 意図的unavailable。

    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。statecommanderror_codetenant_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);

    log行にIDを足す前:

    1. secretまたはauth tokenか。Yesならlogしない。
    2. 直接PIIまたは規制identifierか。Yesならlogしない。
    3. 埋め込みPIIなし自システムopaque surrogateか。Yesなら通常log可。
    4. このfield(Display/span/metric label)で意図以上を露出しないか。Yesならredact、approved schemeでhash、restricted auditのみ。
    5. 型のformattingがsafe logging向けreview済みか。Noなら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各所に散らさない。

    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を上へ。

    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);
    }

    ガイドライン:

    • thiserror enumでは %error#[source] 原因を順序表示
    • domain field(request_idcommanderror_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 はガイドラインの便利実装だが必須依存ではない。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。%fieldDisplay?fieldDebug。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。

    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名セット。

    log、metrics、traceのobservability backend exportにはOpenTelemetryをapplicationレベルdefault。domain/use-caseはfacade API(tracingmetrics)に留め、exporterはapplication startupのみ。

    facadeは自動OTel接続しない。startupでbridge crate:

    • tracing span/trace: tracing-opentelemetry
    • metrics: metrics-exporter-otelmetrics-opentelemetry 等OTel Meter 転送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.

    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、重複エラーログを指摘する。

    マスキングルールは 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_idorder_idcorrelation_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! を指摘する。ヘルパやループで冗長な INFODEBUG にすべき。

    ERROR ログが本当の失敗経路を示し、シークレットを漏らさず診断に足りる文脈を含むか確認する。