Skip to content
NLEN
Illustration: Logging for two purposes: audit vs debugging

Logging for two purposes: audit accountability versus debugging

By Ivo Donker — compiled with AI assistance (Claude & Gemini)

When building and maintaining production applications around Large Language Model (LLM) APIs, a fundamental conflict in logging strategy arises almost immediately. On one hand, developers and Site Reliability Engineers (SREs) require maximum transparency: full prompts, model responses, raw JSON payloads, headers, and intermediate reasoning steps to analyze errors and resolve latency issues. On the other hand, compliance officers, data protection officers (DPOs), and security auditors impose strict requirements on data minimization, immutability, access security, and excluding traceable personal data (PII).

When an architecture dumps all this data unfiltered into one central log bucket, dangerous risks arise. Sensitive customer data leaks to widely accessible dashboards, storage costs explode due to gigabytes of text prompts, and complying with legal retention periods or deletion requests becomes technically impossible. To get a grip on these conflicting interests, a separated architecture is necessary. In this article we analyze how to design a two-pronged logging pipeline that facilitates operational troubleshooting without compromising compliance integrity.

Two different purposes, two different data streams

The distinction between audit logging and debugging observability lies not just in the content of the log message, but also in the lifecycle, the target audience, the retention, and the required guarantees around data integrity. If we mix up both purposes, we fail on both fronts: developers get a slow and over-secured search system, while auditors are confronted with an unwieldy pile of volatile system noise.

For a detailed treatment of compliance requirements and legal frameworks, you can consult audit logging and compliance for LLM applications to see how evidence is structured within organizations. Operational statistics and metrics, on the other hand, call for a different approach; see the guide on observability and logging for LLM applications for setting up metrics, distributed tracing, and APM dashboards. The table below summarizes the structural differences.

Property Audit accountability (governance) Debugging & observability (SRE)
Primary purpose Demonstrating who called which model when and which policy was in force. Detecting outages, analyzing error codes, and optimizing latency.
Target audience Auditors, DPOs, security officers, external regulators. Software engineers, devops teams, support engineers.
Content Metadata, tenant ID, hash of prompt/output, policy decisions, token count. Traces, spans, error stacks, HTTP statuses, network times, sampling of payloads.
Sensitivity (PII) Strictly masked or anonymized; no raw personal data. Temporarily present provided it is strictly shielded, preferably sanitized.
Retention period Long-term: 1 to 7 years (depending on sector and legislation). Short-term: 3 to 14 days (maximum 30 days for traces).
Storage requirement WORM storage (Write Once, Read Many), cryptographically sealed, immutable. High throughput, fast indexing, automatic TTL (Time-To-Live) eviction.
Query patterns Targeted point lookups by timestamp, tenant ID, or transaction hash. Aggregations (p95/p99 latency), full-text filters, correlations across microservices.

The failure mode: data pollution, PII leakage, and compliance risks

The most common failure mode in applications integrating LLM models is blindly logging the complete HTTP request and response body at the INFOor DEBUGlevel to central log systems such as Datadog, Elasticsearch, or CloudWatch. While this may seem convenient during the initial development phase, it leads to serious complications in production.

The first risk is PII leakage. When an end user uploads a document with medical data, national ID numbers, or financial transactions, this data ends up directly in log files. These logs are often accessible to a broad team of developers and administrators who have no operational authorization to view this content data. In addition, such a setup makes processing a GDPR deletion request ("right to be forgotten") extremely complex, because log aggregators aren't designed to selectively delete individual records.

The second risk concerns storage costs and performance loss. Text prompts and context documents for RAG (Retrieval-Augmented Generation) systems contain tens of thousands of tokens per call. If an application processes 50 requests per second, this generates gigabytes of text logs per hour. The costs for network ingress, indexing, and retention at commercial monitoring tools rise exponentially as a result. Moreover, the bulk of text pollutes the dashboards, causing critical operational errors such as connection timeouts and rate-limit warnings (HTTP 429) to be overlooked.

Architecture of the separated logging pipeline

To mitigate these failure modes, we split the data streams directly at the ingress layer of our application or the API gateway. An incoming request goes through a data extraction step in which the payload is split into two independent records: an audit event and an observability span.

The audit event contains only structural metadata and policy context. The content of the prompt and the model output is not stored in readable form, but converted into a cryptographic hash (such as SHA-256). This allows the organization to prove afterward that a specific output was indeed generated by the model based on a specific input, without the confidential text having to be permanently retained.

The architecture pattern below illustrates how a central API handler handles both streams in parallel:

[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)│
└─────────────────────────────┘ └─────────────────────────────┘

Through this physical or logical separation, the audit database meets the strictest governance requirements, while the observability platform remains lightweight, scalable, and secure for the entire development team.

Audit accountability in practice: immutability and data minimization

For compliance and audit purposes, it's crucial that we can demonstrate that processes proceeded within the bounds of policy and regulations. This includes, among other things, accounting for which prompt templates were used, which moderation filters were active, and whether the correct model versions were called. For more context on model choices and privacy aspects under European legislation, the article on AI models and privacy: choices for GDPR compliance offers in-depth background information on data processing and legal bases.

A compliant audit record for an LLM transaction contains at minimum the following fields in a strictly validated 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 this schema, not a single letter of the actual prompt text is visible. Should a regulator or legal party ask whether a specific contract was processed by the AI on date X, the organization can hash the original source document and check whether the hash matches the prompt_sha256 in the audit log. This way, the integrity of the evidence remains guaranteed without privacy-sensitive texts lying around on disk for years.

Debugging and observability: traces, spans, and token usage without PII

Where audit logging looks back at historical liability, observability looks in real time at the current status and stability of the system. Engineers need to be able to see whether an upstream provider is slowing down, whether a particular model version is suddenly returning faulty JSON structures, and where in the microservices chain most of the wait time arises.

In modern distributed architectures, we use OpenTelemetry standards for LLM observability (often referred to as Semantic Conventions for Generative AI). Instead of gigantic log texts, we record structured spans and metrics:

Should it be absolutely necessary for development purposes on a test or staging environment to inspect raw prompts, this must be explicitly enabled via configuration with a strict payload sampling rate (for example 1% of calls) and an aggressive retention period of a maximum of 72 hours. In production environments, dynamic redaction modules must automatically replace PII with placeholders (such as [EMAIL_REDACTED] or [NAME_MASKED]) before a span is sent to the collector.

The tension with zero data retention (ZDR) at LLM providers

Many organizations working with sensitive business data enter into Zero Data Retention agreements with LLM providers. This guarantees that the external provider does not store prompts and outputs on disk and does not use them for model training. For the exact contractual and technical implementation, read more about configuring zero data retention with LLM APIs to understand how this is enforced at the API level via specific headers and enterprise agreements.

However, a major architectural paradox arises here: if we enforce ZDR at the network level with the vendor, but our own application logs then store all unfiltered prompts in a central observability tool, we completely undo the privacy guarantees of ZDR. The risks simply shift from OpenAI's or Anthropic's infrastructure to our own log server.

The solution to this tension is the principle of isolated retention synchronization: our internal log systems must apply the same retention rules as the upstream providers. If a request is processed under ZDR terms, the traceable log record within the observability tool automatically gets the attribute payload_storage: none. The gateway then forwards only status codes, token counters, and latency statistics, and removes the actual request body from working memory as soon as the stream is complete.

Implementation example: routing and field masking in an LLM gateway

In a robust architecture, the separation between audit logging and debugging is handled in a central gateway layer. The Python example below shows a structured handler that performs an LLM call with a strict timeout, safe error handling, SHA-256 hash generation for audit accountability, and PII masking for the observability telemetry.

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 this example we see an explicit separation: the audit record never contains the masked or unmasked text, only cryptographic verification values. The telemetry record contains only operational variables such as the total response time in milliseconds and a strictly sanitized fragment for fast triage.

Setting up retention periods, encryption, and access management

Once the data streams are split, the respective storage systems must be set up according to the least privilegeprinciple and strictly defined retention profiles.

For the audit database the following applies:

For the observability platform the following applies:

Production checklist for separated log architecture

To verify whether an LLM application is ready for production without introducing compliance or operational blind spots, we apply the following technical checkpoints:

  1. Payload isolation: Are prompts and model outputs fully kept out of regular application logs (such as stdout and general syslog streams)?
  2. Hash verification: Is a SHA-256 hash generated for every prompt and response and recorded in the audit record for non-repudiation?
  3. ZDR coherence: Do internal observability systems have a storage policy that runs synchronously with the Zero Data Retention requirements agreed with external LLM providers?
  4. Isolated access: Are audit logs physically or logically separated from telemetry dashboards, with separate authorization roles for security and engineering?
  5. Automated lifecycle: Are TTL and WORM policies active on the storage containers so that logs are automatically eliminated after the required retention period?

By anchoring this separation from the very first design phase in the gateway and backend infrastructure, engineers retain the deep operational insights needed for reliable AI systems, while the organization demonstrably meets the strictest privacy and governance standards.