Privacy-First Logging for AI Apps
Protect user data in AI-powered applications with privacy-first logging patterns. PII redaction, consent-aware logs, and audit trails.
Protect user data in AI-powered applications with privacy-first logging patterns. PII redaction, consent-aware logs, and audit trails.
AI applications handle more sensitive data than traditional software. User prompts contain personal information, business secrets, and context that users expect to remain private. Yet most AI application logging follows the same patterns as traditional web apps -- log everything, worry about privacy later.
This approach fails for AI applications because the data flowing through them is qualitatively different. A traditional web app logs HTTP requests and database queries. An AI app logs natural language prompts that might contain social security numbers, medical information, legal questions, or proprietary business strategies. The logging strategy needs to match the sensitivity of the data.
Standard application logging follows a simple pattern: log the request, log the response, log any errors. For a REST API, this looks like:
logger.info('Request received', {
method: 'POST',
path: '/api/chat',
body: request.body, // This contains the user's prompt
userId: user.id,
})
The problem is request.body. In a traditional API, the request body is structured data -- a JSON object with defined fields. You know what is in it and can redact specific fields.
In an AI application, the request body is a natural language prompt. It could contain anything:
Logging this raw prompt stores PII, health information, financial data, and personal details in your log system. If your logs are compromised, you have a data breach that includes the most sensitive information your users shared.
The safest pattern is to redact sensitive information before it reaches the logging system. Use pattern matching and named entity recognition to identify and replace PII.
import { redactPII } from '@/lib/privacy/redactor'
function logSafely(level: string, message: string, data: Record<string, any>) {
const sanitizedData = {
...data,
prompt: data.prompt ? redactPII(data.prompt) : undefined,
response: data.response ? redactPII(data.response) : undefined,
}
logger[level](message, sanitizedData)
}
// The redactor replaces PII with type indicators
function redactPII(text: string): string {
return text
.replace(/\b\d{3}-\d{2}-\d{4}\b/g, '[SSN_REDACTED]')
.replace(/\b\d{16}\b/g, '[CARD_REDACTED]')
.replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, '[EMAIL_REDACTED]')
.replace(/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g, '[PHONE_REDACTED]')
// Add more patterns as needed
}
Regex-based redaction catches structured PII (SSNs, credit cards, emails) but misses unstructured PII. "My name is John Smith and I live at 123 Oak Street" contains PII that no regex will catch reliably.
For better coverage, use a dedicated PII detection service or library that combines pattern matching with NLP-based entity recognition. Microsoft Presidio, Google Cloud DLP, and AWS Macie offer this capability.
// Using a NER-based redactor for better coverage
async function redactWithNER(text: string): Promise<string> {
const entities = await detectEntities(text)
let redacted = text
// Process entities in reverse order to preserve string positions
for (const entity of entities.reverse()) {
redacted =
redacted.slice(0, entity.start) +
`[${entity.type}_REDACTED]` +
redacted.slice(entity.end)
}
return redacted
}
Not all log data has the same sensitivity. A tiered storage approach routes data to different systems based on its classification.
enum Sensitivity {
PUBLIC = 'public', // Can be logged anywhere
INTERNAL = 'internal', // Internal logs only, 30-day retention
SENSITIVE = 'sensitive', // Encrypted storage, 7-day retention
RESTRICTED = 'restricted' // Audit trail only, no content logging
}
function routeLog(entry: LogEntry) {
switch (entry.sensitivity) {
case Sensitivity.PUBLIC:
standardLogger.log(entry)
break
case Sensitivity.INTERNAL:
internalLogger.log(entry)
break
case Sensitivity.SENSITIVE:
encryptedLogger.log(redactPII(entry))
break
case Sensitivity.RESTRICTED:
auditLogger.log({
timestamp: entry.timestamp,
action: entry.action,
userId: hashUserId(entry.userId),
// No content logged
})
break
}
}
Public tier: Request metadata (timestamp, endpoint, response time, status code). No user content.
Internal tier: Redacted prompts for debugging. Error messages. Model selection and parameters.
Sensitive tier: Full prompts when legally required (e.g., financial services compliance). Encrypted at rest. Short retention.
Restricted tier: Only the fact that an interaction occurred. No content at all. Used for rate limiting and abuse detection.
Different users consent to different levels of data collection. Your logging system needs to respect those preferences.
interface UserConsent {
analyticsLogging: boolean // Can we log usage patterns?
promptLogging: boolean // Can we log prompt content?
modelTraining: boolean // Can we use data for improvement?
thirdPartySharing: boolean // Can we share with partners?
}
function getLoggingLevel(consent: UserConsent): Sensitivity {
if (consent.promptLogging) return Sensitivity.INTERNAL
if (consent.analyticsLogging) return Sensitivity.PUBLIC
return Sensitivity.RESTRICTED
}
async function handlePrompt(userId: string, prompt: string) {
const consent = await getUserConsent(userId)
const loggingLevel = getLoggingLevel(consent)
// Process the prompt
const response = await generateResponse(prompt)
// Log according to consent
routeLog({
sensitivity: loggingLevel,
timestamp: new Date().toISOString(),
action: 'prompt_processed',
userId,
prompt: loggingLevel <= Sensitivity.INTERNAL ? prompt : undefined,
responseLength: response.length,
})
return response
}
When a user withdraws consent, you need to purge their data from the relevant log tiers. Design your storage so that per-user purging is efficient.
async function handleConsentWithdrawal(userId: string, field: keyof UserConsent) {
if (field === 'promptLogging') {
await internalLogger.purgeByUser(userId)
await encryptedLogger.purgeByUser(userId)
}
if (field === 'analyticsLogging') {
await standardLogger.purgeByUser(userId)
}
}
In regulated industries (finance, healthcare, legal), you need to explain why the AI made a specific decision. This requires logging the decision process without logging the sensitive content.
interface AuditEntry {
timestamp: string
requestId: string
userId: string // hashed
modelVersion: string
promptTokens: number
responseTokens: number
toolsUsed: string[]
decision: string // high-level description
confidence: number
// Notably absent: the actual prompt and response content
}
function createAuditEntry(
request: AIRequest,
response: AIResponse
): AuditEntry {
return {
timestamp: new Date().toISOString(),
requestId: request.id,
userId: hashUserId(request.userId),
modelVersion: response.model,
promptTokens: response.usage.prompt_tokens,
responseTokens: response.usage.completion_tokens,
toolsUsed: response.tool_calls?.map(t => t.name) || [],
decision: classifyDecision(response),
confidence: extractConfidence(response),
}
}
The audit trail tells you what happened and why (at a high level) without exposing the sensitive content. If a regulator asks "why did the AI recommend X," you can show the decision classification, the tools used, and the confidence level without revealing the user's private data.
Errors in AI applications often contain sensitive data -- the prompt that caused the error, the response that was malformed, the context that led to a failure. Error logs need the same privacy treatment as request logs.
function logAIError(error: Error, context: AIContext) {
const safeContext = {
errorType: error.constructor.name,
errorMessage: error.message, // Usually safe
model: context.model,
promptLength: context.prompt.length, // Length, not content
toolsInvolved: context.tools,
retryCount: context.retryCount,
// Redacted content for debugging
promptPreview: redactPII(context.prompt.slice(0, 100)) + '...',
}
errorLogger.error('AI processing failed', safeContext)
}
The promptPreview field gives developers enough context to debug the issue without exposing the full prompt. Combined with the prompt length and tools involved, this is usually sufficient to identify and fix the problem.
Before shipping your AI application, verify these privacy-first logging requirements:
For building the analytics layer on top of these privacy-safe logs, see our guide on privacy-first analytics for dev tools.
It makes it different, not harder. You lose the ability to see raw prompts in your logs, but you gain structured metadata that is often more useful for debugging. Most AI bugs are about the model, the tools, or the context -- not the specific words in the prompt.
Privacy-first logging is a strong foundation for GDPR compliance but does not replace a full GDPR assessment. You also need data processing agreements, privacy policies, and data subject access request (DSAR) procedures. The logging patterns in this guide handle the technical requirements.
Encrypt the sensitive and restricted tiers. Public and internal tier encryption is optional depending on your threat model. At minimum, encrypt at rest and in transit. See the Supabase documentation for database-level encryption patterns.
PII patterns vary by language and locale. A US SSN pattern will not catch a UK National Insurance number. Use locale-aware redaction rules and consider NER-based approaches that handle multiple languages.
Regex-based redaction adds microseconds per log entry. NER-based redaction adds 10-50 milliseconds per entry. For most applications, the latency is negligible compared to the AI inference time. If performance is critical, run redaction asynchronously.
Explore production-ready AI skills at aiskill.market/browse or submit your own skill to the marketplace.
Enterprise-grade security auditing skills from Trail of Bits. Run CodeQL and Semgrep analyses, detect vulnerabilities, and enforce security best practices in your codebase.
Secure environment variable management ensuring secrets are never exposed in Claude sessions or git
Security Blue Book Builder is a Claude Code skill that helps development teams create concise, normative security policies for applications handling sensitive data such as PII, PHI, or financial infor
Extract and analyze file metadata for forensic purposes and investigations