Logging LLM requests without leaking personal data

Logs are needed for debugging and cost tracking, but LLM requests often contain personal data. What to log, how to mask it and how to store it.

4 min · · development · errors

Logging LLM requests is needed for debugging, cost tracking and monitoring. But model requests often contain personal data: names, emails, phone numbers, card numbers. A log is a bad place for them: files get copied, shipped to monitoring systems and read by people who have no access to the source data. Here is how to log LLM requests without leaking data: what to record, how to mask it and how to store it.

What to log and what not to

A safe log is built on metadata, not content.

LogDo not log
model IDthe full prompt text
input and output tokensthe full model response
request costemails, phone, card and document numbers in plain text
response timenames and addresses
response IDAPI keys and auth tokens
HTTP status and error typefull request headers
prompt version, function name in codeanything that identifies a person

If you need text for debugging, log it masked and truncated.

Masking personal data in Python

Regular expressions will not catch everything, but they cover the most common patterns. Order matters: keys and emails first, then card numbers, then phone numbers; otherwise the phone pattern may "eat" part of a card number.

bash
import re

PATTERNS = [
    ("API_KEY", re.compile(r"\b(?:fetch|sk)[-_][A-Za-z0-9_\-]{8,}")),
    ("EMAIL", re.compile(r"[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}")),
    ("CARD", re.compile(r"(?<!\d)(?:\d{4}[ \-]?){3}\d{4}(?!\d)")),
    ("PHONE", re.compile(r"(?<!\w)(?:\+7|8)[ \-]?\(?\d{3}\)?[ \-]?\d{3}[ \-]?\d{2}[ \-]?\d{2}(?!\d)")),
    ("PASSPORT", re.compile(r"(?<!\d)\d{2} ?\d{2} \d{6}(?!\d)")),
]

def mask_personal_data(text: str) -> str:
    for label, pattern in PATTERNS:
        text = pattern.sub(f"[{label}]", text)
    return text

Check:

bash
print(mask_personal_data("My email is john.doe@example.com, phone +7 916 123 45 67"))
print(mask_personal_data("Card 2200 1234 5678 9012, passport 45 06 123456"))

Output:

My email is [EMAIL], phone [PHONE] Card [CARD], passport [PASSPORT]

The phone pattern targets Russian numbers (+7 or 8 followed by ten digits). If you have other formats, add them and test them on real examples from your data. Names and addresses cannot be found reliably with regular expressions; if they are critical, do not log the text at all.

Example: logging a model request

We log one JSON line per request. Cost uses Zukko prices: cost = (input × input_price + output × output_price) / 1,000,000.

bash
import json
import logging
import time
from datetime import datetime, timezone

import anthropic

logger = logging.getLogger("llm")
logging.basicConfig(level=logging.INFO, format="%(message)s")

client = anthropic.Anthropic(
    base_url="https://api.zukko.pro",
    api_key="fetch_your_key",
)

What this does:

  • only metadata and a short masked fragment of the request (preview) reach the log; drop the field if you do not need it;
  • error logs record the exception type and status, not str(e): error text sometimes contains part of the request;
  • prices live in one dict; the model set changes, so check it against GET https://api.zukko.pro/v1/models and the "Models" page in the dashboard.

Do not enable HTTP client debug logging in production: in that mode request and response bodies may end up in the log.

Where and how to store logs

Development. A local file, with masking on from day one; sooner or later someone else will read dev logs.

Staging. A central log collector, masking required, access only for the project's developers.

Production.

  • Logs only in a centralized system with access control.
  • An explicit retention period: keep logs no longer than needed for debugging and accounting.
  • Masking before sending, not on the storage side.
  • Encryption in transit and at rest.

Separate keys per environment

It is convenient to have a separate Zukko key per environment. Each key has a name, daily and monthly limits in $ and an allowed-models list, so a bug in dev will not burn the production budget.

bash
import os

ENV = os.getenv("APP_ENV", "dev")
API_KEY = os.environ[f"ZUKKO_KEY_{ENV.upper()}"]
MASK_LOGS = True

Keys are created under API keys; the full key is shown only once. More on limits in the docs.

Tracking spend from logs

The JSON lines make it easy to total spend by model and prompt version:

bash
import json
from collections import defaultdict

def costs_by_model(log_file: str) -> dict:
    totals = defaultdict(float)
    with open(log_file, encoding="utf-8") as f:
        for line in f:
            try:
                entry = json.loads(line)
            except json.JSONDecodeError:
                continue
            if entry.get("status") == 200:
                totals[entry["model"]] += entry.get("cost_usd", 0.0)
    return dict(totals)

Reconcile the result with the "Usage" section in the dashboard: it shows actual charges, and balance and spending history are also available via the API (see Usage).

About compliance

This is not legal advice, just general principles worth keeping in mind (in Russia the relevant law is 152-FZ; other jurisdictions have their own):

  1. Writing to a log is also processing of personal data, so the same rules apply as to your main database.
  2. Minimization: do not collect in logs what you can do without.
  3. Limited access and retention.
  4. Sending text with personal data to an external API is also processing. If the task allows, mask data before it is sent to the model.

Check the specific requirements for your project with a lawyer.

FAQ

Do I need to log every request?

Metadata (tokens, cost, status), yes: it is short and needed for accounting. Text fragments can be logged selectively, for example for 1% of requests or only on errors.

What if masking misses data?

It can happen; regular expressions are not perfect. That is why the main protection is not writing text to the log at all, with masking as a backup. Regularly review a sample of logs for misses.

What if sensitive data has already reached the log?

Find and delete or rewrite the affected records in every copy, including backups and the log collector. If an API key leaked into the log, create a new key in the dashboard, switch the app to it and delete the old one.

Can I store logs in a cloud service?

Yes, if there is encryption in transit and at rest, access control and a clear retention period. Check data residency requirements with a lawyer.

zukko

Pay 25×
less

for Claude Code & ChatGPT

One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.

Get API keyDocumentation

api.zukko.pro · fetch_*

Logging LLM requests safely: data masking and compliance · Zukko