Audit Logging
Deze pagina beschrijft de technische architectuur van het audit logging systeem in AutoMate API: hoe events worden geschreven naar Seq in CLEF-formaat, hoe de ambient process-context via AsyncLocal<T> stroomt, hoe records worden teruggelezen, en hoe nieuwe audit types worden toegevoegd.
Architectuuroverzicht
Het audit logging systeem bestaat uit twee lagen:
Laag 1 — Schrijven via IAuditLog/SeqAuditLogSink
IAuditLog is de centrale write-interface. De implementatie SeqAuditLogSink schrijft elk audit event rechtstreeks naar Seq via POST /api/events/raw in CLEF-formaat (Compact Log Event Format). Tegelijk wordt het event in een ConcurrentDictionary in-memory opgeslagen als niet-duurzame fallback voor same-process reads.
Laag 2 — Lezen via batch-ophaling
GetBatchAsync en StreamBatchAsync bevragen Seq via GET /api/events met een filter op (tenantId, correlationId). Als Seq niet bereikbaar is, valt de sink terug op de in-memory buffer.
Dependency registratie
Dit registreert:
IAuditLog(scoped) — de provider-agnostische write/read interfaceKeyed singletons:
"seq"(SeqAuditLogSink) en"azure-table"(AzureTableAuditLogSink)ISeqStatusService(singleton) — bevraagt Seq voor dashboard module-statusHttpClient "SeqAudit"met 15 seconden timeout (geen retry — trage queries worden anders verergerd)
CLEF event structuur
Elk audit write door SeqAuditLogSink produceert één CLEF-event. De properties op root-niveau zijn:
Property | Type | Altijd aanwezig | Betekenis |
|---|---|---|---|
| string | ja | ISO-8601 timestamp (UTC) |
| string | ja | Altijd |
| string | ja | Message template: |
| string | ja | Altijd |
| string | ja | Tenant UUID als string (voor filter equality) |
| string | ja | Correlatie/batch-id — groepeert alle stappen van één operatie |
| string | ja | Bijv. |
| string | ja | Eerste segment van |
| string | ja | JSON-payload met action, actor, changes, entityDisplayName |
| string | nee | Ambient process-naam, bijv. |
| string | nee | Uitgeflattend uit |
| string | nee | Uitgeflattend uit |
Waarom EventKind = "Audit"?
Dit is de sentinel die zakelijke audit events onderscheidt van reguliere Serilog telemetrie (MSAL token-acquisitie, HTTP client logs, library ruis). Reguliere Serilog events hebben deze property nooit. Alle Seq-queries in het status dashboard gebruiken EventKind = 'Audit' als anker zodat telemetrie-ruis nooit in audit-resultaten landt.
Waarom Module als top-level property?
Module is afgeleid van het eerste segment van Type ("IAM.Employee.Created" → "IAM") en wordt als geïndexeerde Seq-property opgeslagen. Dit maakt de filter Module = 'IAM' mogelijk zonder een Type like 'IAM%' prefix-match, wat aanzienlijk efficiënter is in Seq.
Waarom EntityDisplayName en Action uitflattenen?
De Message-veld bevat een JSON-payload. Als SeqStatusService deze informatie nodig heeft voor het dashboard, zou het client-side JSON-parsing vereisen op elk event. Door EntityDisplayName en Action als top-level CLEF-properties op te nemen kan Seq ze direct indexeren en kan de status service ze zonder verdere parsing lezen.
Voorbeeld CLEF-event (IAM sync)
Voorbeeld CLEF-event (form-actie, geen Process)
AuditContext — ambient process propagation
AuditContext in API/AutoMate.API.Shared/Conventions/AuditContext.cs slaat de actieve process-naam op via AsyncLocal<string?>. Hierdoor stroomt de process-context automatisch mee door alle async/await-chains zonder dat callsites op IAuditLog.WriteAsync aangepast hoeven te worden.
Hoe LogJobContextAttribute dit aanzet
LogJobContextAttribute is een Hangfire job filter die bij aanvang van een job zowel Serilog's LogContext als AuditContext vult:
Bij OnPerforming roept de filter AuditContext.PushProcess(process) aan. SeqAuditLogSink leest AuditContext.Process op het moment van schrijven en stamt het CLEF-event ermee.
Onderscheid tussen triggers in Seq
Filter | Betekenis |
|---|---|
| Events geschreven door IAM sync job |
| Events geschreven door AutoLeave check job |
| Events geschreven via de AutoMate form/API (door een gebruiker) |
AuditLogRecord model
Het Id-veld groepeert alle stappen van één operatie (batch). Een IAM sync batch schrijft meerdere records met hetzelfde Id, één per verwerkte employee of stap. GetBatchAsync haalt alle records voor een (tenantId, id) op in chronologische volgorde.
Lezen en downloaden van audit records
GetBatchAsync / StreamBatchAsync
Filter gebruikt bij Seq-query: Id = '{id}' and TenantId = '{tenantId}'
Terugvalstrategie:
Probeer Seq (duurzaam, binnen
QueryWindowDaysterugkijkvenster)Als Seq niet bereikbaar of geen resultaten: in-memory buffer (
ConcurrentDictionary)
DownloadAuditLogsCommandHandler
Verwerkt auditlog://-URIs zoals auditlog://seq/{tenantId}/{correlationId}. Haalt de batch op via IAuditLog.GetBatchAsync en rendert de records als een leesbaar tekstbestand voor download.
URI-formaat:
Seq:
auditlog://seq/{tenantId}/{correlationId}AzureTable:
auditlog://azure-table/{tenantId}/{tableName}/{correlationId}
Het correlationId is altijd het laatste segment van de URI.
Audit types per module
IAM — EmployeeAuditConstants
Locatie: API/Src/AutoMate.API/Features/IAM/Employees/EmployeeAuditConstants.cs
Type | Trigger |
|---|---|
| Medewerker aangemaakt (sync of form) |
| Medewerker bijgewerkt (sync of form) |
| Medewerker verwijderd |
| Profielfoto geüpload |
| Wachtwoord gereset |
| MFA gereset |
| Direct offboarden (handmatig) |
| Offboard gepland |
| Account geactiveerd |
Events met Process = 'AutoLeave' (AutoLeave check job) schrijven eveneens naar IAM-audit types — typisch IAM.Employee.Deleted of een vergelijkbaar type voor auto-uitdiensttreding.
OrgModel — OrgModelAuditConstants
Locatie: API/Src/AutoMate.API/Features/UserTypes/OrgModel/OrgModelAuditConstants.cs
Type | Trigger |
|---|---|
| Attribuutwaarde vervangen via OrgModel resolutie |
| Attribuutwaarde gewist via OrgModel resolutie |
Centrale log-ID voor de OrgModel-overzichtspagina: "OrgModel.AttributeChanges".
Seq query strategie
Alle Seq-queries in zowel SeqAuditLogSink als SeqStatusService volgen deze aanpak:
Correcte query parameters
Parameter | Waarde | Reden |
|---|---|---|
| URL-encoded Seq filter-expressie | Doelgerichte query |
| ISO-8601 UTC timestamp | Tijdvenster start (gebruik |
| ISO-8601 UTC timestamp | Tijdvenster einde (gebruik |
| N | Maximum events te retourneren |
| N (zelfde als count) | Seq stopt scannen zodra N events gevonden zijn |
|
| Seq rendert |
shortCircuitAfter is kritisch voor performance: zonder dit parameter blijft Seq het volledige tijdvenster scannen ook als het maximum al bereikt is.
render=true wordt alleen toegepast op status-queries (niet op batch-ophaling) zodat het dashboard weergave-klare berichten ontvangt zonder extra client-side rendering.
API key authenticatie
De API key wordt verzonden via de X-Seq-ApiKey HTTP header — nooit via query string.
Voorbeeld query URL
HttpClient configuratie
De "SeqAudit" HttpClient heeft een timeout van 15 seconden en geen retry-handler. Retries worden bewust weggelaten: als een Seq-query traag is, maakt een retry de situatie erger. Mislukte writes worden door de in-memory buffer opgevangen; mislukte status-queries retourneren ModuleHealthStatus.Unknown.
Configuratie
Sleutel | Standaard | Betekenis |
|---|---|---|
|
| Actieve audit sink. Ondersteunde waarden: |
| — | Basis-URL van de Seq-instantie (inclusief poort) |
| — | Seq API key; verzonden als |
| 7 | Hoever terug Seq kijkt bij |
| 1000 | Maximum events bij |
In lokale ontwikkeling met Aspire: als AuditLog:Seq:ServerUrl niet is geconfigureerd valt de registratie automatisch terug op ConnectionStrings:seq (door Aspire geïnjecteerd via .WithReference(seq) in de AppHost).
Schrijven van audit events
De tenantId wordt altijd opgehaald via IMultiTenantService.GetTenantId() uit de X-Tenant-Id HTTP header — nooit als request body parameter.
Nieuwe audit types toevoegen
Definieer een constants class in de desbetreffende feature-folder:
Schrijf het event via
IAuditLog.WriteAsyncmet een JSON-payload die minimaalActionenEntityDisplayNamebevat:
Moduleis automatisch:SeqAuditLogSinkleidtModuleaf uit het eerste segment vanType."MyModule.MyEntity.Created"produceertModule = "MyModule".Processis automatisch: als het event wordt geschreven vanuit een Hangfire job met[LogJobContext(...)], wordtProcessautomatisch gestampt viaAuditContext. Bij een API endpoint-aanroep blijftProcessnull.Optioneel: voeg dashboard signals toe in
StatusSignals.csals het nieuwe type zichtbaar moet zijn op het status dashboard. Gebruik altijdEventKind = 'Audit'als filter-anker.
Aandachtspunten
De in-memory
ConcurrentDictionaryfallback is niet duurzaam: bij herstart van de API gaan niet-naar-Seq geschreven records verloren. Zorg datSeq.ServerUrlaltijd correct geconfigureerd is voor productie.Pas
QueryWindowDaysaan als batches langer dan 7 dagen terugkijken. Dit verlengd ook de Seq-scantijd.Gebruik
EventKind = 'Audit'als primair filter-anker in alle dashboardqueries — nooit@l = 'Information'als enige onderscheid, want reguliere Serilog events zijn ook Information-level.Tenant ID wordt altijd via
IMultiTenantService.GetTenantId()opgehaald uit deX-Tenant-Idheader — nooit als request body parameter. Dit geldt voor alle AutoMate.API endpoints.