- Kotlin 99.2%
- Shell 0.6%
- Dockerfile 0.2%
|
|
||
|---|---|---|
| .clusterfuzzlite | ||
| .github | ||
| .mvn/wrapper | ||
| src | ||
| .gitattributes | ||
| .gitignore | ||
| compose.yml | ||
| GOVERNANCE.md | ||
| LICENSE | ||
| mvnw | ||
| mvnw.cmd | ||
| pom.xml | ||
| publiccode.yml | ||
| README.md | ||
| RELEASING.md | ||
| SUPPORT.md | ||
Logboek Dataverwerkingen JVM implementatie
Dit is een Kotlin implementatie van de - in ontwikkeling zijnde - standaard Logboek Dataverwerkingen (LDV) van Logius. De library is bruikbaar vanuit Kotlin- en Java-projecten die op Quarkus draaien.
Inleiding
Vanuit het programma MijnOverheid Zakelijk sluiten we zoveel mogelijk aan op de standaarden uit het stelsel Generieke Digitale Infrastructuur: https://www.digitaleoverheid.nl/mido/generieke-digitale-infrastructuur-gdi/. Een van de onderdelen daarvan is de standaard Logboek Dataverwerkingen van Logius: Voor meer informatie over de LDV standaard, zie: https://github.com/Logius-standaarden/logboek-dataverwerkingen.
Doel
Dit Open Source project is opgezet om de LDV standaard eenvoudig aan nieuwe of bestaande Quarkus-applicaties (Java of Kotlin) toe te voegen.
Afhankelijkheden
- Quarkus - De library is gebouwd op CDI-interceptors (
@Logboek), JAX-RS (HttpHeaders, client filter) en MicroProfile Config. Quarkus levert deze API's; in de library zijn ze alsprovidedgedeclareerd. Andere runtimes, zoals Spring Boot, worden niet ondersteund. De integratietest voor de interceptorvolgorde draait op Quarkusquarkus.platform.version(nu 3.38.2); de overige dependency-versies staan in depom.xml. - Clickhouse of PostgreSQL database - Voor het opslaan van de logging wordt standaard ClickHouse gebruikt: https://clickhouse.com/. ClickHouse is geoptimaliseerd voor zeer grote volumes. Organisaties die liever PostgreSQL beheren, kunnen dat als backend kiezen via
logboekdataverwerking.dbms=postgresql(zie hieronder). - Verwerkingsactiviteiten register - Bij het loggen van de activiteit wordt verwezen naar een ID van een verwerkingsactiviteit in een activiteiten register. Meer informatie hierover is te vinden in de documentatie van de standaard. Hierbij wordt geen richtlijn opgegeven voor de technische implementatie en deze is daarom niet inbegrepen bij deze implementatie.
Hoe te gebruiken
Om deze package te gebruiken moet je in je (maven) project de volgende variablen in je application.properties file toevoegen:
logboekdataverwerking.enabled=true
logboekdataverwerking.service-name=service-name
# Database backend: 'clickhouse' (standaard) of 'postgresql'.
logboekdataverwerking.dbms=clickhouse
# Optionele OpenTelemetry resource-attributen
# Worden alleen toegevoegd aan de standalone OpenTelemetry resource;
# in een Quarkus-container met quarkus-opentelemetry komen deze uit de Quarkus-config.
logboekdataverwerking.service-version=1.0.0
logboekdataverwerking.deployment-environment=production
# Span processor: 'simple' (standaard, aanbevolen) of 'batch' (afgeraden, niet conform de acknowledgement-MUST).
# Zie 'Span processor en acknowledgement' hieronder voor de trade-off.
logboekdataverwerking.span-processor=simple
# Wat te doen als het Logboek een schrijfactie weigert: 'fail-closed' (standaard) of 'fail-open'.
# Zie 'Span processor en acknowledgement' hieronder.
logboekdataverwerking.write-failure-policy=fail-closed
Configureer daarnaast alleen de backend die je bij dbms koos — niet beide. Bij dbms=clickhouse:
logboekdataverwerking.clickhouse.endpoint=http://localhost:8123
logboekdataverwerking.clickhouse.username=user
logboekdataverwerking.clickhouse.password=password
logboekdataverwerking.clickhouse.database=db_name
logboekdataverwerking.clickhouse.table=table_name
# Optioneel: time-out (seconden) voor ClickHouse-queries en -inserts. Standaard 30.
logboekdataverwerking.clickhouse.query-timeout-seconds=30
Of, bij dbms=postgresql:
logboekdataverwerking.postgresql.url=jdbc:postgresql://localhost:5432/ldv_logging
logboekdataverwerking.postgresql.username=user
logboekdataverwerking.postgresql.password=password
logboekdataverwerking.postgresql.table=spans
# Optioneel: time-out (seconden) voor het controleren of de verbinding nog actief is. Standaard 5.
logboekdataverwerking.postgresql.connection-validation-timeout-seconds=5
of application.yml (hier met dbms: clickhouse; vervang het clickhouse-blok door een postgresql-blok bij dbms: postgresql):
logboekdataverwerking:
enabled: true
service-name: service-name
service-version: 1.0.0
deployment-environment: production
span-processor: simple
write-failure-policy: fail-closed
dbms: clickhouse
clickhouse:
endpoint: http://localhost:8123
username: user
password: password
database: db_name
table: table_name
query-timeout-seconds: 30
Als enabled=true is, valideert de library bij applicatiestart dat alle properties van de gekozen backend aanwezig en niet-leeg zijn (clickhouse.* bij dbms=clickhouse, postgresql.* bij dbms=postgresql). Ontbrekende of lege waarden geven een IllegalStateException met een lijst van de missende keys, in plaats van pas bij de eerste export te falen.
PostgreSQL is een alternatieve backend voor ClickHouse, bruikbaar waar PostgreSQL operationeel beter past. De attributes- en resource-velden worden opgeslagen als jsonb-kolommen.
De JDBC-drivers van beide backends zijn in deze library als optional gemarkeerd: ze komen niet transitief mee, zodat je applicatie alléén de driver van de gekozen backend hoeft te declareren (com.clickhouse:client-v2 óf org.postgresql:postgresql). Kies je een backend zonder de bijbehorende driver, dan faalt de applicatie luid bij start (zie de config-validatie hierboven).
De lokale databases draaien achter een Compose-profiel, zodat je alleen de gekozen backend start:
docker compose --profile clickhouse up -d # standaard backend
docker compose --profile postgresql up -d # alternatieve backend
Let op (geldt voor beide backends): Bij een mislukte export worden de betreffende spans niet opnieuw aangeboden — geen enkele OpenTelemetry-spanprocessor (
batchofsimple) probeert een mislukte export opnieuw. De standaardcombinatiespan-processor=simple+write-failure-policy=fail-closedvoldoet aan de LDV-acknowledgement-eis: de applicatie ziet synchroon of de logregel is opgeslagen en laat de verwerking falen als dat niet zo is. Dat garandeert geen opslag bij een databasestoring, maar maakt een mislukking wél direct zichtbaar. Zet jespan-processor=batch, dan weet de applicatie niet óf de opslag is geslaagd en degradeertfail-closedtot log-only.
Hierna kun je endpoints voorzien van de @Logboek() annotatie:
@Logboek(name = "behandelen-aanvraag", processingActivityId = "1234")
Hierbij is name de beschrijving van je eigen trace log en processingActivityId is de verwijzing naar een Register met meer informatie over de Verwerkingsactiviteit.
Daarnaast kan er in de betreffende functie extra informatie aan de Span worden toegevoegd:
Kotlin:
@Inject
lateinit var handler: ProcessingHandler
@Inject
lateinit var logboekContext: LogboekContext
@GET
@Path("/{identificatieType}/{identificatieNummer}")
@Logboek(name = "test", processingActivityId = "1")
fun test(): Response {
val innerSpan = handler.startSpan("span-2", null)
val innerContext = LogboekContext().apply {
status = StatusCode.ERROR
dataSubjectId = "123"
dataSubjectType = "BSN"
processingActivityId = "4321"
}
handler.addLogboekContextToSpan(innerSpan, innerContext)
innerSpan.end()
logboekContext.dataSubjectId = "000000000"
logboekContext.dataSubjectType = "KVK"
logboekContext.status = StatusCode.OK
return Response.ok("Hello world").build()
}
Java:
@Inject
ProcessingHandler handler;
@Inject
LogboekContext logboekContext;
@GET
@Path("/{identificatieType}/{identificatieNummer}")
@Logboek(name = "test", processingActivityId = "1")
public Response test() {
var innerSpan = handler.startSpan("span-2", null);
LogboekContext innerContext = new LogboekContext();
innerContext.setStatus(StatusCode.ERROR);
innerContext.setDataSubjectId("123");
innerContext.setDataSubjectType("BSN");
innerContext.setProcessingActivityId("4321");
handler.addLogboekContextToSpan(innerSpan, innerContext);
innerSpan.end();
logboekContext.setDataSubjectId("000000000");
logboekContext.setDataSubjectType("KVK");
logboekContext.setStatus(StatusCode.OK);
return Response.ok("Hello world").build();
}
Uitschakelen tijdens testen
Om de database en OpenTelemetry functionaliteit uit te schakelen tijdens testen, stel je logboekdataverwerking.enabled=false in je test configuratie bestand:
test/resources/application.properties:
logboekdataverwerking.enabled=false
Wanneer uitgeschakeld, worden er geen verbindingen met de database gemaakt.
Cross-organisatie trace context (W3C Trace Context)
De LogboekInterceptor extraheert automatisch inkomende traceparent/tracestate headers, zodat een verwerking die door een andere organisatie is gestart in het eigen Logboek wordt voortgezet onder hetzelfde trace_id. Geneste @Logboek-acties krijgen de omsluitende actie als parent; alleen de buitenste actie neemt de inkomende traceparent over.
Voor de andere richting (uitgaande calls vanuit deze service naar een andere organisatie) registreer je LogboekClientRequestFilter op je JAX-RS / MicroProfile REST clients. De filter injecteert traceparent op elke uitgaande request op basis van de actieve OpenTelemetry-context:
Kotlin:
import nl.mijnoverheidzakelijk.ldv.client.LogboekClientRequestFilter
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
@Path("/api")
@RegisterRestClient(configKey = "andere-organisatie")
@RegisterProvider(LogboekClientRequestFilter::class)
interface AndereOrganisatieClient { /* ... */ }
Java:
@Path("/api")
@RegisterRestClient(configKey = "andere-organisatie")
@RegisterProvider(LogboekClientRequestFilter.class)
public interface AndereOrganisatieClient { /* ... */ }
Of programmatisch:
val client = ClientBuilder.newClient().register(LogboekClientRequestFilter::class.java)
dpl.core.foreign_operation.processor
Het LDV-attribuut dpl.core.foreign_operation.processor identificeert de andere partij in een cross-organisatie verwerking en hoort gezet te worden op de uitgaande kant: door applicatiecode op de actieve span, met de URL of identifier van de externe service. De interceptor zet dit attribuut niet automatisch; alleen de tracecontext wordt voor je gepropageerd.
Span processor en acknowledgement
De LDV-standaard stelt dat de applicatie moet kunnen weten dat een logregel daadwerkelijk is opgeslagen. Dit wordt bepaald door twee instellingen die samenwerken.
span-processor kiest de OpenTelemetry-processor:
simple(standaard, en de aanbevolen keuze):SimpleSpanProcessor. Elke span wordt synchroon geëxporteerd; de applicatie wacht op de bevestiging van de database voordat de request afrondt. Conform de acknowledgement-MUST, maar verhoogt p99-latency en koppelt request-doorlooptijd aan de beschikbaarheid van het Logboek.batch(afgeraden):BatchSpanProcessor. Spans worden asynchroon in batches geëxporteerd. De applicatie keert terug naar de aanroeper vóórdat de export bevestigd is, dusbatchvoldoet niet aan de acknowledgement-MUST: bij een JVM-crash tussen response en flush gaat de logregel stil verloren, enfail-closeddegradeert tot log-only.
Kies je tóch batch, doe dat dan als bewuste, gedocumenteerde afweging. De situaties die dat rechtvaardigen zijn een hoog verwerkingsvolume of acties met veel betrokkenen: de standaard vereist een aparte logregel per betrokkene, dus onder simple doet een verwerking met N betrokkenen N+1 synchrone inserts binnen de request. Bij ClickHouse telt daarbij dat de engine op grote, gebundelde inserts is ontworpen; veel kleine losse inserts leggen merge-druk op de MergeTree-tabel.
write-failure-policy bepaalt wat er gebeurt als de database een schrijfactie weigert:
fail-closed(standaard): bij een schrijffout gooit de interceptor eenLogboekWriteException, zodat een verwerking niet als afgerond-en-gelogd geldt terwijl de logregel niet is opgeslagen. Dit is de strikte lezing van de acknowledgement-MUST en koppelt het slagen van een verwerking aan de beschikbaarheid van het Logboek.fail-open: de schrijffout wordt gelogd (SEVERE) en de verwerking gaat door.
Een schrijffout komt zonder de inhoud van de logregel in de applicatielog en bij de aanroeper. Een databasedriver kan het geweigerde statement in zijn foutmelding herhalen, inclusief de betrokkene (de PostgreSQL-driver doet dat). De wrapper logt daarom een SanitizedWriteFailure in plaats van de oorspronkelijke exceptie. Per exceptie uit de keten blijft over:
- het exceptietype en de stackframes;
- bij PostgreSQL de SQLState;
- bij ClickHouse de foutcode van de server en het query-id, waarmee de volledige fout in
system.query_logis terug te vinden; - de vaste foutmelding van de wrapper zelf (bijvoorbeeld
Failed to insert into PostgreSQL) en de melding van de JVM-foutenOutOfMemoryError,NoClassDefFoundErrorenUnsupportedClassVersionError.
Alle andere foutmeldingen vallen weg en zijn nergens meer beschikbaar; diagnose loopt via het bovenstaande, samen met het aantal verloren logregels en hun trace_id:span_id in de SEVERE-regel.
De cause van een LogboekWriteException is dezelfde SanitizedWriteFailure, ook beschikbaar als failure. De buitenste schakel is de exceptie van de wrapper zelf: daar staan bij ClickHouse vendorCode en queryId. De SQLState van PostgreSQL zit dieper in de keten; zoek die met failure.chain.firstNotNullOfOrNull { it.sqlState }, bijvoorbeeld om een verbroken verbinding (SQLState-klasse 08) anders af te handelen dan een geweigerde rij. Een controle als cause.cause is SQLException werkt niet meer.
De wrapper heeft geen invloed op wat de databasedriver zelf logt, via de loggers org.postgresql en com.clickhouse. Ga na wat die op het ingestelde logniveau schrijven voordat je ze fijner afstelt: een driver kan dan statements, rijen of zijn eigen excepties loggen.
Afdwingen van fail-closed werkt alleen op de synchrone simple-processor: daar draait de export op dezelfde thread als de request, vlak voor het einde van de verwerking. Onder batch gebeurt de export op een achtergrond-thread en degradeert het beleid tot log-only; de wrapper logt daarover een waarschuwing bij het opstarten. Alleen de standaardcombinatie simple + fail-closed voldoet aan de acknowledgement-MUST.
fail-closed wordt éénmaal afgedwongen, door de buitenste @Logboek-actie. Een geneste actie gooit zelf niet: ze laat de schrijffout geregistreerd staan en rondt gewoon af, en pas nadat de buitenste actie klaar is gooit die de LogboekWriteException. Zo faalt de request als geheel zodra ergens in de keten een logregel niet is opgeslagen, terwijl businesscode tussen de acties de exceptie niet per ongeluk kan wegvangen (wat de garantie stilletjes zou uitschakelen) of kan aanzien voor een functionele fout van de geneste actie.
De afdwinging is thread-gebonden: een @Logboek-actie die op een andere thread draait dan haar aanroeper dwingt het beleid daar zelf af, ook wanneer de OpenTelemetry-context is gepropageerd (die propagatie bepaalt alleen de parent-relatie van de logregel). De registratie van schrijffouten is namelijk per thread; uitstellen tot de buitenste actie zou de fout op een andere thread onzichtbaar maken. De keten-brede afdwinging op de buitenste actie geldt dus binnen één thread.
Gaat een export mis, dan wordt zo veel mogelijk gered: het mappen van een span naar een databaserij gebeurt per span, dus één onverwerkbare span laat de rest van de batch niet sneuvelen. De insert zelf is wél alles-of-niets — een half weggeschreven batch is een niet te interpreteren logregel. In beide gevallen levert verlies een mislukte export op (dus fail-closed slaat aan) en worden de trace_id:span_id van de verloren logregels op SEVERE gelogd.
Interceptorvolgorde en transacties
LogboekInterceptor heeft prioriteit Interceptor.Priority.APPLICATION (2000). De @Transactional-interceptor van Quarkus staat op PLATFORM_BEFORE + 200, en de lagere waarde draait als buitenste. Bij een methode met @Logboek én @Transactional is de volgorde daardoor: transactie opent, logregel start, methode draait, logregel eindigt en wordt geëxporteerd, acknowledgement, commit.
Onder simple + fail-closed volgt daaruit een garantie voor databasemutaties: kan de logregel niet worden opgeslagen, dan gooit de interceptor de LogboekWriteException vóór de commit en rolt de transactie terug. Er is dan geen logregel en geen verwerking. Slaagt de export en mislukt daarna de commit, dan staat er een logregel voor een teruggerolde verwerking. Dat is over-rapporteren, de kant die de standaard toestaat. De garantie vereist dat de transactie de @Logboek-actie omsluit: met @Logboek op de resource en @Transactional op de servicemethode commit de transactie binnen de methode, dus nog steeds vóór de acknowledgement. Zet @Transactional dan op dezelfde methode of op een omsluitende.
De garantie geldt niet wanneer de methode zelf een niet-aangekondigde exceptie gooit waarop de transactie commit: een checked exception, of een exceptie uit @Transactional(dontRollbackOn = ...). De interceptor gooit dan geen LogboekWriteException, omdat een schrijffout de oorspronkelijke fout niet mag maskeren. Mislukt in dat geval ook het opslaan van de logregel, dan blijft de mutatie staan zonder logregel en meldt alleen een SEVERE-regel het verlies.
De garantie geldt alleen voor blokkerende methoden met declaratieve @Transactional. Geeft de methode een Uni, CompletionStage of Publisher terug, dan stelt Quarkus de commit uit tot het asynchrone resultaat er is, terwijl LogboekInterceptor de logregel afrondt en exporteert op het moment dat de methode het asynchrone resultaat teruggeeft. De acknowledgement komt dan vóór de verwerking is afgerond, de logregel bevat geen uitkomst en geen duur van de verwerking, en een schrijffout rolt alleen terug wat op dat moment al in de transactie stond. Een transactie die de methode zelf beheert met QuarkusTransaction commit binnen de methode, dus vóór de acknowledgement; @WithTransaction van Hibernate Reactive commit buiten de aanroepende thread. In al deze gevallen is de volgorde hierboven niet van toepassing en geldt de aanpak uit "Logregel vooraf, uitkomst achteraf".
Kanttekening: de garantie geldt voor wat de transactie terugdraait. Een externe bijwerking binnen de methode, zoals een verstuurde e-mail, blijft staan; daarvoor blijft de logregel-vooraf-aanpak nodig. De logregel krijgt zijn eindtijd vóór de commit, dus de duur van de commit valt buiten de logregel. De export valt wel binnen de transactie: locks en de databaseverbinding blijven vastgehouden tot de logregel is opgeslagen. De PostgreSQL-repository gebruikt daarbij één JDBC-verbinding, dus onder simple wachten gelijktijdige exports applicatiebreed op elkaar, en ook die wachttijd valt nu binnen de transactie. Een traag Logboek of een piek aan gelijktijdige verwerkingen kan een transactie daardoor over de transactietimeout van Quarkus (standaard 60 seconden) duwen, waarna de transactie terugrolt.
Dezelfde prioriteit plaatst de interceptor binnen de security-interceptors van Quarkus (@RolesAllowed, @Authenticated, @DenyAll, prioriteit PLATFORM_BEFORE + 150). Een geweigerde aanroep bereikt de methode niet en levert geen logregel op: er heeft geen verwerking plaatsgevonden. Op REST-resourcemethoden controleert Quarkus REST deze annotaties al vóór de CDI-interceptors; het verschil is zichtbaar bij @Logboek op een CDI-bean, waar een geweigerde aanroep voorheen een ERROR-logregel opleverde.
Ook de cache-interceptors van Quarkus (@CacheResult, prioriteit PLATFORM_BEFORE + 2) draaien nu buiten LogboekInterceptor. Bij een cache-hit wordt de methode niet aangeroepen en dus ook geen logregel geschreven, terwijl de gegevens wél aan de aanroeper worden verstrekt. Dat is een verwerking die de standaard gelogd wil zien; het Logboek kent geen sampling en geen uitzondering voor gecachte antwoorden. Combineer @Logboek daarom niet met @CacheResult op dezelfde methode: zet @Logboek op een methode die de gecachte methode aanroept, of @CacheResult op een onderliggende bean. Een prioriteit die binnen @Transactional (200) en tegelijk buiten @CacheResult (2) valt bestaat niet. De interceptor logt een WARNING bij de eerste aanroep van een methode die beide annotaties draagt (eenmaal per methode; de eerste aanroep is altijd een cache-miss, dus de waarschuwing verschijnt ook als de cache daarna alles afvangt).
Logregel vooraf, uitkomst achteraf
Wie de acknowledgement-eis strikt neemt, schrijft de logregel vóór de verwerking: simple + fail-closed, en pas na enforceWriteAcknowledgement de opslag of levering. Kan de logregel niet worden geschreven, dan vindt de verwerking niet plaats. Op dat moment is de uitkomst nog onbekend. Faalt de verwerking daarna, dan blijft de logregel met status UNSET staan, wat de standaard leest als "afgerond zonder systeemfout". Een geëxporteerde logregel is definitief en de tabel is insert-only.
De mislukte uitkomst leg je daarom vast als eigen logregel. recordFailedOutcome schrijft per bevestigde logregel één ERROR-regel met de oorspronkelijke regel als parent_span_id, dezelfde naam, dezelfde verwerkingsactiviteit, dezelfde betrokkene en de exception.*-attributen (stacktrace alleen bij opt-in). Het attribuut moza.ldv.uitkomst=mislukt onderscheidt een uitkomst-regel van de per-betrokkene regels, die dezelfde parent dragen. Het succespad schrijft niets extra.
addLogboekContextToSpan geeft alle geëxporteerde regels terug: de actie-regel, en bij meerdere betrokkenen ook de regel per betrokkene. Iedere regel krijgt zijn eigen uitkomst-regel, zodat de uitkomst vindbaar is vanaf de actie-regel én vanaf de regel die een betrokkene bij een inzage ziet.
Leesregel: een logregel zonder ERROR-child is geslaagd. Dit is een MOZa-afspraak; de standaard kent deze leesregel nog niet (Logius-standaarden/logboek-dataverwerkingen#314). Een logregel voor een verwerking die uiteindelijk niet doorging is binnen de standaard acceptabel: over-rapporteren is de veilige kant.
// Buiten een @Logboek-actie: een pooled thread kan nog een schrijffout van een eerdere request dragen.
LogboekWriteFailureRecorder.clear()
val span = handler.startSpan("aanleveren", null)
val context = LogboekContext().apply {
processingActivityId = "https://register.example.org/activiteiten/aanleveren"
dataSubjectId = "000000000"
dataSubjectType = "KVK"
}
val logregels = handler.addLogboekContextToSpan(span, context)
span.end()
handler.enforceWriteAcknowledgement()
try {
lever(bericht)
} catch (e: Exception) {
val zonderUitkomst = handler.recordFailedOutcome(logregels, e)
if (zonderUitkomst.isNotEmpty()) {
alarmeer(zonderUitkomst)
}
throw e
}
recordFailedOutcome heeft geen @Logboek-actie en geen actieve request nodig: de parent komt uit de Logregel, niet uit de huidige context. Wie zijn spans zelf beheert en alleen een SpanContext bewaart, bouwt de Logregel zelf. De methode vangt alles af, ook een JVM Error: een schrijffout van de uitkomst-regel mag de oorspronkelijke fout niet maskeren. De logregels waarvan de uitkomst-regel niet is opgeslagen komen terug als resultaat, en staan met hun trace_id:span_id in één SEVERE-regel. Een lege lijst betekent dat iedere uitkomst-regel bevestigd is; een niet-lege lijst betekent onder-rapportage, want zonder ERROR-child leest die logregel als geslaagd. Wat daarop volgt is aan de afnemer: opnieuw proberen of alarmeren. Dit verlies is alleen zichtbaar onder simple, waar de export op de eigen thread loopt; onder batch meldt alleen de exporter zelf het verlies en is de lijst dus altijd leeg. Een schrijffout die een omliggende actie nog had openstaan blijft staan voor de fail-closed-controle van die actie.
Foutdetails en dataminimalisatie
Error-logregels krijgen altijd exception.type en exception.message; bij meerdere betrokkenen draagt iedere betrokkene-logregel dezelfde foutdata (conform de foutdata-velden uit de standaard). De volledige exception.stacktrace wordt alleen opgeslagen als logboekdataverwerking.log-exception-stacktrace=true; standaard staat dit uit, omdat stacktraces groot zijn en persoonsgegevens kunnen bevatten (dataminimalisatie, AVG art. 5(1)(c)). Houd om dezelfde reden persoonsgegevens buiten exception-messages: het bericht wordt ongefilterd in het Logboek opgeslagen, gekoppeld aan de betrokkene.
Sampling
LDV-spans gebruiken altijd een eigen, toegewijde OpenTelemetry-SDK met een AlwaysOn-sampler, ook wanneer de host-applicatie zelf een OpenTelemetry-SDK levert (bijv. quarkus-opentelemetry). Dit voorkomt dat logregels worden weggesampled door de sampler van de host of door een inkomende traceparent met sampled-flag 0, wat in strijd zou zijn met de LDV-eis dat Log Sampling niet is toegestaan. De toegewijde SDK wordt niet globaal geregistreerd en bestaat naast een eventuele host-SDK; tracecontext blijft propageren omdat de W3C-propagator en OTel-Context SDK-onafhankelijk zijn.
Meerdere betrokkenen
De standaard vereist een aparte logregel per betrokkene. Voor het enkelvoudige geval zet je dataSubjectId/dataSubjectType op de LogboekContext. Verwerk je meerdere betrokkenen in één actie (bijv. een batch), gebruik dan logboekContext.addSubject(id, type) per betrokkene: de interceptor maakt dan één child-logregel per betrokkene onder de actie-span, met hetzelfde trace_id.
Contextvalidatie en het exception-pad
Validatie breekt de verwerking nooit (LDV-standaard, foutafhandeling): ontbreekt processing_activity_id, ontbreekt een betrokkene of is de activity-id geen absolute URI, dan wordt dit als WARNING gelogd (met trace_id:span_id) en wordt de logregel geëxporteerd met de attributen die wél aanwezig zijn. Een lege @Logboek-naam valt terug op de methodenaam. Een logregel zonder betrokkene is toegestaan (de standaard staat 0 of 1 betrokkenen per logregel toe, bijvoorbeeld bij verwerkingen zonder persoonsgegevens); de warning helpt om vergeten context snel op te sporen.
Gooit de geïntercepteerde methode zelf een exceptie, dan wordt de logregel geëxporteerd met status ERROR en de exception.*-attributen; bij meerdere betrokkenen krijgen ook de child-logregels status ERROR. De oorspronkelijke exceptie wordt nooit gemaskeerd door een fout uit de logging zelf.
Niet elke exceptie is een fout van de verwerking. De standaard reserveert Error voor systeemfouten (database-timeout, API onbereikbaar) en schrijft Unset voor wanneer een verwerking zonder systeemfout afrondt maar geen resultaat oplevert. Kondig zo'n uitkomst aan met logboekContext.expectException(e) vlak vóór de throw: de logregel krijgt Unset in plaats van Error en geen exception.*-attributen, ook niet op de child-logregels. De exceptie zelf gaat ongewijzigd naar de aanroeper.
De aankondiging is gebonden aan de exceptie zelf, op identiteit. Gooit de actie daarna iets anders, dan is dat gewoon een fout. Dezelfde exceptie die door meerdere geneste @Logboek-acties omhoog komt, blijft op elk niveau aangekondigd; na afloop van de buitenste actie is de aankondiging verbruikt, zodat een hergebruikte exceptie-instantie niet aangekondigd blijft voor latere acties in dezelfde request. Een tweede aankondiging vervangt de eerste. Vang je de aangekondigde exceptie zelf af en loopt de actie door, trek de aankondiging dan in met clearExpectedException() en zet daarna zelf de status. Een expliciete status kan mee: expectException(e, StatusCode.OK).
Fail-closed geldt ook voor een aangekondigde uitkomst: kan de logregel niet worden opgeslagen, dan gooit de buitenste actie een LogboekWriteException, met de aangekondigde exceptie als suppressed. Alleen een niet-aangekondigde fout die al propageert wordt nooit door een schrijffout gemaskeerd.
NotFoundException nietGevonden = new NotFoundException();
logboekContext.expectException(nietGevonden);
throw nietGevonden;
Releasen
Zie RELEASING.md voor het publiceren van nieuwe versies naar Maven Central.