The problem: A local
print()helps someone watching the terminal now. An unattended AI service needs searchable evidence hours later, across many requests and components—without exposing prompts, personal data, or API keys.What you will learn: Logging records events with time, source, severity, and request context. You will create and route those records, distinguish logs from metrics and traces, and decide what must never be logged. The goal is enough evidence to investigate a 3 a.m. RAG failure without creating a privacy risk or an uncontrolled storage bill.
1. Why Logging Matters
Logging turns events inside a running program into evidence that can be examined later. A useful log record answers several small questions:
WHEN did it happen? timestamp
WHERE did it happen? module or service
WHAT happened? event and severity
WHICH request? request/trace ID
Logs do not automatically become permanent or searchable merely because Python created them. A handler sends records to the console, a file, or a logging platform, and that destination controls retention and search. Sensitive values must be removed before the record leaves the application.
A Record of Program Events
Logging is the practice of recording what a program is doing, as it runs, in a structured, searchable, permanent way.
Why print() Is Not Enough in Production
print() output disappears the moment your terminal closes. In
production, nobody is watching your terminal — your AI service runs on a
server, handling requests from real users, unattended, 24/7. When
something breaks, logs are the only way to reconstruct what happened.
Picture a Searchable Notebook
print() is shouting into an empty room — useful only if someone happens
to be standing there listening right now. Logging is writing everything
down in a notebook that’s still there tomorrow, searchable, timestamped,
and categorized by importance.
The Flight Recorder Analogy
Think of a flight data recorder (“black box”) on an airplane. Pilots don’t narrate every action out loud for someone to overhear — the black box quietly records everything, in a structured way, so that if something goes wrong, investigators can reconstruct exactly what happened, in order, with timestamps.
2. print vs logging
# Bad / naive approach — using print for everything
print("Starting API call")
print("API call succeeded")
print("Warning: response took longer than expected")
print("ERROR: API call failed after 3 retries")
# Better approach — using logging, with severity built in
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info("Starting API call")
logger.info("API call succeeded")
logger.warning("Response took longer than expected")
logger.error("API call failed after 3 retries")
Expected Output (logging version):
INFO:__main__:Starting API call
INFO:__main__:API call succeeded
WARNING:__main__:Response took longer than expected
ERROR:__main__:API call failed after 3 retries
Why the better approach matters in AI
With print(), every message looks identical — you cannot tell “routine
status update” apart from “something is badly wrong” without reading and
judging every line yourself. With logging:
- Messages carry a severity level, so you can filter (“show me only warnings and above”) without reading everything.
- Output can be routed to a file, a monitoring service, or the console — configurable in one place, without touching your actual code.
- Every message is automatically timestamped and labeled with where it
came from (
__main__, or a specific module name).
For a script you run once and read the output of yourself, print() is
fine. For an AI service running unattended, logging is the only
reasonable choice.
3. Logging Levels
| Level | When to use it | AI example |
|---|---|---|
DEBUG | Detailed internal info, useful only while actively debugging | “Raw prompt sent to model: …” |
INFO | Normal, expected events worth recording | “Request processed successfully in 1.2s” |
WARNING | Something unexpected, but not breaking anything | “Retrieved only 1 document, expected at least 3” |
ERROR | Something failed, this specific operation didn’t complete | “LLM API call failed after 3 retries” |
CRITICAL | The whole application/service is in serious trouble | “Database connection pool exhausted” |
In Python, each level is associated with a numeric value. Setting the logging volume to a level displays all messages at that severity and above:
graph TD
CRITICAL[CRITICAL: 50] --> ERROR[ERROR: 40]
ERROR --> WARNING[WARNING: 30]
WARNING --> INFO[INFO: 20]
INFO --> DEBUG[DEBUG: 10]
style CRITICAL fill:#f99,stroke:#333,stroke-width:2px
style ERROR fill:#fcb,stroke:#333,stroke-width:2px
style WARNING fill:#ffe,stroke:#333,stroke-width:2px
style INFO fill:#bbf,stroke:#333,stroke-width:2px
style DEBUG fill:#eee,stroke:#333,stroke-width:2px
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
logger.debug("Prompt token count: 245")
logger.info("Document retrieval completed")
logger.warning("Similarity score below expected threshold: 0.42")
logger.error("Model API returned status 500")
logger.critical("Vector database is unreachable")
🧠 Intuition
Levels are a volume dial for importance — setting level=logging.INFO
means “show me INFO and anything more severe (WARNING, ERROR, CRITICAL),
but hide DEBUG” — letting you tune how much noise you see without deleting
any code.
⚠️ Common Beginner Mistake: Logging everything at
INFO(or worse, everything atERROR) regardless of actual severity. This defeats the entire purpose of levels — if everything is “important,” nothing stands out when you’re scanning logs for a real problem.
🤖 How Is This Used in AI? A production RAG service typically logs at
INFO in normal operation (so you have a record of activity without
overwhelming detail), and temporarily switches to DEBUG while actively
investigating a specific issue (e.g., “why did this particular query
return irrelevant documents?”).
4. Log Formatting
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
)
logger = logging.getLogger("rag_pipeline")
logger.info("Query received")
logger.warning("Low relevance score: 0.38")
Expected Output (approximate):
2026-08-15 10:32:01,123 | INFO | rag_pipeline | Query received
2026-08-15 10:32:01,456 | WARNING | rag_pipeline | Low relevance score: 0.38
🧠 Intuition
A consistent format is what makes logs searchable and parseable —
both by a human scanning them, and by automated monitoring tools that
watch for specific patterns (like every ERROR line) and can trigger
alerts.
🤖 How Is This Used in AI? Production AI services almost always include a timestamp (when did this happen?) and a logger name (which part of the pipeline — retrieval? generation? tool-calling?) so that when you’re debugging an incident, you can immediately see the timeline and pinpoint which component was involved.
5. Logging to Files
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(message)s",
filename="ai_pipeline.log",
filemode="a", # append, not overwrite — recall Module 7!
)
logger = logging.getLogger(__name__)
logger.info("Pipeline started")
logger.error("Failed to connect to vector database")
This writes every log message into ai_pipeline.log instead of (or in
addition to) the console — so the record persists even after your program
exits, and can be reviewed later or shipped to a centralized logging
system.
🧠 Recall Module 7’s file-append mode ("a") — filemode="a" here is the
exact same idea, applied to logs: every run adds to the history instead of
erasing what came before.
[!IMPORTANT] Production Reality: Structured Logging (JSON format) While text files with simple layouts (e.g.
2026-08-20 23:22:36 | INFO | Pipeline started) are great for human reading in small scripts, they are hard to query in production. Modern production monitoring platforms (like Datadog, Elasticsearch, or Splunk) ingest logs from thousands of parallel microservices.To support automated filtering and querying, production AI applications output Structured Logs (JSON strings) where metadata fields are explicitly separated:
{ "timestamp": "2026-08-20T23:22:36Z", "level": "INFO", "logger": "rag_pipeline", "message": "API call succeeded", "model": "gpt-4o-mini", "latency_ms": 320 }In Python, you can use specialized libraries like
structlogor custom formatters to convert standard logging records into JSON format automatically, allowing you to instantly search logs bymodel,latency_msranges, or specific user IDs.
6. Exception Logging
import logging
logger = logging.getLogger(__name__)
def call_llm(prompt):
try:
if not prompt:
raise ValueError("Prompt cannot be empty")
return f"Response to: {prompt}"
except ValueError as e:
logger.exception("Failed to process prompt") # logs the FULL traceback
raise
try:
call_llm("")
except ValueError:
print("Handled the error after logging it.")
Expected Output (approximate):
ERROR:__main__:Failed to process prompt
Traceback (most recent call last):
File "...", line ..., in call_llm
raise ValueError("Prompt cannot be empty")
ValueError: Prompt cannot be empty
Handled the error after logging it.
🧠 Intuition
logger.exception(...) is like logger.error(...), but it also
automatically captures the full traceback — exactly what code path led
to the failure — which is invaluable when debugging a failure you can’t
reproduce interactively.
🤖 How Is This Used in AI? When an LLM API call fails deep inside a
multi-step agent pipeline, logger.exception(...) at the point of failure
means you can trace exactly which step, with what input, caused the
problem — instead of just knowing “something broke somewhere.”
7. Logging AI Applications
A realistic combined example:
import logging
import time
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
)
logger = logging.getLogger("rag_service")
def retrieve_documents(query):
logger.info(f"Retrieving documents for query: '{query}'")
time.sleep(0.1) # simulate a vector DB lookup
results = [
{"text": "Python is used for AI development.", "score": 0.91},
{"text": "Bananas are yellow.", "score": 0.12},
]
relevant = [r for r in results if r["score"] >= 0.5]
if len(relevant) == 0:
logger.warning("No relevant documents found above threshold")
else:
logger.info(f"Retrieved {len(relevant)} relevant document(s)")
return relevant
def generate_answer(query, documents):
if not documents:
logger.error("Cannot generate answer: no documents provided")
return None
logger.info("Calling LLM to generate answer")
time.sleep(0.1) # simulate an LLM API call
return f"Answer based on {len(documents)} document(s) for: {query}"
def handle_query(query):
logger.info(f"New request received: '{query}'")
documents = retrieve_documents(query)
answer = generate_answer(query, documents)
if answer:
logger.info("Request completed successfully")
return answer
result = handle_query("How is Python used in AI?")
print(result)
Expected Output (approximate):
2026-08-15 10:40:01,001 | INFO | rag_service | New request received: 'How is Python used in AI?'
2026-08-15 10:40:01,001 | INFO | rag_service | Retrieving documents for query: 'How is Python used in AI?'
2026-08-15 10:40:01,102 | INFO | rag_service | Retrieved 1 relevant document(s)
2026-08-15 10:40:01,102 | INFO | rag_service | Calling LLM to generate answer
2026-08-15 10:40:01,203 | INFO | rag_service | Request completed successfully
Answer based on 1 document(s) for: How is Python used in AI?
This is a genuinely realistic sketch of how a small RAG service logs its own pipeline: request received → retrieval → (warning if nothing relevant found) → generation → completion — each stage visible, timestamped, and labeled by severity.
8. Debugging AI Pipelines with Logs
Consider this flow, and what each log line tells a future debugger:
User request
↓
[INFO] New request received: '...'
↓
RAG retrieval
↓
[INFO] Retrieved N relevant document(s)
[WARNING] No relevant documents found above threshold ← if retrieval struggled
↓
LLM call
↓
[INFO] Calling LLM to generate answer
[ERROR] Model API returned status 500 ← if the call failed
↓
Response
↓
[INFO] Request completed successfully
↓
Logs
🧠 Intuition: Good logging turns an invisible pipeline into a readable story — when a user reports “the AI gave a weird answer,” you can look at the logs for that request’s timestamp and reconstruct exactly what happened at each stage, without needing to reproduce the bug live.
🤖 How Is This Used in AI? This is the actual, everyday debugging
workflow for production RAG and agent systems: something went wrong for a
user → find the relevant timestamp in the logs → read the story of that
request from INFO/WARNING/ERROR lines → identify exactly which stage
failed and why.
9. Basic Production Practices
A few standards worth adopting from the start:
- Use a named logger per module, not the root logger —
logging.getLogger(__name__)automatically labels logs by which file they came from. - Log at the right level — don’t make everything
INFO, don’t make everythingERROR. - Include context, not just a message —
"Failed for query: {query}"is far more useful later than a bare"Failed". - Don’t log inside tight loops at high volume — logging every single
token of a stream at
INFOlevel will flood your logs and hide the messages that actually matter. - Centralize configuration — set up
logging.basicConfig(...)once, near your application’s entry point, not scattered across many files.
10. What Should and Should NOT Be Logged
✅ Safe and useful to log
- Request metadata: timestamp, which endpoint/operation, how long it took
- High-level outcomes: “retrieval returned 3 documents,” “generation succeeded”
- Error types and messages (without sensitive payload data)
- Performance metrics: latency, token counts, retry counts
⚠️ WARNING — Never log these
- API keys or secrets, even partially in some contexts — recall
Module 8’s masking pattern (
key[:4] + "..." + key[-4:]) if you must reference a key at all - Full user prompts or documents, if they may contain personal or sensitive information — depending on your application’s privacy requirements, log a length or a hash instead of the raw content
- Full API responses containing user data, without review
- Passwords, tokens, or database connection strings
import logging
logger = logging.getLogger(__name__)
api_key = "sk-ant-abc123realkeyvalue"
# NEVER do this:
# logger.info(f"Using API key: {api_key}")
# Do this instead — mask it, exactly like Module 8:
masked_key = api_key[:6] + "..." + api_key[-4:]
logger.info(f"Using API key: {masked_key}")
Expected Output:
INFO:__main__:Using API key: sk-ant...value
[!WARNING] Production Alert: Multi-Line Prompts Breaking Log Parsers In AI applications, user prompts and model responses frequently contain newlines (
\n). If you log a multi-line string directly:prompt = "Summarize this:\n- Point 1\n- Point 2" logger.info(f"Incoming prompt: {prompt}")A standard text-based logger will output this across three separate lines in your log file.
This is a disaster in production because log collectors (like Datadog, Logstash, or Fluentd) treat each line of text as a completely separate log entry. This splits a single API trace across multiple unrelated entries, making it impossible to search or parse!
How to solve this in production:
- Use JSON Structured Logging (Section 5): JSON strings naturally escape newlines (replacing them with
\ncharacters in a single-line string), keeping the entire log record on a single physical line of text.- Sanitize Newlines for Text Logs: If using text logs, replace newlines with a symbol before printing:
safe_prompt = prompt.replace("\n", " [NEWLINE] ") logger.info(f"Incoming prompt: {safe_prompt}")
✅ Key Takeaway: A leaked API key or a logged full user conversation containing personal information is a real security and privacy incident — not a hypothetical one. Treat log statements with the same care you’d give to code that handles secrets, because logs frequently end up stored, searched, and sometimes even accidentally made public.
Logs, Metrics, and Traces Answer Different Questions
| Signal | Best question it answers | AI example |
|---|---|---|
| Log | What happened in this event? | Retrieval returned zero documents |
| Metric | How often or how much over time? | p95 latency or tokens per request |
| Trace | Where did one request spend time? | API → retrieval → reranker → model |
A request or trace ID should travel through the pipeline and appear in each related log. This lets an engineer connect events without recording the user’s entire prompt. Structured JSON logs are easier for software to search than one long human-formatted sentence.
Privacy and Log Volume
Prompts, retrieved documents, model outputs, tool arguments, and user IDs can contain personal or confidential data. Prefer lengths, hashes, categories, and approved identifiers; redact secrets before logging. Decide who may read logs and how long the organization keeps them.
Avoid adding the same handler multiple times, which can duplicate every log line. Avoid logging every streamed token too: high-volume logs cost money and can hide the few events that explain a real failure.
Module Summary
You now know why logging replaces print() in production code, how to
use severity levels appropriately, how to format and persist logs to
files, how to capture full tracebacks with logger.exception(...), and —
critically — the discipline of deciding what belongs in a log and what
must never appear there.
AI Connection
An AI pipeline running unattended in production is only debuggable through its logs. Every stage — retrieval, generation, tool calls, retries — should leave a clear, appropriately-leveled trail, so that when something goes wrong (and eventually, something always does), you can reconstruct exactly what happened without needing to reproduce the failure live. And because AI pipelines constantly handle API keys and user data, logging discipline here isn’t optional — it’s a real security requirement.
Mini Practice
- Set up basic logging configuration with a custom format including timestamp, level, and message, then log one message at each severity level.
- Write a function that logs a
WARNINGif a list of retrieved documents is empty, and anINFOmessage with the count otherwise. - Wrap a function that might raise an exception in
try/except, and uselogger.exception(...)to log the failure with its full traceback. - Write a function
mask_api_key(key)that returns a safely-masked version of an API key, and use it in a log message instead of the raw key. - Explain, in your own words, why logging every raw user prompt at
INFOlevel could be a problem in a real production AI service, and describe one safer alternative.
Next: Module 13 — Async Python — concurrent LLM calls and async agent tools.
- Author
- TechByteByByte Editorial Team
- Reviewed by
- TechByteByByte Admin
- Published
- Last reviewed