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

    ロギングとメトリクス

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

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

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

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

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

    1. 意味のあるメッセージ: 関数名ではなくdomain用語でイベントや判断を述べる。「assignDriver called」より「driver assigned to waiting request」。
    2. Domain オブジェクト state: 判断理解に必要なidentifier、現state variant、値。structured fieldを優先する。
    3. 遷移情報: 操作目的がstate遷移なら、source state、target state、トリガー commandまたはevent。
    import org.typelevel.log4cats.Logger
    import org.typelevel.log4cats.syntax.*
    final case class AssignDriverLog(
    requestId: RequestId,
    passengerId: PassengerId,
    driverId: DriverId,
    from: TaxiRequestState,
    to: TaxiRequestState,
    commandId: CommandId
    )
    def logAssignment[F[_]: Logger](log: AssignDriverLog): F[Unit] =
    Logger[F].info(
    s"driver assigned to waiting request" +
    s" requestId=${log.requestId.value}" +
    s" passengerId=${log.passengerId.value}" + // safe only for opaque surrogate IDs
    s" driverId=${log.driverId.value}" +
    s" from=${log.from}" +
    s" to=${log.to}" +
    s" commandId=${log.commandId.value}"
    )

    structured logging API(log4cats、key-value marker付きSLF4J、OpenTelemetry log attribute)を、entity全体の文字列補間より優先する。

    人間可読文から意味をparseせず、key-value fieldを使う。集約はmessageでグループ化し、fieldによるfilterができるようlogテンプレートを安定させる。

    // Good: stable template, structured context map or MDC.
    Logger[F].info(Map(
    "requestId" -> requestId.value,
    "state" -> state.toString
    ))("request state persisted")
    // Avoid: values baked only into free-form sentence text.
    Logger[F].info(s"request ${requestId.value} persisted in state $state")

    logレベルを意図的に選ぶ:

    • ERROR: domain不変条件失敗、ユースケース完了不能、インフラ依存unhealthy。secretを漏らさず再現に足るcontext。
    • WARN: リトライ可能timeoutなど回復可能異常、予期外だが処理済みedge case。
    • INFO: 重要ビジネスイベントまたはライフサイクルstep。
    • DEBUG: 特定問題診断向け詳細state。高コスト値はDEBUGでguard。

    logは長寿命で広くアクセス可能: 公開境界として扱う。PII 保護ライブラリガイド(secrets) のルールに従う。

    • raw氏名、メール、電話、住所、位置、token、資格情報をlogしない。
    • opaque型とredacting wrapperで toString とstructured fieldのaccidental露出を防ぐ。
    • identifierがsensitiveならhashまたはopaque referenceをlog。

    分類ルールは どの ID を log に載せるか 参照。

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

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

    種別通常安全な理由
    Correlation / tracingcorrelationId, traceId, spanId, HTTP requestId一時的または運用向け。identity ではない
    Internal aggregate IDsorderId, requestId, shipmentId, commandId, eventIdサービス内 opaque surrogate key
    Process / job IDsjobId, outboxId, batchId, internal transactionIdインフラ相関
    Tenant / org contexttenantId, organizationId, fleetIdアクセス制御下 multi-tenant ops に必要
    Bounded domain enumsstate, commandName, eventType, errorCode低 cardinality。個人データではない

    「logしてよい」の要件:

    1. Opaque surrogate: システム内ランダムまたはsequential。email、phone、氏名、政府ID、カードデータ由来でない。
    2. Secret ではない: session token、API key、password、signed URL capabilityではない。
    3. 単体再識別リスク低: 値単体がアプリ制御datastore外で自然人を特定しない。
    4. Safe toString / Show: opaque型のformatting経路がlog向けにreview済みでnested PIIを露出しない。

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

    種別理由
    Secrets / auth materialAPI keys, passwords, session tokens, signed URLs資格情報漏洩
    Government / regulated IDsSSN, passport, national health ID直接個人 identity
    Payment identifiersPAN, CVV, full bank accountPCI exposure
    Contact identityemail, phone used as account identity直接 PII
    Person descriptorslegal name, birth date, address, notes about a person直接 PII
    Health datadiagnosis, prescription規制 sensitive data
    Precise locationlat/long, full street address位置プライバシー
    Network identityclient IP, device fingerprinttracking / PII

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

    種別ログしてよいときログしないとき
    userId, passengerId, customerId自システム発行 opaque surrogate UUID/ULID値が email/phone、政府 ID、provider subject、PII 可逆 hash
    deviceId, installationIdtracking リスク方針が低い opaque app 生成 surrogatevendor advertising ID または hardware serial
    externalId, partnerRef契約上 ops log 可な opaque partner referencepartner 供給値に email、phone、national ID

    条件付きIDをlog可能にするときnamed opaque型。log不可は Redacted[T]、adapter-only exposePII 保護 参照)。

    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。statecommanderrorCode、有界 tenantId など有界domain label
    // Good: bounded domain vocabulary.
    metrics.counter("taxi_request.driver_assigned", "fleet" -> fleet.value).increment()
    // Avoid: per-user labels explode cardinality and leak identity.
    metrics.counter("notification.sent", "userId" -> userId.value).increment()

    log行にIDを足す前:

    1. secretまたはauth tokenか。Yesならlogしない。
    2. 直接PIIまたは規制identifierか。Yesならlogしない。
    3. 埋め込みPIIなし自システムopaque surrogateか。Yesなら通常log可。
    4. このfield(toString/span/metric label)で意図以上を露出しないか。Yesならredact、restricted auditのみ。
    5. 型のformattingがsafe logging向けreview済みか。Noならlog前に型を直す。

    state遷移はdomain振る舞いの中心。before/after stateをlogしtrace、audit、インシデント調査でライフサイクル再構築可能に。

    遷移がeventを出すとき、payload全体ではなくevent名またはtype(payloadがsafeでopsに有用な場合を除く)。

    Logger[F].info(Map(
    "requestId" -> outcome.state.requestId.value,
    "from" -> "waiting",
    "to" -> "en-route",
    "events" -> outcome.events.map(_.name).mkString(",")
    ))("driver assignment completed")

    domainレベルlogはトランザクションを所有するユースケース近く。getterやvalidation helper各所に散らさない。

    domain errorに失敗経路と影響objectを追跡できるcontext。周囲ユースケースのstructured identifierを再利用。ad-hocラベルを作らない。

    repository.findWaiting(requestId).flatMap {
    case None =>
    Logger[F].warn(Map("requestId" -> requestId.value))("request not found") >>
    ME.raiseError(AssignDriverError.RequestNotFound(requestId))
    case Some(waiting) =>
    ME.pure(waiting)
    }

    各層で同一失敗をlogしない。ユースケースまたはapplication serviceが権威log行を所有しtyped errorを上へ。

    Either error ADTとloggingを連携し、1 log行でdomain contextと根因を見せる。

    execute(requestId, driver).flatMap {
    case Left(error) =>
    Logger[F].error(Map(
    "requestId" -> requestId.value,
    "driverId" -> driver.id.value,
    "error" -> error.toString // ADT with safe Display, not raw PII
    ))("assign driver use case failed") >>
    ME.raiseError(error)
    case Right(value) =>
    ME.pure(value)
    }

    ガイドライン:

    • error ADTの toString はnested error経由で cause を含める
    • domain field(requestIdcommanderrorCode)をerrorの toString 内ではなく横に
    • raw client errorを意味論variantへマップしてからendpoint、SQL、secretを漏らさない
    • enum variant由来 errorCode 等bounded labelでmetric increment。full error textではない

    error ADT設計は エラーハンドリング と照合。ユースケースがricher domain contextで同一失敗をlog済みならrepository adapterで重複logしない。

    Tracing と span(trace4cats / OpenTelemetry)

    Section titled “Tracing と span(trace4cats / OpenTelemetry)”

    trace4catsまたはOpenTelemetryを使うプロジェクトでは:

    • spanはユースケース/application service境界。internal helper各所ではない
    • span名は操作(use_case.assign_driver)、aggregate IDを運ぶ
    • attributeは明示追加。raw DTOやpatient/user struct全体をauto-serializeしない
    • logと同じID分類ルールを適用
    import trace4cats.Span
    Span[F].trace("use_case.assign_driver") {
    Span[F].putAll(
    "requestId" -> requestId.value,
    "driverId" -> driver.id.value
    ) >> /* ... */
    }

    spanをdomain eventやaudit記録と混同しない。observability補助。耐久性はdomain event型またはoutbox。

    metricsはruntime機構だけでなくビジネスoutcomeを反映。

    • Counters: 遷移、command受理/拒否、published event等
    • Histograms: 各aggregate state滞在時間、ユースケース実行latency等
    • Gauges: 現在waiting request数等point-in-state

    domain型由来の一貫label。TSDB向けcardinality低く。raw IDやtimestampより有界state/command名セット。

    exporterはapplication startupのみ。domain/use-caseはfacade API(log4cats、Micrometer)に留める。

    一般的なScalaスタック:

    • Metrics: Micrometer + OTLP、またはOpenTelemetry Java SDKをCats Effectアプリのcomposition rootでbridge
    • Tracing: trace4cats OTLP exporter、またはJVMサービス向けOpenTelemetry agent
    • Logs: logback JSON appenderまたはOTLP log exporter

    domain/application層を特定vendor backend向けに設計しない。

    request、command、transaction全体でcorrelation identifierを運ぶ。structured logに含め、実用的ならtrace attributeでlog/metrics/trace間pivot。

    val correlationId = CorrelationId.generate()
    // MDC, span attribute, or log context map

    spanはinternal call各所ではなくユースケース境界。操作名とaggregate identifier。実行thread詳細ではない。

    レビューでは、意味のないログメッセージ、ドメイン文脈の欠如、遷移ログの不足、非構造化ログ、ドメイン次元のないメトリクス、高カーディナリティラベル、PII漏洩、誤分類ID、重複エラーログを指摘する。

    マスキングルールは PII 保護 も参照。

    PII とシークレットはログ、スパン、メトリクスから除外されているか — High

    Section titled “PII とシークレットはログ、スパン、メトリクスから除外されているか — High”

    PII 保護 も照合する。生の機密値を載せるログフィールド、スパン属性、メトリクスラベル、エラー表示文字列を指摘する。

    ログに載せる ID は正しく分類されているか — High

    Section titled “ログに載せる ID は正しく分類されているか — High”

    本文の「ログに載せるID」節も参照。文書化された安全性ではなくフィールド名の仮定でIDをログする箇所を指摘する。

    型の整形がレビュー済みでPII由来でない場合、不透明サロゲート集約ID(requestIdorderIdcorrelationId、内部 transactionId)には指摘しない。

    エラーチェーンはドメイン文脈付きで一度だけログされているか — Medium

    Section titled “エラーチェーンはドメイン文脈付きで一度だけログされているか — Medium”

    同一失敗を各アダプタ層で重複logする、またはerror ADTのsafe toString なしにエラーを文字列化するログを指摘する。

    メトリクスのカーディナリティは制御されているか — Medium

    Section titled “メトリクスのカーディナリティは制御されているか — Medium”

    生ID、タイムスタンプ、メールアドレス、無制限文字列をラベルに使う箇所を指摘する。

    ログメッセージは意味があるか — Medium

    Section titled “ログメッセージは意味があるか — Medium”

    関数名だけ、またはドメイン文脈のないログメッセージを指摘する。

    各ログに影響を受けたドメインオブジェクトの状態が含まれるか — Medium

    Section titled “各ログに影響を受けたドメインオブジェクトの状態が含まれるか — Medium”

    識別子、現在の状態バリアント、判断に必要な値を欠くログを指摘する。

    状態遷移は明示的にログされているか — Medium

    Section titled “状態遷移は明示的にログされているか — Medium”

    ソースとターゲットの両方の状態、または遷移を起こしたコマンド / イベントを記録しないライフサイクル変更を指摘する。

    エラーメトリクスは境界のあるラベルを使っているか — Low

    Section titled “エラーメトリクスは境界のあるラベルを使っているか — Low”

    生エラーテキスト、SQL断片、無制限文字列をラベルにするカウンタやヒストグラムを指摘する。

    メトリクスはドメイン結果に結びついているか — Low

    Section titled “メトリクスはドメイン結果に結びついているか — Low”

    HTTPステータスコード、スレッド数、汎用ランタイム値だけを数え、ドメイン次元のないメトリクスを指摘する。

    ログは構造化され、レベルは適切か — Low

    Section titled “ログは構造化され、レベルは適切か — Low”

    補間値のみの Logger[F].info(s"...")println を指摘する。