Logging
When an agent misbehaves in production, you cannot ask it what it was thinking. You can only read what it wrote down. Logging is how it writes things down.
This page is short because the library is simple. It matters because the alternative — print everywhere — falls apart the moment anything runs unattended.
Why not just use print
print is fine while you are learning. It stops being fine quickly.
print | logging | |
|---|---|---|
| Switch off in production | delete every line | change one setting |
| Levels of importance | none | debug, info, warning, error |
| Timestamps | you add them by hand | automatic |
| Where it goes | the screen, always | screen, file, or a log service |
| Which module it came from | unknown | recorded |
The deciding one is the first. You will scatter print calls while debugging, and then either leave them in, where they clutter production output, or hunt them all down. With logging you change one line and the noise disappears.
The smallest useful setup
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s - %(message)s",
)
logger = logging.getLogger(__name__)
logger.info("agent started")
logger.warning("retrying after timeout")
logger.error("tool call failed")2026-09-02 09:14:01,203 INFO myagent.graph - agent started
2026-09-02 09:14:02,551 WARNING myagent.graph - retrying after timeoutTwo habits in that snippet are worth copying exactly:
basicConfigonce, at the entry point of your program, never in a library modulegetLogger(__name__)in each module, so every line records where it came from
The levels, and what each is for
| Level | Use it for |
|---|---|
debug | detail you want while diagnosing — full prompts, raw responses |
info | the normal story — started, finished, chose this tool |
warning | something unexpected but survivable — a retry, a fallback |
error | something failed and you could not recover |
critical | the process cannot continue |
Setting level=logging.INFO means debug messages are computed but not shown. Switch to logging.DEBUG while investigating and the detail appears with no code change. That is the entire point of levels.
Logging an exception properly
try:
result = call_tool(name, args)
except Exception:
logger.exception("tool %s failed", name)logger.exception records the message and the full traceback, and it only works inside an except block. It is almost always what you want there — logger.error alone tells you something failed but not where.
Note the comma rather than an f-string. Passing %s and the value separately means the string is only built if the message is actually emitted, and log services can group identical messages. It is a small habit with real benefit at volume.
Writing to a file
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s - %(message)s",
handlers=[
logging.FileHandler("agent.log", encoding="utf-8"),
logging.StreamHandler(),
],
)That writes to both the file and the screen. Note encoding="utf-8" on the file handler, for the same reason as everywhere else.
What to log in an agent
Agents are hard to debug because the interesting decisions happen inside a model. Log the boundaries.
logger.info("run started | question=%s", question)
logger.info("model chose tool=%s args=%s", tool_name, args)
logger.info("tool returned | tool=%s chars=%d", tool_name, len(result))
logger.warning("retry %d of %d", attempt, max_attempts)
logger.info("run finished | steps=%d tokens=%d", steps, tokens)A run's log should let you answer, without re-running anything:
- what was asked
- which tools were chosen, in what order, with what arguments
- what each tool gave back
- how many steps it took, and why it stopped
- what it cost
That last one matters more than beginners expect. An agent that loops is an agent spending money, and the log is where you notice.
Never log secrets
logger.info("calling API with key=%s", api_key)Do not do this. Logs get copied into tickets, pasted into chats, and shipped to third-party services. An API key in a log is a leaked key.
The same applies to personal data. Log the ticket id, not the customer's full record.
logger.info("calling API | key=***%s", api_key[-4:])Turning down a noisy library
Some libraries log heavily at INFO. Quiet them individually rather than raising your own level:
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)Logging is not tracing
Worth being clear about, because they solve neighbouring problems.
- Logging is a flat list of lines you wrote deliberately
- Tracing records the nested structure of a run — this call inside that step inside this run — with timings and token counts
For agents you will eventually want both, and tools like LangSmith give you the second. Logging is what you have on day one, works everywhere, and needs no account.
Coming from Java or C#
This will be familiar territory.
| Concept | Java | C# | Python |
|---|---|---|---|
| Library | SLF4J + Logback, Log4j | ILogger, Serilog | logging, built in |
| Get a logger | LoggerFactory.getLogger(X.class) | injected ILogger<T> | logging.getLogger(__name__) |
| Levels | trace, debug, info, warn, error | same | debug, info, warning, error, critical |
| Configuration | XML or properties file | appsettings.json | basicConfig, or dictConfig |
| Placeholders | {} | {Name} | %s |
Two differences: Python's logging is in the standard library, so there is nothing to add; and configuration is usually code rather than a file, which is simpler for small projects and less tidy for large ones.
Common mistakes
printin code that runs unattended.basicConfigin a library module, which hijacks the configuration of whatever imports it.logger.errorinsideexceptwherelogger.exceptionwould have given you the traceback.- f-strings in log calls, which build the message even when it will not be shown.
- Logging secrets or personal data.
- Logging nothing at all until something breaks, at which point there is nothing to read.
Practise this
- Set up
basicConfigand log one line at each of the five levels. Change the level toWARNINGand see which survive. - Catch an exception and log it with
logger.exception. Confirm the traceback appears. - Add logging to a function that calls something external, recording what went in and what came back.
- Write a log line that includes only the last four characters of a secret.