Tichá mezera v metadatech: Proč vaše Axon 5 eventy nemají atribuci uživatele

Když jsme migrovali z Axon Framework 4 na 5, pečlivě jsme aktualizovali agregáty na EventAppender, přepsali command handlery na statické tovární metody a upravili event sourcing handlery. Vše se zkompilovalo. Testy prošly. Eventy proudily. Pak jsme během interního QA kola zpozorovali něco zvláštního: timeline historie agregátu ukazovala eventy bez autora — jen časové značky a typy eventů vznášející se v prázdnu.
Příčina? Metadata příkazů v Axonu se automaticky nepropagují do eventů. A když se soustředíte na velké změny migrace na Axon 5, přesně tohle je věc, která vám proklouzne.
Výchozí stav: Atribuce uživatele přes metadata příkazů
Naše architektura používá AuthAwareCommandGateway — tenký wrapper kolem Axon CommandGateway, který extrahuje autentizovaného uživatele z reaktivního kontextu Spring Security a připojí userId a userName jako metadata příkazu:
@Service
@RequiredArgsConstructor
public class AuthAwareCommandGateway {
private final CommandGateway commandGateway;
private final ReactiveAuthenticationSupplier authenticationSupplier;
public <R> Mono<R> send(Object command, Class<R> resultType) {
return authenticationSupplier.getAuthentication()
.map(auth -> {
Metadata metadata = Metadata.with("userId", auth.getName());
String userName = extractUserName(auth);
if (userName != null && !userName.equals(auth.getName())) {
metadata = metadata.and("userName", userName);
}
return metadata;
})
.defaultIfEmpty(Metadata.emptyInstance())
.flatMap(metadata -> {
CommandResult result = commandGateway.send(command, metadata);
return Mono.fromFuture(result.resultAs(resultType));
});
}
}Na čtecí straně naše služba historie agregátu čte eventy z Axon Serveru přes gRPC a extrahuje userId/userName z metadat eventů pro vykreslení timeline:
● Záznam aktualizován — Petr — před 5 minutami
● Záznam vytvořen — Sofia — před 23 hodinamiJenže se vykreslovalo:
● Záznam aktualizován — před 5 minutami
● Záznam vytvořen — před 23 hodinamiŽádný uživatel. U žádného eventu. U žádného typu entity.
Příčina: Dvě oddělené obálky zpráv
Následující diagram ukazuje celý tok zpráv od GraphQL mutace po event store — a kde dochází k mezeře v metadatech:
ReactiveSecurityContext GW->>GW: Připojí metadata:
{userId, userName} GW->>Bus: dispatch(command + metadata) Bus->>Agg: handle(UpdateRecordCommand, EventAppender) Note over Agg: eventAppender.append(
new RecordUpdatedEvent(...)) rect rgb(255, 230, 230) Note over Agg,ES: ❌ BEZ CorrelationDataProvider
Metadata eventu = {} (prázdná!) end Agg->>ES: uložení eventu Note over ES: Event uložen
BEZ atribuce uživatele
V Axon Frameworku jsou příkaz a eventy, které produkuje, zásadně odlišné zprávy s oddělenými obálkami metadat. Když command handler vašeho agregátu vytváří eventy přes EventAppender.append():
@CommandHandler
public void handle(UpdateRecordCommand command, EventAppender eventAppender) {
eventAppender.append(new RecordUpdatedEvent(command.recordId(), command.data()));
}RecordUpdatedEvent začíná s čistou, prázdnou mapou metadat. userId a userName, které byly připojeny k UpdateRecordCommand, zůstávají na příkazu — magicky se nepřenesou na event.
Je to záměr. Ne všechna metadata příkazů by měla být persistována do event store. Auth tokeny, korelační ID pro tracing, čítače opakování — to pravděpodobně nechcete ukládat permanentně s každým eventem. Axon dělá propagaci metadat explicitní, opt-in záležitostí.
Oprava: CorrelationDataProvider
Axon poskytuje rozhraní CorrelationDataProvider přesně pro tento účel. Odpovídá na otázku: „Když se zpracovává zpráva, které klíče metadat se mají zkopírovat z příchozí zprávy do všech zpráv vytvořených během zpracování?"
Nejjednodušší implementace je SimpleCorrelationDataProvider, který kopíruje pojmenované klíče metadat:
@Configuration
public class AxonCorrelationDataConfig {
@Bean
public CorrelationDataProvider userMetadataCorrelationDataProvider() {
return new SimpleCorrelationDataProvider("userId", "userName");
}
}To je vše. Jeden bean. CorrelationDataProviderAutoConfiguration v Axon 5 automaticky objeví CorrelationDataProvider beany ve Spring kontextu a zaregistruje je v CorrelationDataProviderRegistry. Od té chvíle každý event vytvořený při zpracování příkazu nesoucího metadata userId nebo userName tyto hodnoty zdědí.
Jak to funguje pod kapotou
Když příkaz dorazí k agregátu, Axon infrastruktura obalí zpracování do ProcessingContext. Před spuštěním command handleru CorrelationDataInterceptor extrahuje korelační data z příchozího příkazu pomocí všech registrovaných providerů. Když se zavolá EventAppender.append(), metadata připojeného eventu se obohatí o korelační data předtím, než se event uloží do event store.
{userId, userName} Bus->>CDP: intercept before handling Note over CDP: Extrahuje korelační klíče
z metadat příkazu CDP->>Agg: handle(UpdateRecordCommand, EventAppender) Agg->>EA: append(RecordUpdatedEvent) rect rgb(230, 255, 230) Note over EA: ✅ Obohatí metadata eventu
o korelační data:
{userId, userName} end EA->>ES: uložení eventu s kompletními metadaty Note over ES: Event uložen
S atribucí uživatele ✅
Proč to bylo těžké odhalit
Několik faktorů dělalo tento bug obzvlášť zákeřným:
Žádná chyba, žádná výjimka. Eventy se ukládají naprosto v pořádku s prázdnými metadaty. Agregát funguje. Projekce se aktualizují. Jediným příznakem je chybějící atribuce uživatele v UI, což se při vývoji snadno přehlédne.
Axon 4 měl stejný požadavek. Tohle není novinka v Axon 5. V Axon 4 jste také potřebovali
CorrelationDataProvider— typicky konfigurovaný přesConfigurer.configureCorrelationDataProviders(). Pokud jste ho nikdy nenakonfigurovali ani v Axon 4, nikdy jste neměli atribuci uživatele v eventech a migrace na tom nic nezměnila.Command gateway „funguje".
AuthAwareCommandGatewaysprávně připojuje metadata k příkazům. Logování to potvrzuje. Pokud debugujete odesílání příkazů, vše vypadá správně. Mezera je neviditelná, dokud neinspektujete uložené eventy v Axon Serveru.Testy mockují event store. Unit testy pro command handlery ověřují, že se emitují správné eventy, ale netestují propagaci metadat — to je infrastrukturní chování mimo odpovědnost agregátu.
Více než identita uživatele: Co dalšího propagovat
Jakmile máte vzor zavedený, zvažte propagaci dalších průřezových záležitostí:
@Bean
public CorrelationDataProvider auditCorrelationDataProvider() {
return new SimpleCorrelationDataProvider(
"userId",
"userName",
"traceId", // Korelace distribuovaného tracingu
"sourceSystem" // Webové UI vs API vs plánovaná úloha
);
}traceId je obzvlášť cenný pro korelaci eventů zpět k původnímu HTTP požadavku v systémech distribuovaného tracingu jako OpenTelemetry. A sourceSystem vám umožní rozlišit, zda byla změna provedena přes webové UI, plánovanou úlohu nebo API integraci — užitečné jak pro audit trail, tak pro debugging.
Klíčové poznatky
Metadata příkazů se nepropagují do eventů automaticky. Platí to jak pro Axon 4, tak pro 5. Zaregistrujte
CorrelationDataProviderbean pro všechny klíče metadat, které chcete mít na svých eventech.Tichá ztráta dat je nejhorší druh bugu. Žádné výjimky, žádné padající testy — jen prázdná pole ve vašem UI. Přidejte do integračních testů aserce, které ověří, že metadata eventů obsahují očekávané klíče.
Autokonfigurace Spring Boot v Axon 5 je váš přítel. Stačí definovat
CorrelationDataProviderbeany a budou automaticky nalezeny — žádná ruční voláníConfigurernejsou potřeba.Historické eventy se zpětně neopraví. Eventy již uložené bez uživatelských metadat takové zůstanou. Pouze nové eventy vytvořené po opravě ponesou atribuci uživatele. Pokud potřebujete zpětnou atribuci, museli byste upcastovat historické eventy — nebo přijmout mezeru.
Titulní foto: Conny Schneider na Unsplash


