Structured Logging Around SDK Calls
A log line that says "S3 call succeeded" is nearly useless at 2am. A log line with the operation name, request id, duration, and outcome as separate fields is something you can filter, graph, and alert on.
Busque em todas as páginas da documentação
A log line that says "S3 call succeeded" is nearly useless at 2am. A log line with the operation name, request id, duration, and outcome as separate fields is something you can filter, graph, and alert on.
Structured logging means emitting logs as key-value data - most commonly JSON - instead of a free-text sentence. Around AWS SDK calls specifically, structured logging turns every call into a small, consistent, queryable record.
This page builds that pattern for both boto3 and AWS SDK v3, and is deliberately narrow about what belongs in the log: identifiers and outcomes, never the request or response bodies themselves.
The pattern: one structured log line per SDK call, with a fixed set of fields, emitted from a shared wrapper so every call site is consistent.
# --- Python (boto3) ---
import json
import logging
import time
logger = logging.getLogger("app")
def log_call(operation: str, duration_ms: float, request_id: str, outcome: str, **extra):
logger.info(json.dumps({
"operation": operation,
"duration_ms": round(duration_ms, 1),
"request_id": request_id,
"outcome": outcome,
**extra,
}))// --- TypeScript (AWS SDK v3) ---
function logCall(
operation: string,
durationMs: number,
requestId: string | undefined,
outcome: "ok" | "error",
extra: Record<string, unknown> = {},
) {
console.log(JSON.stringify({
operation,
durationMs: Math.round(durationMs * 10) / 10,
requestId,
outcome,
...extra,
}));
}When to reach for this:
Wire the wrapper around a real call, capturing timing and the AWS-assigned request id in both success and failure paths.
# --- Python (boto3) ---
import json
import logging
import time
import boto3
from botocore.exceptions import ClientError
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("orders-service")
ddb = boto3.client("dynamodb")
def get_order(order_id: str):
start = time.monotonic()
try:
response = ddb.get_item(TableName="Orders", Key={"id": {"S": order_id}})
duration_ms = (time.monotonic() - start) * 1000
logger.info(json.dumps({
"operation": "dynamodb.GetItem",
"duration_ms": round(duration_ms, 1),
"request_id": response["ResponseMetadata"]["RequestId"],
"outcome": "ok",
"found": "Item" in response,
}))
return response.get("Item")
except ClientError as err:
duration_ms = (time.monotonic() - start) * 1000
logger.warning(json.dumps({
"operation": "dynamodb.GetItem",
"duration_ms": round(duration_ms, 1),
"request_id": err.response["ResponseMetadata"]["RequestId"],
"outcome": "error",
"error_code": err.response["Error"]["Code"],
}))
raise// --- TypeScript (AWS SDK v3) ---
import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";
const ddb = new DynamoDBClient({});
async function getOrder(orderId: string) {
const start = Date.now();
try {
const response = await ddb.send(
new GetItemCommand({ TableName: "Orders", Key: { id: { S: orderId } } }),
);
console.log(JSON.stringify({
operation: "dynamodb.GetItem",
durationMs: Date.now() - start,
requestId: response.$metadata.requestId,
outcome: "ok",
found: response.Item !== undefined,
}));
return response.Item;
} catch (err) {
const e = err as { name: string; $metadata?: { requestId?: string } };
console.warn(JSON.stringify({
operation: "dynamodb.GetItem",
durationMs: Date.now() - start,
requestId: e.$metadata?.requestId,
outcome: "error",
errorCode: e.name,
}));
throw err;
}
}What this demonstrates:
operation, duration_ms, request_id, outcome) appears whether the call succeeds or fails.found and similar boolean/enum fields describe the shape of the result without logging the item itself.response["Item"] or response.Item, since order records may carry customer data.logging module can emit JSON with json.dumps(...) as shown, or via a formatter library like python-json-logger for less boilerplate at scale.pino and winston are common choices, or JSON.stringify directly for a minimal footprint as shown here.info for success, warning/error for failures) so filtering by severity works without parsing the JSON body.| Field | Purpose |
|---|---|
operation | The AWS API operation name (for example dynamodb.GetItem) |
request_id / requestId | AWS's identifier for this specific call |
duration_ms / durationMs | Elapsed time for the call, in milliseconds |
outcome | "ok" or "error", for quick filtering |
error_code / errorCode | Present only on failure, the AWS error code |
Adding your own correlation id (from an incoming request) alongside these fields lets you group every SDK call made while serving one user request.
found above). Log a summary, not the record.Authorization header and any resolved credentials should never reach a log line. See the security-best-practices section's guidance on secrets handling for the broader pattern.info level; reserve those for error level and consider redacting inner exception args that may echo input parameters.Both examples above repeat the same shape at each call site. In practice, wrap this once (as shown in Observability Basics) and reuse it for every client, so a new call site gets structured logging for free rather than by convention alone.
logger.info(response) or console.log(response) can dump the entire item or object body. Fix: log named, derived fields only.latency_ms, another duration_ms. Fix: agree on one field set and enforce it in a shared logging helper.f"call took {duration}ms" is not filterable. Fix: always emit structured key-value pairs, JSON at minimum.request_id / requestId from the error response, not just from the success path.error level (or every real error at debug) breaks alerting. Fix: map outcome to log level deliberately.| Alternative | Use When | Don't Use When |
|---|---|---|
| Structured JSON logs (this page) | You need queryable, per-call records with minimal setup | You already need cross-service timing (use tracing instead or in addition) |
| Distributed tracing (X-Ray/OTel) | You need to see where time went across services | A single-service log line already answers your question |
| CloudWatch Application Signals | You want an aggregated service map and SLOs | You have not yet emitted any trace data for it to build from |
| Free-text logs | Quick local debugging only | Any log that needs to be searched, filtered, or alerted on in production |
Operation name, request id, duration, and outcome. Add a correlation id if you need to group calls by the user request that triggered them.
No. Log derived, named fields (like whether an item was found) instead of the object itself, since responses can carry customer data.
The standard logging module with json.dumps(...) works with no dependencies. python-json-logger reduces boilerplate if you are logging from many modules.
JSON.stringify on a plain object passed to console.log, matching the shape shown here. Add pino or winston later if you need log levels, transports, or redaction built in.
It is the identifier AWS support and CloudTrail use to trace a specific call. Without it, an intermittent failure is very hard to correlate to a specific AWS-side event.
No. Logging tells you what happened on a single call; tracing shows you where time went across an entire multi-service request. Use both.
Keep fields minimal and consistent, log at the right level, and consider sampling very high-volume, low-value operations rather than logging every single call at full detail.
Stack versions: This page was written for boto3 1.43.x (Python 3.10+) and the AWS SDK for JavaScript v3 (Node.js 18+).
Revisado por Chris St. John·Última atualização: 23 de jul. de 2026