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.
| Log | Do not log |
|---|---|
| model ID | the full prompt text |
| input and output tokens | the full model response |
| request cost | emails, phone, card and document numbers in plain text |
| response time | names and addresses |
| response ID | API keys and auth tokens |
| HTTP status and error type | full request headers |
| prompt version, function name in code | anything 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.
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 textCheck:
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.
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/modelsand 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.
import os
ENV = os.getenv("APP_ENV", "dev")
API_KEY = os.environ[f"ZUKKO_KEY_{ENV.upper()}"]
MASK_LOGS = TrueKeys 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:
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):
- Writing to a log is also processing of personal data, so the same rules apply as to your main database.
- Minimization: do not collect in logs what you can do without.
- Limited access and retention.
- 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.
Related articles
- How to update Claude Code, roll back a version or remove it completelyCommands to update, roll back and remove Claude Code by install method, and how to keep your gateway connection settings when reinstalling.
- OpenAI API Keys in the Open: Why You Should Not Use ThemWhere free keys on GitHub come from, why using them is dangerous for you and for the owner, and how to protect your own key from the same leak.
- GLM API Errors: Why the Request Fails and How to Fix ItA wrong model id, an overflowing 128K context, the key, the balance, limits: the typical causes of GLM API errors and a quick way to find yours.
- This organization has been disabled in the Claude API: What to DoAn Anthropic key suddenly answers "organization has been disabled". What the error is, what to check, and how to move the SDK and Claude Code to another provider.