Log API Errors

Su Suriya Ravichandran Updated 10 Nov 2026
7 min read ·Lesson 149 of 200

What a Failure Needs to Tell You Later

Retries and timeouts decide what happens during a failure. Logging decides what is known afterward. When a user reports that an answer never arrived, or a nightly job finishes with a hundred failures, the question is always the same: what happened, how often, and why? The answer exists only if the application recorded it at the time, in a form that can be searched.

A useful error log answers specific questions. Which operation failed? Which model was called? What status and error type came back? Was the failure retried, and how many times? Which request ID would a support team need? And, just as important, what was deliberately left out, such as the content of prompts and answers. This lesson builds a structured logging approach that records those facts consistently, and explains the privacy boundaries that apply to what is written down.


Why Structured Logs

A log line written as a sentence, such as "API call failed for summarize with 529", reads well to a person and works poorly for a machine. Finding every failure of one type, or counting failures by operation, requires parsing text, which breaks whenever the wording changes.

A structured log line records the same information as named fields. Each event becomes one JSON object, with a stable event name and consistent keys. A log platform can then filter by status, group by operation, or count will_retry values without any parsing. The rule is simple: the message says what kind of event occurred, and the fields say the details.


A JSON Formatter for the Standard Library

Python's logging module separates the logger, which creates records, from the handler and formatter, which write them. A small formatter turns each record into one JSON line. Extra fields are passed through the extra argument, which attaches them to the record:

"""api_logging.py: structured error logs built on the standard logging module."""

import json
import logging


class JsonFormatter(logging.Formatter):
    def format(self, record: logging.LogRecord) -> str:
        payload = {"event": record.getMessage(), "level": record.levelname.lower()}
        payload.update(getattr(record, "fields", {}))
        return json.dumps(payload, ensure_ascii=False, sort_keys=True)

The formatter is the only place that knows the output format. If the team later switches to another logging backend, the calling code does not change, because it still passes events and fields to the logger.

sort_keys=True keeps the field order stable, which makes the output easy to compare between runs and in tests. It does not change the meaning of the data.


Turning an Error into Fields

Each SDK error type yields a different set of useful fields. An APIStatusError has a status, a request ID, and a body with an error type. A connection error has only a kind, and a timeout is a specific connection error. The function below extracts whatever applies, and skips what does not, so no log line is filled with placeholders:

from anthropic import APIConnectionError, APIStatusError, APITimeoutError


def error_fields(error: Exception, operation: str, attempt: int) -> dict:
    fields = {"operation": operation, "attempt": attempt, "error_class": type(error).__name__}
    if isinstance(error, APIStatusError):
        body = error.body if isinstance(error.body, dict) else {}
        fields["status"] = error.status_code
        fields["error_type"] = body.get("error", {}).get("type")
        fields["request_id"] = error.request_id
    elif isinstance(error, APITimeoutError):
        fields["kind"] = "timeout"
    elif isinstance(error, APIConnectionError):
        fields["kind"] = "connection"
    return fields


def log_api_error(logger: logging.Logger, error: Exception, operation: str,
                  attempt: int, will_retry: bool) -> None:
    fields = error_fields(error, operation, attempt)
    fields["will_retry"] = will_retry
    level = logging.WARNING if will_retry else logging.ERROR
    logger.log(level, "api_call_failed", extra={"fields": fields})

How It Works

  1. error_fields() starts with what every error has: the operation name, the attempt number, and the class name.
  2. Status errors add the status, the error type from the body, and the request ID. The request ID is the field a support team needs most, because it locates the exact request on the service's side.
  3. Timeouts and connection errors add a kind, which separates them without pretending they have a status.
  4. log_api_error() chooses the level from the decision. A failure that will be retried is a warning, since the system is handling it. A failure that will not be retried is an error, since it needs attention. The decision and the level therefore always agree.
  5. The extra argument carries the fields to the formatter, where they become part of the JSON object.

Recording the Request Without Its Content

Errors are often easier to diagnose when the request's shape is known: which model, how large the limit, how many messages, how much text. Those facts are safe to log, and they help. The content of the prompts and answers is different. It may contain personal information, and logs are usually kept longer and read by more people than the data itself. The safe approach records a summary of the request instead of the request:

def request_summary(params: dict) -> dict:
    messages = params.get("messages", [])
    characters = sum(len(str(m.get("content", ""))) for m in messages)
    return {
        "model": params.get("model"),
        "max_tokens": params.get("max_tokens"),
        "message_count": len(messages),
        "input_characters": characters,
    }

The summary tells an investigator that a request was very large, or used an unexpected model, without revealing a word of what it said. Whether to log even that much depends on the data and the policy in force, and the summary can be reduced further if needed.


Practical Example: Logging a Failing Job

The example below sends three failures through the logging functions: a retried overload, a terminal bad request, and a timeout. A handler writes each event to standard output as one JSON line, and the request summary is shown for one of them. The output is deterministic, because the formatter writes no timestamps, and the fields are sorted.

"""api_logging_demo.py: structured error logs for three failures."""

import logging
import sys

from api_errors import fake_status_error
from api_logging import JsonFormatter, log_api_error, request_summary
from anthropic import APITimeoutError
import httpx

logger = logging.getLogger("gocourse.api")
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JsonFormatter())
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logger.propagate = False

REQUEST = httpx.Request("POST", "https://api.anthropic.com/v1/messages")

if __name__ == "__main__":
    log_api_error(logger, fake_status_error(529, "overloaded_error", "overloaded", "req_529"),
                  operation="summarize", attempt=1, will_retry=True)
    log_api_error(logger, fake_status_error(400, "invalid_request_error", "bad input", "req_400"),
                  operation="summarize", attempt=1, will_retry=False)
    log_api_error(logger, APITimeoutError(request=REQUEST),
                  operation="summarize", attempt=2, will_retry=True)

    summary = request_summary({"model": "claude-opus-5-5", "max_tokens": 512,
                               "messages": [{"role": "user", "content": "Summarize the report."}]})
    logger.info("request_sent", extra={"fields": summary})

Output:

{"attempt": 1, "error_class": "InternalServerError", "error_type": "overloaded_error", "event": "api_call_failed", "level": "warning", "operation": "summarize", "request_id": "req_529", "status": 529, "will_retry": true}
{"attempt": 1, "error_class": "BadRequestError", "error_type": "invalid_request_error", "event": "api_call_failed", "level": "error", "operation": "summarize", "request_id": "req_400", "status": 400, "will_retry": false}
{"attempt": 2, "error_class": "APITimeoutError", "event": "api_call_failed", "kind": "timeout", "level": "warning", "operation": "summarize", "will_retry": true}
{"event": "request_sent", "input_characters": 21, "level": "info", "max_tokens": 512, "message_count": 1, "model": "claude-opus-5-5"}

How It Works

  1. The logger is configured once, with a handler that uses the JSON formatter. The calling code only needs log_api_error(), which keeps the output format in one place.
  2. The overload is logged as a warning, because the request will be retried. The status, the error type, and the request ID are all present.
  3. The bad request is logged as an error, because retrying it would not help. The same fields appear, with will_retry set to false, so a dashboard can count terminal failures separately.
  4. The timeout has no status, so its line carries kind instead. The absence of a status is information, and the log says so by leaving the field out.
  5. The request summary records size and model. The one message, "Summarize the report.", is 21 characters, and the log records that count while leaving out the words themselves.

What Not to Log

Certain values should never reach a log, however convenient they seem. The API key is the most obvious, and it can appear in configuration dumps or exception text if code is careless. Prompt and answer content can carry personal data. Full tool results may contain records from other systems. A log that holds any of these becomes a second copy of sensitive data, with its own retention and access rules.

The rule is to log the decisions and the facts about the work, not the work itself. When the content is needed for debugging, it belongs in a separate, access-controlled store with a shorter retention period, and it should be captured deliberately, not by default.


Environment-Level Logging

The SDK itself can log its requests and responses. The ANTHROPIC_LOG environment variable enables this at levels such as info and debug (Lesson 97). Those logs are useful during development, but they are detailed, and in production they can record more than the policy allows. Application-level structured logs, as built here, are the normal production choice, and the SDK logs are switched on only when a specific investigation needs them.


Summary

Structured logs record what failed, in fields that can be searched and counted. The request ID connects a log line to the service's own record, the status and error type explain the failure, and the will_retry field shows what the system did about it. Request summaries describe the shape of the work without its content, and secrets and personal data stay out of the logs entirely. The next lesson follows the request ID further, from the application through every attempt.

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 Log API Errors and get answers drawn from it.

Signed-in readers only.