Logging
Call configure_logging() once to enable JSON logs for every rboto client.
No handler, formatter, or interceptor registration is required.
from rboto import S3Client, configure_logging
configure_logging()
client = S3Client()
await client.list_buckets()
Logs go to stdout. Each service has its own logger name: rboto.s3,
rboto.dynamodb, rboto.bedrock-runtime, and so on. The name is also available
as client.logger_name.
Every operation log includes a UTC timestamp, level, logger, service,
operation, message, and operation metadata. JSON is formatted automatically,
including when stdout is collected by a container or Lambda.
Set levels and disable services
Use one configuration for the whole application:
This disables S3 logs, enables DynamoDB retry diagnostics, and uses INFO for the other services. Settings apply to existing clients, future clients, Session-based clients, and compatibility factories.
Calling configure_logging() again replaces the default level and per-service
overrides. It resets rboto service loggers to that configuration and reuses its
handler; it does not add duplicate handlers. The application root logger and its
handlers are unchanged. rboto records do not propagate to the root handler.
To disable all operation logging:
To enable only S3:
For a change to one logger without reconfiguring the others, use get_logger().
It returns the standard Python logger with the service's name:
import asyncio
import logging
from rboto import S3Client, configure_logging, get_logger
client = S3Client()
configure_logging() # Also applies to clients created before this call.
async def main() -> None:
logger = get_logger("s3")
assert logger.name == client.logger_name == "rboto.s3"
await client.list_buckets()
logger.disabled = True
await client.list_buckets()
logger.disabled = False
logger.setLevel(logging.DEBUG)
await client.list_buckets()
if __name__ == "__main__":
asyncio.run(main())
get_logger("s3") and logging.getLogger("rboto.s3") return the same object.
Disabling that logger skips automatic logging and its native metadata collection.
Custom application middleware, if configured, still runs.
No operation logs are produced until configure_logging() is called. Without
logging or application middleware, operations use direct native dispatch.
What gets logged
| Level | Event | Meaning |
|---|---|---|
INFO |
operation.completed |
The operation returns successfully |
ERROR |
operation.completed |
The operation raises an exception |
WARNING |
operation.completed |
The operation is cancelled |
DEBUG |
operation.retry |
Native dispatch made more than one attempt |
There is one completion record per invocation when its level is enabled. A retry record summarizes up to 32 completed attempts after native dispatch finishes.
Logging automatically wraps application middleware, so cache hits, policy blocks,
and error recovery are included. A cache hit has dispatched=false and zero native
attempts. A policy failure is an error even if no request was sent. If middleware
recovers from a native error, the final outcome is successful while the native error
fields retain the underlying failure.
For streaming operations, completion means the response is available. Logging does not read the stream or measure later consumption; subsequent stream errors remain with the code consuming it.
JSON fields
| Field | Meaning |
|---|---|
timestamp, level, logger |
UTC time, log level, and service logger name |
message, event |
operation.completed or operation.retry |
schema_version |
1 |
service, operation |
Service identifier and modeled operation name |
operation_id |
Unique ID for this invocation |
correlation_id |
Application ID from operation_context() |
region |
Resolved native region when available |
duration_ms |
Total elapsed milliseconds, including application middleware |
outcome |
success, error, or cancelled |
dispatched |
Whether the native adapter was called |
native_duration_ms |
Native call duration, including initialization and retries |
attempt_count, retry_count |
Native attempts/retries; zero before dispatch, null when unknown |
status_code, request_id |
Last available native HTTP status and request ID |
error_code, error_type |
Final exception's modeled code and class name |
native_error_code, native_error_type |
Underlying native failure, including recovered failures |
trace_id, span_id |
Optional application-supplied trace IDs |
attempts |
Retry records only: number, duration in milliseconds, status and request ID |
attempts_truncated |
Retry records only: whether detailed attempts were omitted |
Unavailable values are null. Trace and retry detail fields are omitted when not applicable. Attempt duration excludes retry backoff. Cancellation can prevent the native collector from delivering attempt counts, so unknown counts remain null.
Automatic operation logs never inspect parameters, response properties, streams,
to_dict(), exception messages, or tracebacks. Correlation and trace IDs are supplied
by your application; use IDs appropriate for logs.
Concurrent calls and correlation
Each task keeps its own operation and correlation IDs:
import asyncio
from rboto import DynamoDBClient, S3Client, configure_logging, operation_context
# One configuration for every client: JSON to stdout, no formatter required.
configure_logging(level="INFO", service_levels={"dynamodb": "DEBUG", "s3": "OFF"})
async def main() -> None:
dynamo = DynamoDBClient()
storage = S3Client()
async def tables() -> None:
with operation_context(correlation_id="list-tables"):
await dynamo.list_tables()
async def buckets() -> None:
with operation_context(correlation_id="list-buckets"):
await storage.list_buckets()
async with asyncio.TaskGroup() as group:
group.create_task(tables())
group.create_task(buckets())
if __name__ == "__main__":
asyncio.run(main())
Trace IDs
An optional synchronous provider can return LogTraceContext or None. The
provider runs on the operation's task when the record's level is enabled. It does
not require a telemetry dependency:
import asyncio
from contextvars import ContextVar
from rboto import DynamoDBClient, LogTraceContext, configure_logging
trace_context: ContextVar[LogTraceContext | None] = ContextVar("application_trace", default=None)
configure_logging(trace_context=trace_context.get)
async def main() -> None:
client = DynamoDBClient()
# In an application, obtain these IDs from the tracer's current span.
token = trace_context.set(
LogTraceContext(
trace_id="0123456789abcdef0123456789abcdef",
span_id="0123456789abcdef",
)
)
try:
await client.list_tables()
finally:
trace_context.reset(token)
if __name__ == "__main__":
asyncio.run(main())
Use your tracer's active span IDs in the provider. No spans are created by logging.
AWS Lambda
Configure logging once, outside the handler. Attach the invocation ID with
operation_context() and keep the client available for reuse:
import asyncio
from typing import Protocol
from rboto import DynamoDBClient, configure_logging, operation_context
configure_logging()
client = DynamoDBClient()
class LambdaContext(Protocol):
@property
def aws_request_id(self) -> str: ...
async def list_tables() -> dict[str, object]:
response = await client.list_tables()
return {"tables": response.table_names}
def lambda_handler(event: dict[str, object], context: LambdaContext) -> dict[str, object]:
with operation_context(correlation_id=context.aws_request_id):
return asyncio.run(list_tables())
The output is already JSON; no runtime formatter is needed. If you enable Lambda log-level filtering, choose a level that retains the rboto records you want. Context is restored on exit, including failures, so reused environments do not retain a previous invocation's correlation ID. See AWS Lambda for packaging and client reuse.
Output destination
Stdout is the default. Pass a text stream when the application needs a different destination, such as a file it manages:
with open("operations.jsonl", "a", encoding="utf-8") as output:
configure_logging(stream=output)
await client.list_buckets()
configure_logging(level="OFF")
rboto does not close application-owned streams. Writes are synchronous; a slow destination can delay the caller. Ordinary logging failures do not replace the operation result or its original exception. Delivery is not retried, so use these records for diagnostics rather than guaranteed audit delivery.