Configure Logging

Su Suriya Ravichandran Updated 10 Nov 2026
5 min read ·Lesson 187 of 200

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:

PartQuestion it answersExample
LoggerWhich part of the program is speaking?gocourse.api, anthropic
LevelHow important is this line?DEBUG, INFO, WARNING, ERROR
Handler and formatterWhere 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:

LevelUse it for
DEBUGDetail needed only while investigating a specific problem
INFONormal, meaningful events: a service started, a job finished
WARNINGSomething unexpected that the program handled: a retry, a fallback
ERRORA 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

  1. The configuration is applied once, at startup, before any logging happens.
  2. 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.
  3. The redaction removes the value and keeps the field name, so the log still shows that a key was present, without showing it.
  4. The debug line is absent, because the application's logger is at INFO, and the level check drops it before the handler sees it.
  5. The SDK's info line is absent, because the anthropic logger is at WARNING, 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.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Configure Logging and get answers drawn from it.

Signed-in readers only.