Configure Logging
Deciding What the Program Says About Itself
Lesson 147 showed how to write structured error events, one line at a time, from the code that handles errors. Configuring logging is the layer beneath that code. It decides where log lines go, how detailed each part of the program may be, which lines are kept, and which are dropped. A well-configured program is quiet when it is healthy and loud when something needs attention, and its loudness is set in one place, not scattered through the code.
This lesson covers the parts of Python's logging system that matter for an application that calls a model: loggers, levels, handlers, and formatters; the configuration that wires them together; the SDK's own logging switch; and filters that remove sensitive values before a line is written. The configuration is shown as data, so it can be checked and changed without editing the code that logs.
Loggers, Levels, and Handlers
The logging system has three moving parts, and each one answers a different question:
| Part | Question it answers | Example |
|---|---|---|
| Logger | Which part of the program is speaking? | gocourse.api, anthropic |
| Level | How important is this line? | DEBUG, INFO, WARNING, ERROR |
| Handler and formatter | Where does the line go, and in what shape? | Standard output, as JSON |
Loggers are named in a hierarchy, with dots separating the parts. A logger called gocourse.api inherits its settings from gocourse unless it has its own, which lets one setting cover a whole area of the program. The level sets a threshold: a logger configured at WARNING drops INFO and DEBUG lines before they reach any handler, so a quiet production setting costs almost nothing.
Choosing Levels
Each level has a purpose, and using them consistently is what makes a log useful:
| Level | Use it for |
|---|---|
DEBUG | Detail needed only while investigating a specific problem |
INFO | Normal, meaningful events: a service started, a job finished |
WARNING | Something unexpected that the program handled: a retry, a fallback |
ERROR | A failure that the program could not recover from for this request |
A production configuration usually keeps the application at INFO, so that normal events are recorded, and sets the SDK's logger higher, since its own detail is rarely needed in production. A problem under investigation can raise one logger to DEBUG without changing anything else.
Configuring with a Dictionary
Python accepts logging configuration as a dictionary, which can be kept in a file, compared between environments, and tested. The configuration below uses the JSON formatter from Lesson 147, sends the application's logs to standard output, and sets the SDK's logger to warnings only:
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"json": {"()": "api_logging.JsonFormatter"},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "json",
"stream": "ext://sys.stdout",
},
},
"loggers": {
"gocourse": {"level": "INFO", "handlers": ["console"], "propagate": False},
"anthropic": {"level": "WARNING"},
},
}
The "()" key tells the logging system to build the formatter by calling the named class, which is how the JSON formatter from Lesson 147 is plugged in without importing it into the configuration itself. The propagate setting stops the application's lines from also reaching the root logger, which would print each line twice.
The SDK's Own Logging
The SDK writes its own log lines, which describe requests and responses. They are controlled two ways. The environment variable ANTHROPIC_LOG turns on the SDK's logging at a chosen level, as Lesson 97 showed. The SDK's logger can also be configured in the same dictionary, as above, which keeps the setting next to the rest of the configuration. The environment variable is the right tool for a temporary investigation, and the dictionary is the right place for a standing policy.
Detailed SDK logging can include more than the policy allows, so it should stay at warnings in production, and be raised only when a specific question needs answering.
Filtering Sensitive Values
Some values must never reach a log, whatever level is configured. A filter sits on a handler or a logger, examines each record before it is written, and can change the record. The filter below replaces the value of any field whose name suggests a credential:
"""logging_setup.py: apply the logging configuration, and redact credential-like fields."""
import logging
import logging.config
import sys
class RedactFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
fields = getattr(record, "fields", None)
if fields:
record.fields = {k: ("[redacted]" if "key" in k.lower() else v) for k, v in fields.items()}
return True
if __name__ == "__main__":
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {"json": {"()": "api_logging.JsonFormatter"}},
"handlers": {"console": {"class": "logging.StreamHandler", "formatter": "json",
"stream": "ext://sys.stdout"}},
"loggers": {"gocourse": {"level": "INFO", "handlers": ["console"], "propagate": False},
"anthropic": {"level": "WARNING"}},
}
logging.config.dictConfig(LOGGING)
logging.getLogger("gocourse").handlers[0].addFilter(RedactFilter())
app = logging.getLogger("gocourse.api")
app.info("service_started", extra={"fields": {"provider": "direct", "timeout": 60.0}})
app.info("request_config", extra={"fields": {"api_key": "sk-ant-api03-EXAMPLE", "model": "claude-sonnet-5-5"}})
app.debug("detail_hidden_at_info_level")
logging.getLogger("anthropic").info("sdk_detail_hidden_at_warning_level")
Output:
{"event": "service_started", "level": "info", "provider": "direct", "timeout": 60.0}
{"api_key": "[redacted]", "event": "request_config", "level": "info", "model": "claude-sonnet-5-5"}
How It Works
- The configuration is applied once, at startup, before any logging happens.
- The filter is attached to the handler, not to a single call, so every line written to that handler passes through it, including lines added later by other developers.
- The redaction removes the value and keeps the field name, so the log still shows that a key was present, without showing it.
- The debug line is absent, because the application's logger is at
INFO, and the level check drops it before the handler sees it. - The SDK's info line is absent, because the
anthropiclogger is atWARNING, which is the quiet production setting from the configuration.
Logs Are Data
The records a configuration produces are data about users and their work, and they must be protected like the rest of the system's data (Lessons 147 and 180). Configuration should decide retention, the destination should be a system with access controls, and the levels should be chosen so that the logs contain events and decisions, not content. A configuration that sends everything to a shared console for convenience is a configuration that exposes everything to everyone with access to the console.
Summary
Logging configuration decides which parts of the program speak, how loudly, where their lines go, and in what form. Levels are used consistently, the application stays at INFO, and the SDK is held at warnings unless a question requires more. The configuration is a dictionary that can be checked and versioned, the JSON formatter from Lesson 147 is wired in by name, and a filter removes credential-like values before they are written. The next lesson turns to packaging the application in a container, which is how it is deployed.