意味的ログ

「記録されるものは記憶となり、記憶されるものは真実となる」

  —オーウェル『1984年』の概念より(1949年)

概要

従来のログ:

[INFO] User registered: alice@example.com
[INFO] Verification passed
[INFO] Insert into users table

意味的ログ:

{
  "open": { "from": "UnverifiedEmail", "to": "RegisteredUser" },
  "events": [
    { "type": "email_format_asserted", "context": { "email": "alice@example.com" } },
    { "type": "user_inserted", "context": { "userId": 42, "email": "alice@example.com" } }
  ],
  "close": { "properties": { "userId": 42, "value": "alice@example.com" } }
}

従来のログは行単位のテキストが時系列に並ぶだけで、どの行が同じ操作に属するかは読み手の推測に委ねられます。

意味的ログでは、ひとつの変容がひとつのJSONに収まります。変容元と変容先、途中の出来事、最終プロパティ — 「何が何になり、なぜそうなったか」が型付きの構造化データとして記録され、JSONスキーマで検証できます。

Be Frameworkには二つの意味的記録の仕組みがあります。

  • $been — Finalオブジェクトが自分の来歴を保持する証明(proof)
  • SemanticLoggerInterface — 階層的な操作を記録するログ(log)
  log $been
性質 descriptive(記述) constitutive(構成)
視点 第三者(観測者) 一人称
問い 何が起きたか なぜ今の私なのか
文法 doing being
役割 記録 証明

$been — 存在証明

FinalオブジェクトのコンストラクタでBeenをインジェクトし、with()で出来事を記録していくと、そのオブジェクトがなぜ今の状態にあるかの証明になります。

final class RegisteredUser
{
    public readonly int $userId;
    public readonly Been $been;

    public function __construct(
        #[Input] string $value,
        #[Inject] EmailVerifier $verifier,
        #[Inject] UserRepository $users,
        #[Inject] Been $been,
    ) {
        if (! $verifier->check($value)) {
            throw new UnbecomingException('email format failed');
        }

        $this->userId = $users->insert(['email' => $value]);
        $this->been = $been
            ->with(new EmailFormatAssertedContext(
                email: $value,
            ))
            ->with(new UserInsertedContext(
                userId: $this->userId,
                email: $value,
            ));
    }
}

Been#[Inject]でDIコンテナから受け取ります。受け取ったBeenにはフレームワークが変容開始時に記録した変容元・変容先の情報がすでに含まれています。開発者はwith()で、Finalオブジェクトの内部でしか知り得ない出来事 — メールを検証した、ユーザーを挿入した — を追記します。

イベントコンテキスト

Beenに渡すイベントはAbstractContextのサブクラスです。

final class EmailFormatAssertedContext extends AbstractContext
{
    public const string TYPE = 'email_format_asserted';
    public const string SCHEMA_URL = 'https://myvendor.example.com/schemas/email-format-asserted.json';

    public function __construct(
        public readonly string $email,
    ) {}
}

TYPEはログ上のイベント種別、SCHEMA_URLはそのイベントのJSONスキーマを指します。コンストラクタのプロパティがそのままJSONのcontextフィールドになります。

DDDでいうドメインイベント — ビジネス上「起きたこと」を表すオブジェクトです。Finalオブジェクトが完了までに経験した事実を、アプリケーション固有のイベントコンテキストとして定義します。

SemanticLoggerInterface — 階層的な操作記録

階層的な操作記録が必要な場合は、SemanticLoggerInterfaceを直接インジェクトします。

final class RegisteredUser
{
    public function __construct(
        #[Input] string $value,
        #[Inject] UserRepository $users,
        #[Inject] SemanticLoggerInterface $logger,
    ) {
        // 意図を宣言(open)
        $id = $logger->open(new DbTransactionContext(table: 'users'));

        // 途中の出来事を記録(event)
        $this->userId = $users->insert(['email' => $value]);
        $logger->event(new RowInsertedContext(userId: $this->userId));

        // 結果を記録(close)
        $logger->close(new TransactionResultContext(committed: true), $id);
    }
}

open/event/closeはKoriym.SemanticLoggerの階層構造をそのまま使います。意図(intent)→ 出来事(occurrences)→ 結果(result)の三層で、ひとまとまりの操作を記録できます。

$beenはFinalオブジェクトが何であるかの証明です。SemanticLoggerInterfaceは従来のログに近い、途中経過の詳細な記録です。通常は$beenで足ります。

変容の自動記録

$beenSemanticLoggerInterfaceとは別に、変容そのものもフレームワークがopen/closeで自動記録します。開発者がこの記録コードを書く必要はありません。

出力されるJSONの全体像です。

{
  "open": {
    "type": "metamorphosis_open",
    "context": {
      "fromClass": "MyVendor\\MyApp\\UnverifiedEmail",
      "beAttribute": "#[Be(RegisteredUser::class)]",
      "immanentSources": {
        "value": "MyVendor\\MyApp\\UnverifiedEmail::value"
      },
      "transcendentSources": {
        "verifier": "MyVendor\\MyApp\\EmailVerifier",
        "users": "MyVendor\\MyApp\\UserRepository",
        "been": "Be\\Framework\\SemanticLog\\Been"
      }
    }
  },
  "events": [
    {
      "type": "email_format_asserted",
      "context": { "email": "alice@example.com" }
    },
    {
      "type": "user_inserted",
      "context": { "userId": 42, "email": "alice@example.com" }
    }
  ],
  "close": {
    "type": "metamorphosis_close",
    "context": {
      "properties": { "userId": 42, "value": "alice@example.com" },
      "be": { "finalClass": "MyVendor\\MyApp\\RegisteredUser" }
    }
  }
}

openが変容の意図(何から何へ、どの材料で)、eventsが$been->with()で記録された出来事、closeが結果(最終プロパティと変容先)です。

ログからDSLへ

従来のログは実行の記録です。コードが走った後に生まれ、デバッグに使われ、やがて消えます。

このJSONは違います。実行の記録であると同時に、変容の仕様でもあり、存在の証明でもあります。どこから来て、どう成ったのか、何であるのかの記録です。「UnverifiedEmailRegisteredUserになる過程でemail_format_asserteduser_insertedが起きる」— これは過去の事実の記述としても、未来の期待の宣言としても読めます。しかも型付きの構造化データなので、AIが読み書きできるDSLとしても機能します。

記録、仕様、証明、DSL。この四つが同じJSONに重なるとき、JSONスキーマによるテスト並みの厳密な検証と、ログからコードを生成しコードからログを生成する循環が可能になりえます。


技術的基盤: Koriym.SemanticLogger

フレームワークの全体像はリファレンスへ ➡️