Pular para conteúdo

Audit e Change Tracking

Esta página detalha a direção arquitetural esperada pelo ADR-0008.

O objetivo é separar com clareza:

  • audit: rastreabilidade do fato, ator, origem e mudança observável;
  • revision: reconstrução estrutural de estado;
  • Decorator: mecanismo preferencial quando audit ou revision precisam de consistência forte com a operação principal;
  • listener: mecanismo lateral para reporting, integração externa e reações secundárias.

Regra principal

Quando audit ou revision não podem virar best effort, o caso deve ser anexado por Decorator.

Isso vale principalmente para:

  • alteração de estado relevante de item;
  • mudanças administrativas;
  • ações cujo rastro precisa existir junto com a mutação principal.

Listeners continuam válidos, mas para consumidores laterais.

Política explícita por caso

Cada caso auditável deve declarar explicitamente:

  • qual signal ou ocorrência despachada origina o rastro;
  • qual sujeito foi afetado, quando existir;
  • qual canal executou a ação;
  • se usa diff, snapshot ou ambos;
  • se exige consistência forte.

Exemplo educacional:

<?php

namespace local_middag\framework\audit_policy\item;

final class item_status_updated_policy
{
    public const ORIGIN = \local_middag\framework\signal\item\status_updated_signal::class;
    public const SUBJECT = 'item';
    public const CHANNEL = 'web';
    public const USE_DIFF = true;
    public const USE_SNAPSHOT = false;
    public const STRONG_CONSISTENCY = true;
}

Como cada camada deve aplicar

Framework

No core, Decorator é o caminho preferencial quando o rastro deve ser persistido junto com a operação.

Exemplo educacional:

<?php

final class audited_status_updater implements status_updater_interface
{
    public function __construct(
        private status_updater_interface $inner,
        private audit_writer_interface $audit_writer,
        private revision_writer_interface $revision_writer,
    ) {}

    public function update(int $item_id, string $status): void
    {
        $before = ['status' => 'draft'];

        $this->inner->update($item_id, $status);

        $this->revision_writer->capture(
            item_id: $item_id,
            snapshot: ['status' => $status],
        );

        $this->audit_writer->record(
            origin: \local_middag\framework\signal\item\status_updated_signal::class,
            subject: 'item',
            subject_id: $item_id,
            diff: ['status' => ['old' => $before['status'], 'new' => $status]],
        );
    }
}

Extension do ecossistema

A extension segue o mesmo padrão quando o seu próprio caso de uso exige rastreabilidade forte.

Exemplo educacional:

<?php

final class audited_order_processor implements order_processor_interface
{
    public function __construct(
        private order_processor_interface $inner,
        private audit_writer_interface $audit_writer,
    ) {}

    public function process(int $order_item_id): void
    {
        $this->inner->process($order_item_id);

        $this->audit_writer->record(
            origin: \local_middag\extensions\ecommerce\signal\order_processed_signal::class,
            subject: 'item',
            subject_id: $order_item_id,
            diff: null,
            snapshot: ['status' => 'processed'],
        );
    }
}

Diferença prática

No exemplo acima, a extension escolhe snapshot porque o valor comparável não é o ponto principal do caso. Se houver alteração campo a campo relevante, diff continua sendo o padrão.

Plugin terceiro

O plugin terceiro pode consumir hooks públicos ou seus próprios signals para manter rastreio externo, sem alterar o schema interno do MIDDAG.

Exemplo educacional:

<?php

use local_middag\middag;

middag::add_action('middag/extension/ecommerce/order_processed', function (object $payload): void {
    external_audit_store::append([
        'hook' => 'middag/extension/ecommerce/order_processed',
        'data' => method_exists($payload, 'to_array') ? $payload->to_array() : [],
    ]);
});

Interface externa

O plugin terceiro pode manter rastreio próprio, mas isso não redefine a política de audit do core nem das extensions internas.

Quando usar diff

Use diff como padrão quando houver mudança comparável.

Exemplo educacional:

<?php

$diff = [
    'status' => ['old' => 'draft', 'new' => 'published'],
    'visibility' => ['old' => 0, 'new' => 1],
];

Isso responde melhor à pergunta: “o que mudou?”

Quando usar snapshot

Use snapshot quando diff não bastar para troubleshooting, compliance ou reconstrução contextual.

Exemplo educacional:

<?php

$snapshot = [
    'order_id' => 10,
    'status' => 'processed',
    'gateway' => 'shopify',
];

Isso responde melhor à pergunta: “em que estado relevante isso estava?”

Quando usar ambos

Use os dois quando o caso exigir:

  • comparação objetiva do que mudou;
  • e contexto suficiente para reconstrução posterior.

Exemplo educacional:

<?php

$audit_writer->record(
    origin: order_processed_signal::class,
    subject: 'item',
    subject_id: 10,
    diff: ['status' => ['old' => 'pending', 'new' => 'processed']],
    snapshot: ['gateway' => 'shopify', 'channel' => 'api'],
);

Listener lateral continua válido

Quando o objetivo for integração externa, analytics ou reporting, listener continua sendo um caminho válido.

Exemplo educacional:

<?php

final class send_audit_to_bigquery
{
    public function __invoke(
        \local_middag\extensions\ecommerce\signal\order_processed_signal $signal
    ): void {
        // reporting externo
    }
}

O que não fazer

  • não depender só de listener para audit ou revision que exigem consistência forte;
  • não registrar audit automático para toda ocorrência despachada;
  • não tratar revision como sinônimo de audit;
  • não acoplar plugin terceiro ao schema interno de auditoria do MIDDAG.