Loggen voor twee doelen: audit-verantwoording versus debugging
Bij het bouwen en onderhouden van productieapplicaties rond Large Language Model (LLM) API's ontstaat vrijwel direct een fundamenteel conflict in de logstrategie. Aan de ene kant vereisen ontwikkelaars en Site Reliability Engineers (SRE's) maximale transparantie: volledige prompts, modelreacties, ruwe JSON-payloads, headers en tussentijdse redeneerstappen om fouten te analyseren en latentieproblemen op te lossen. Aan de andere kant stellen compliance officers, data protection officers (DPO's) en beveiligingsauditors juist strikte eisen aan dataminimalisatie, onveranderbaarheid, toegangsbeveiliging en het uitsluiten van herleidbare persoonsgegevens (PII).
Wanneer een architectuur al deze data ongefilterd in één centrale logbucket dumpt, ontstaan gevaarlijke risico's. Gevoelige klantdata lekt naar algemeen toegankelijke dashboards, opslagkosten exploderen door gigabytes aan tekstprompts, en het naleven van wettelijke bewaartermijnen of verwijderverzoeken wordt technisch onmogelijk. Om grip te krijgen op deze tegenstrijdige belangen is een gescheiden architectuur noodzakelijk. In dit artikel analyseren we hoe je een tweeledige logpijplijn ontwerpt die operationele foutopsporing faciliteert zonder de compliance-integriteit in gevaar te brengen.
Twee verschillende doelen, twee verschillende datastromen
Het onderscheid tussen audit-logging en debugging-observability zit niet alleen in de inhoud van het logbericht, maar ook in de levenscyclus, de doelgroep, de retentie en de vereiste garanties rondom data-integriteit. Als we beide doelen door elkaar halen, falen we op beide vlakken: ontwikkelaars krijgen een traag en overbeveiligd zoeksysteem, terwijl auditors geconfronteerd worden met een onoverzichtelijke berg vluchtige systeemruis.
Voor een gedetailleerde uitwerking van compliance-eisen en wettelijke kaders kun je audit-logging en compliance voor LLM-toepassingen raadplegen om te zien hoe bewijsvoering binnen organisaties wordt gestructureerd. Operationele statistieken en metrieken vragen daarentegen om een andere aanpak; zie hiervoor de gids over observability en logging voor LLM-toepassingen voor de inrichting van metrics, distributed tracing en APM-dashboards. De onderstaande tabel vat de structurele verschillen samen.
| Eigenschap | Audit-verantwoording (Governance) | Debugging & Observability (SRE) |
|---|---|---|
| Primair doel | Aantonen wie wanneer welk model aanriep en welk beleid van kracht was. | Detecteren van storingen, analyseren van foutcodes en optimaliseren van latentie. |
| Doelgroep | Auditors, DPO's, security officers, externe toezichthouders. | Software engineers, devops-teams, support engineers. |
| Inhoud | Metadata, tenant-ID, hash van prompt/output, beleidsbesluiten, token-telling. | Traces, spans, foutstacks, HTTP-statussen, netwerktijden, sampling van payloads. |
| Gevoeligheid (PII) | Strikt gemaskeerd of geanonimiseerd; geen ruwe persoonsgegevens. | Tijdelijk aanwezig mits strikt afgeschermd, bij voorkeur gesanitiseerd. |
| Bewaartermijn (Retentie) | Langdurig: 1 tot 7 jaar (afhankelijk van sector en wetgeving). | Kortstondig: 3 tot 14 dagen (maximaal 30 dagen voor traces). |
| Opslagvereiste | WORM-opslag (Write Once, Read Many), cryptografisch verzegeld, onveranderbaar. | Hoge doorvoer, snelle indexering, automatische TTL (Time-To-Live) evictie. |
| Querypatronen | Gerichte point-lookups op tijdstip, tenant-ID of transactie-hash. | Aggregaties (p95/p99 latentie), full-text filters, correlaties over microservices. |
De faalmodus: datavervuiling, PII-lekkage en compliancerisico's
De meest voorkomende faalmodus in applicaties die LLM-modellen integreren, is het klakkeloos loggen van de complete HTTP-request- en response-body op INFO- of DEBUG-niveau naar centrale logsystemen zoals Datadog, Elasticsearch of CloudWatch. Hoewel dit tijdens de initiële ontwikkelingsfase handig lijkt, leidt dit in productie tot ernstige complicaties.
Het eerste risico is PII-lekkage. Wanneer een eindgebruiker een document uploadt met medische gegevens, burgerservicenummers of financiële transacties, belanden deze gegevens direct in logbestanden. Deze logs zijn vaak toegankelijk voor een breed team van ontwikkelaars en beheerders die geen operationele autorisatie hebben om deze inhoudelijke data in te zien. Daarnaast maakt een dergelijke opzet het verwerken van een AVG-verwijderverzoek ("recht op vergetelheid") extreem complex, omdat logaggregators niet zijn ontworpen om individuele records selectief te wissen.
Het tweede risico betreft opslagkosten en prestatieverlies. Tekstprompts en contextdocumenten voor RAG-systemen (Retrieval-Augmented Generation) bevatten tienduizenden tokens per aanroep. Als een applicatie 50 verzoeken per seconde verwerkt, genereert dit gigabytes aan tekstlogs per uur. De kosten voor netwerkingress, indexering en retentie bij commerciële monitoringtools lopen hierdoor exponentieel op. Bovendien vervuilt de bulk aan tekst de dashboards, waardoor kritieke operationele fouten zoals connection timeouts en rate-limit warnings (HTTP 429) over het hoofd worden gezien.
Architectuur van de gescheiden logging-pijplijn
Om deze faalmodi te mitigeren, splitsen we de datastromen direct bij de ingress-laag van onze applicatie of de API-gateway. Een binnenkomend verzoek doorloopt een data-extractiestap waarin de payload wordt opgesplitst in twee onafhankelijke records: een audit-event en een observability-span.
Het audit-event bevat uitsluitend structurele metadata en beleidsmatige context. De inhoud van de prompt en de modeloutput wordt niet in leesbare vorm opgeslagen, maar omgezet in een cryptografische hash (zoals SHA-256). Hierdoor kan de organisatie achteraf bewijzen dat een specifieke uitvoer daadwerkelijk door het model is gegenereerd op basis van een specifieke invoer, zonder dat de vertrouwelijke tekst permanent bewaard hoeft te blijven.
Het onderstaande architectuurpatroon illustreert hoe een centrale API-handler beide stromen parallel afhandelt:
[Client Request]
│
▼
┌─────────────────────────────────────────────────────────┐
│ LLM API Gateway / Handler │
│ │
│ 1. Genereer unieke Request-ID (Trace Context) │
│ 2. Bereken SHA-256 van Prompt en Verrijkte Context │
│ 3. Maskeer PII en valideer invoer │
└──────────────┬───────────────────────────┬──────────────┘
│ │
[Audit Stream: Metadata] [Debug Stream: Telemetrie]
│ │
▼ ▼
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ Audit Verantwoording │ │ Observability / Tracing │
│ - Request-ID & Timestamp │ │ - Trace-ID & Span-ID │
│ - Tenant-ID & User-Hash │ │ - Latentie (TTFT, Totaal) │
│ - Model-ID & Provider │ │ - Tokenverbruik (In/Uit) │
│ - Hash van Prompt/Output │ │ - HTTP Status & Error codes │
│ - Token Usage & Cost │ │ - Geprofileerde spans │
│ - Retentie: 1-7 jaar (WORM) │ │ - Retentie: 7-14 dagen (TTL)│
└─────────────────────────────┘ └─────────────────────────────┘
Door deze fysieke of logische scheiding voldoet de audit-database aan de strengste governance-eisen, terwijl het observability-platform lichtgewicht, schaalbaar en veilig blijft voor het voltallige developmentteam.
Audit-verantwoording in de praktijk: onveranderbaarheid en dataminimalisatie
Voor compliance en audit-doeleinden is het cruciaal dat we kunnen aantonen dat processen binnen de kaders van beleid en regelgeving zijn verlopen. Dit omvat onder meer verantwoording afleggen over welke promptsjablonen zijn gebruikt, welke moderatiefilters actief waren, en of de juiste modelversies zijn aangeroepen. Voor meer context over modelkeuzes en privacyaspecten onder de Europese wetgeving biedt het artikel over AI-modellen en privacy: keuzes voor AVG-compliance diepgaande achtergrondinformatie over gegevensverwerking en grondslagen.
Een compliant audit-record voor een LLM-transactie bevat minimaal de volgende velden in een strikt gevalideerd JSON-schema:
{
"audit_version": "2026-08",
"event_id": "aud_9f8b2c4e-6712-4a0e-bc91-31a87e2b1099",
"timestamp": "2026-08-15T14:23:10.451Z",
"tenant_id": "tenant_enterprise_402",
"user_identifier_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"action": "llm_completion",
"model_requested": "claude-3-5-sonnet-20241022",
"model_routed": "claude-3-5-sonnet-20241022",
"provider": "anthropic",
"prompt_template_id": "tpl_contract_analysis_v3",
"prompt_sha256": "4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a",
"response_sha256": "ef2d127de37b942baad06145e54b0c619a1f22327b2ebbcfbec78f5564afe39d",
"policy_evaluations": {
"pii_filter_triggered": false,
"prompt_injection_score": 0.02,
"moderation_decision": "ALLOWED"
},
"token_usage": {
"prompt_tokens": 1420,
"completion_tokens": 312,
"total_tokens": 1732
},
"compliance_flags": ["ZDR_ACTIVE", "EU_DATA_RESIDENCY"]
}
In dit schema is geen letter van de daadwerkelijke prompttekst zichtbaar. Mocht een toezichthouder of juridische partij vragen of een specifiek contract op datum X door de AI is verwerkt, kan de organisatie het originele brondocument hashen en controleren of de hash overeenkomt met de prompt_sha256 in het audit-logboek. Zo blijft de integriteit van de bewijsvoering gewaarborgd zonder dat privacygevoelige teksten jarenlang op schijf rondslingeren.
Debugging en observability: traces, spans en tokenverbruik zonder PII
Waar audit-logging terugkijkt naar historische aansprakelijkheid, kijkt observability realtime naar de actuele status en stabiliteit van het systeem. Engineers moeten kunnen zien of een upstream provider trager wordt, of een bepaalde modelversie plotseling foutieve JSON-structuren terugstuurt, en waar in de microservices-keten de meeste wachttijd ontstaat.
In moderne gedistribueerde architecturen gebruiken we OpenTelemetry-standaarden voor LLM-observability (vaak aangeduid als Semantic Conventions for Generative AI). In plaats van gigantische logteksten registreren we gestructureerde spans en metrieken:
- Time to First Token (TTFT): De tijd in milliseconden tussen het versturen van het verzoek en de ontvangst van het eerste streaming-pakket.
- Inter-token Latency: De gemiddelde tijd tussen opeenvolgende tokens tijdens het streamen, wat inzicht geeft in provider-drukte.
- HTTP Response Codes en Foutcategorisatie: Onderscheid maken tussen netwerktimeouts, authenticatiefouten (401), rate limits (429) en modeloverbelasting (503).
- Token Consumption Velocity: Het aantal verbruikte input- en output-tokens per minuut per tenant, essentieel voor rate-limiting algoritmes.
Mocht het voor ontwikkeldoeleinden op een test- of staging-omgeving absoluut vereist zijn om ruwe prompts te inspecteren, dan moet dit expliciet via configuratie worden ingeschakeld met een strikte payload sampling rate (bijvoorbeeld 1% van de calls) en een agressieve retentieperiode van maximaal 72 uur. In productieomgevingen moeten dynamische redactiemodules PII automatisch vervangen door placeholders (zoals [EMAIL_REDACTED] of [NAME_MASKED]) voordat een span naar de collector wordt verstuurd.
De spanning met Zero Data Retention (ZDR) bij LLM-providers
Veel organisaties die werken met gevoelige bedrijfsgegevens sluiten bij LLM-providers overeenkomsten af voor Zero Data Retention. Dit garandeert dat de externe provider prompts en outputs niet opslaat op schijf en deze niet gebruikt voor modeltraining. Lees voor de exacte contractuele en technische implementatie meer over zero-data-retention configureren bij LLM-API's om te begrijpen hoe dit op API-niveau wordt afgedwongen via specifieke headers en enterprise-overeenkomsten.
Hier ontstaat echter een grote architecturale paradox: als we op netwerkniveau ZDR afdwingen bij de leverancier, maar onze eigen applicatielogs vervolgens alle ongefilterde prompts opslaan in een centrale observability-tool, doen we de privacygaranties van ZDR volledig teniet. De risico's verplaatsen zich simpelweg van de infrastructuur van OpenAI of Anthropic naar onze eigen logserver.
De oplossing voor deze spanning is het principe van geïsoleerde retentiesynchronisatie: onze interne logsystemen moeten dezelfde retentieregels hanteren als de upstream providers. Als een verzoek onder ZDR-voorwaarden wordt verwerkt, krijgt het traceerbare logrecord binnen de observability-tool automatisch het attribuut payload_storage: none. De gateway stuurt dan uitsluitend statuscodes, tokencounters en latentie-statistieken door, en verwijdert de daadwerkelijke request-body direct uit het werkgeheugen zodra de stream is voltooid.
Implementatievoorbeeld: routering en veldmaskering in een LLM-gateway
In een robuuste architectuur wordt de scheiding tussen audit-logging en debugging afgehandeld in een centrale gateway-laag. Het onderstaande Python-voorbeeld toont een gestructureerde handler die een LLM-aanroep uitvoert met een strikte timeout, een veilige foutafhandeling, SHA-256 hash-generatie voor audit-verantwoording, en PII-maskering voor de observability-telemetrie.
import hashlib
import json
import time
import re
import urllib.request
import urllib.error
def sanitize_pii(text: str) -> str:
# Eenvoudige demonstratieve regex voor e-mails en BSN-patronen
text = re.sub(r'[\w\.-]+@[\w\.-]+\.\w+', '[EMAIL_REDACTED]', text)
text = re.sub(r'\b\d{9}\b', '[BSN_REDACTED]', text)
return text
def calculate_sha256(content: str) -> str:
return hashlib.sha256(content.encode('utf-8')).hexdigest()
def execute_llm_call(tenant_id: str, prompt: str, api_key: str, timeout_seconds: float = 10.0):
url = "https://api.openai.com/v1/chat/completions"
request_payload = {
"model": "gpt-4o",
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.2
}
start_time = time.time()
prompt_hash = calculate_sha256(prompt)
sanitized_debug_prompt = sanitize_pii(prompt)
req = urllib.request.Request(
url,
data=json.dumps(request_payload).encode('utf-8'),
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}"
},
method="POST"
)
try:
with urllib.request.urlopen(req, timeout=timeout_seconds) as response:
duration_ms = int((time.time() - start_time) * 1000)
response_data = json.loads(response.read().decode('utf-8'))
output_text = response_data['choices'][0]['message']['content']
output_hash = calculate_sha256(output_text)
usage = response_data.get('usage', {})
# 1. Stuur Audit Event (Alleen Hashes en Metadata, Geen PII)
audit_record = {
"event_type": "AUDIT_RECORD",
"tenant_id": tenant_id,
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"prompt_sha256": prompt_hash,
"output_sha256": output_hash,
"tokens": usage,
"status": "SUCCESS"
}
persist_audit_log(audit_record)
# 2. Stuur Observability Trace (Telemetrie & Gemaskeerde Payload)
telemetry_record = {
"trace_type": "METRIC_SPAN",
"duration_ms": duration_ms,
"http_status": 200,
"prompt_tokens": usage.get("prompt_tokens", 0),
"completion_tokens": usage.get("completion_tokens", 0),
"sample_snippet": sanitized_debug_prompt[:120]
}
send_observability_metric(telemetry_record)
return output_text
except urllib.error.HTTPError as e:
duration_ms = int((time.time() - start_time) * 1000)
error_body = e.read().decode('utf-8')
# Audit log registreert het foutbeleid
persist_audit_log({
"event_type": "AUDIT_RECORD",
"tenant_id": tenant_id,
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"prompt_sha256": prompt_hash,
"status": f"HTTP_ERROR_{e.code}"
})
# Observability log registreert technische error details voor debugging
send_observability_metric({
"trace_type": "ERROR_SPAN",
"duration_ms": duration_ms,
"http_status": e.code,
"error_message": error_body[:200]
})
raise RuntimeError(f"LLM API Gateway fout (HTTP {e.code}) na {duration_ms}ms")
except urllib.error.URLError as e:
# Netwerktimeout of DNS falen
duration_ms = int((time.time() - start_time) * 1000)
send_observability_metric({
"trace_type": "TIMEOUT_SPAN",
"duration_ms": duration_ms,
"http_status": 0,
"error_message": str(e.reason)
})
raise TimeoutError(f"Verbinding met LLM provider mislukt: {e.reason}")
def persist_audit_log(record: dict):
# Schrijf naar onveranderbare, beveiligde audit store (bijv. S3 WORM / Object Lock)
pass
def send_observability_metric(record: dict):
# Schrijf naar APM / Prometheus / OpenTelemetry collector (korte TTL)
pass
In dit voorbeeld zien we een expliciete scheiding: het audit-record bevat nooit de gemaskeerde of ongemaskeerde tekst, maar uitsluitend cryptografische verificatiewaarden. Het telemetrie-record bevat uitsluitend operationele variabelen zoals de totale responstijd in milliseconden en een streng gesanitiseerd fragment voor snelle triage.
Bewaartermijnen, encryptie en toegangsbeheer inrichten
Zodra de datastromen zijn gesplitst, moeten de respectievelijke opslagsystemen worden ingericht volgens het least privilege-principe en strikt gedefinieerde retentieprofielen.
Voor de audit-database geldt:
- Toegangsbeheer (RBAC): Uitsluitend toegankelijk voor compliance auditors en geautomatiseerde rapportagescripts. Ontwikkelaars hebben standaard geen leesrechten op deze tabellen.
- Encryptie en Integriteit: Versleuteld at-rest met klant-specifieke sleutels (KMS/HSM) en opgeslagen op buckets met Object Lock in WORM-modus (Write Once, Read Many). Hierdoor kunnen logs zelfs door beheerders met root-rechten niet voortijdig worden gewijzigd of verwijderd.
- Retentie: Vaste bewaartermijnen op basis van wettelijke verplichtingen (bijvoorbeeld 5 of 7 jaar), waarna automatische lifecycle policies de records permanent vernietigen.
Voor het observability-platform geldt:
- Toegangsbeheer: Toegankelijk voor het engineering- en operations-team ten behoeve van incident management en troubleshooting.
- Automatische Evictie: Strikte Time-To-Live (TTL). Gedetailleerde spans en eventuele payload-samples worden na 7 tot maximaal 14 dagen automatisch gewist. Alleen geaggregeerde statistieken (zoals gemiddelde latentie en foutpercentages per uur) blijven langer bewaard.
- Data Masking at Rest: Alle velden die mogelijke persoonsgegevens kunnen bevatten worden vóór indexering door een filter gehaald.
Productiechecklist voor gescheiden logarchitectuur
Om te verifiëren of een LLM-applicatie gereed is voor productie zonder compliance- of operationele blinde vlekken te introduceren, hanteren we de volgende technische controlepunten:
- Payload-isolatie: Worden prompts en modeloutputs volledig geweerd uit reguliere applicatielogs (zoals stdout en algemene syslog-streams)?
- Hash-verificatie: Wordt van elke prompt en response een SHA-256 hash gegenereerd en vastgelegd in het audit-record voor niet-weerlegbaarheid?
- ZDR-coherentie: Hebben interne observability-systemen een opslagbeleid dat synchroon loopt met de Zero Data Retention-eisen die met externe LLM-providers zijn afgesproken?
- Geïsoleerde Toegang: Zijn audit-logs fysiek of logisch gescheiden van telemetrie-dashboards, met afzonderlijke autorisatierollen voor security en engineering?
- Geautomatiseerde Lifecycle: Zijn TTL- en WORM-policies actief op de opslagcontainers zodat logs na de vereiste bewaartermijn automatisch worden geëlimineerd?
Door deze scheiding vanaf de eerste ontwerpfase te verankeren in de gateway en backend-infrastructuur, behouden engineers de diepe operationele inzichten die nodig zijn voor betrouwbare AI-systemen, terwijl de organisatie aantoonbaar voldoet aan de strengste privacy- en governance-normen.


