TechByteByByte

Logging and Production Python

Learn how to replace print statements with proper logging in Python, using logging levels, formatting, and file logging, and understand what should never appear in AI application logs.

#Python#Logging#Production#Debugging#AI#Python for AI

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

LevelWhen to use itAI example
DEBUGDetailed internal info, useful only while actively debugging“Raw prompt sent to model: …”
INFONormal, expected events worth recording“Request processed successfully in 1.2s”
WARNINGSomething unexpected, but not breaking anything“Retrieved only 1 document, expected at least 3”
ERRORSomething failed, this specific operation didn’t complete“LLM API call failed after 3 retries”
CRITICALThe 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 at ERROR) 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 structlog or custom formatters to convert standard logging records into JSON format automatically, allowing you to instantly search logs by model, latency_ms ranges, 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 everything ERROR.
  • 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 INFO level 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:

  1. Use JSON Structured Logging (Section 5): JSON strings naturally escape newlines (replacing them with \n characters in a single-line string), keeping the entire log record on a single physical line of text.
  2. 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

SignalBest question it answersAI example
LogWhat happened in this event?Retrieval returned zero documents
MetricHow often or how much over time?p95 latency or tokens per request
TraceWhere 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

  1. Set up basic logging configuration with a custom format including timestamp, level, and message, then log one message at each severity level.
  2. Write a function that logs a WARNING if a list of retrieved documents is empty, and an INFO message with the count otherwise.
  3. Wrap a function that might raise an exception in try/except, and use logger.exception(...) to log the failure with its full traceback.
  4. 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.
  5. Explain, in your own words, why logging every raw user prompt at INFO level 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