Agent Logging
AgentVisor™ provides structured logging for agent code, with logs forwarded securely from the guest sandbox to the host.
Why Not print()?
Inside the AgentVisor sandbox, stdout is reserved for result data. The guest runtime parses the last line of stdout as JSON to extract the agent's return value. Any print() output is captured internally at debug level and never shown in normal operation.
Use Python's standard logging module for diagnostic output instead. Captured log records are sent directly to the host over gRPC — they never touch the process's stdout or stderr streams. (stderr still exists as a fallback channel, used only for output that never reaches a logging handler at all — see How It Works.)
Quick Start
Replace print() calls with standard Python logging:
import logging
logger = logging.getLogger(__name__)
# Instead of: print("Processing query...")
logger.info("Processing query")
# Include structured context
logger.info("Calling LLM", extra={"model": "claude-sonnet-4-5-20250929", "tokens": 1500})
# Log errors with tracebacks
try:
result = call_api()
except Exception:
logger.exception("API call failed")
That's it. No additional configuration is needed — logs are captured automatically and appear on the host's configured logging output by default, with the correct level, logger name, and structured fields preserved. This also means third-party libraries (LangChain, httpx, etc.) get forwarded logs for free, with no code changes on your part.
How It Works
There are two independent paths, and it's important not to conflate them:
1. Structured path (the one you want) — direct gRPC, no stderr involved:
Agent (Python) Guest Runtime Host Runtime
─────────────────── ───────────────────── ──────────────────
logger.info("msg")
→ gRPC (ForwardLog) ───→ relays via gRPC ─────────→ log handler
[AgentService] [HostService] → configured output (default: stderr)
During startup, the AgentVisor Python SDK attaches a GrpcLogHandler to the Python root logger (and to the agentvisor.agent hierarchy used by agentvisor.get_logger()). Any log record handled by that handler — including a plain logging.getLogger(__name__) logger, once root-logger capture attaches to it — is sent as a structured gRPC call straight from the Python process to the guest runtime (AgentService.ForwardLog), which immediately relays it to the host (HostService.ForwardLog) for rendering. The process's actual stdout/stderr file descriptors are never touched by this path.
2. Raw stderr fallback (for output that bypasses logging entirely):
Agent (Python) Guest Runtime Host Runtime
───────────────── ───────────────────── ──────────────────
print("msg")
→ stderr ──────────→ reads line
→ forwards to host ─────→ log handler
(as "agent.stderr", → configured output (default: stderr)
level=DEBUG)
Anything that never reaches a Python logging handler — stray print() calls, output from a spawned subprocess, or (if root-logger capture is disabled) a logger outside the agentvisor.agent hierarchy — still ends up on the process's real stderr stream. The guest runtime reads that stream line-by-line and forwards each line generically to the host, but with the real level, logger name, and structured fields lost: everything arrives labeled as logger agent.stderr at DEBUG level. This is a last-resort safety net, not something to rely on for real diagnostic output.
Log Levels
The logger respects standard Python log levels. Control the agent log level using the agent component override in the unified log level grammar:
# Set agent logs to DEBUG while keeping everything else at INFO (default)
export AGENTVISOR_LOG_LEVEL=info,agent=debug
Or via the config file:
# agentvisor.yaml
log_level: "info,agent=debug"
The default level is INFO.
Root-Logger Capture
Any Python logging in your agent — logging.getLogger(__name__), bare logging.info(...), or third-party libraries like LangChain, httpx, or urllib3 — is captured automatically, with no code changes required. This works because setup_logging() (called automatically by the framework runners) attaches a gRPC log handler to the Python root logger, so every logger in the process tree flows through the same forwarding path with the correct level, logger name, and structured fields preserved.
Without this, unrecognized loggers would fall through to stderr, where the guest runtime captures them generically at DEBUG level under the synthetic logger name agent.stderr — losing the real level, logger name, and any structured fields.
The agent Umbrella and Per-Logger Overrides
Root-captured logging is governed by the same agent component described under Log Levels:
# Umbrella level for all root-captured Python logging
export AGENTVISOR_LOG_LEVEL=warn,agent=info
To tune an individual Python logger — your own module or a third-party library — use an agent.<name> key, where <name> matches the logger's name:
# Raise a chatty library to DEBUG while keeping the agent umbrella at INFO
export AGENTVISOR_LOG_LEVEL=warn,agent=info,agent.httpx=debug
# Silence one of your own noisy modules
export AGENTVISOR_LOG_LEVEL=info,agent.my_module=warn
Library and application logger names are always scoped under agent. — never as bare top-level grammar keys. Top-level keys (host, guest, temporal, agent, mcp, a2a, ...) are reserved for real AgentVisor subsystems; scoping Python logger names under agent. avoids colliding with them (e.g. a library coincidentally named temporal would otherwise be indistinguishable from the AgentVisor Temporal component).
Unlike agent.<name>, the other top-level components have a closed set of recognized sub-components: host.accesslog, mcp.client, mcp.pool, a2a.client, a2a.pool. Any override key that doesn't resolve to a known component (exactly or as a dot-boundary descendant of one) — e.g. a typo like mcp=warn,mcp.pol=debug — still parses successfully but produces a startup warning naming the unrecognized key, since the override has no effect. mcp=warn,mcp.pool=debug is valid: it quiets the mcp umbrella while keeping the connection pool at debug.
Built-in Denoise List
Known-chatty third-party libraries default to WARNING so their DEBUG/INFO output doesn't flood the log stream: httpx, httpcore, urllib3, requests, openai, anthropic, langchain, langchain_core, langgraph, langsmith, botocore, boto3, s3transfer, asyncio, grpc, filelock, huggingface_hub. An agent.<name> override for any of these takes precedence over its default (as in the agent.httpx=debug example above).
Opting Out
To disable root-logger capture — for example if you want every non-agentvisor.get_logger() logger to fall through to stderr as agent.stderr at DEBUG instead — set:
export AGENTVISOR_LOG_CAPTURE_ROOT=false
The agentvisor.get_logger() API (see Advanced: agentvisor.get_logger()) and stderr fallback capture are unaffected either way.
Output Formats
Plain (default)
Human-readable format with timestamp, level, source, logger name, and context:
2026-02-07T12:34:56.123Z INFO [agent.qa-agent] Processing query {"logger": "my_agent", "query_length": "42"}
JSON
Structured JSON, one object per line — suitable for log aggregation systems:
{"time":"2026-02-07T12:34:56.123Z","level":"INFO","component":"agent.qa-agent","logger":"my_agent","msg":"Processing query","query_length":"42"}
Set the format via host configuration:
logging:
format: json # plain or json
Or via environment variable:
export AGENTVISOR_LOG_FORMAT=json
Log File Output
By default, agent logs are written to the host's stderr (the default logging.output),
alongside all other host log output. To write to a file (or stdout) instead, set the
host's logging.output — agent logs follow the same destination as the rest of the
host's logging:
logging:
output: /var/log/agentvisor/agentvisor.log
Or via environment variable:
export AGENTVISOR_LOG_OUTPUT=/var/log/agentvisor/agentvisor.log
Contextual Fields
Every log line automatically includes context from the execution environment:
| Field | Source | Description |
|---|---|---|
agent_name | AGENTVISOR_AGENT_NAME | Name of the agent being executed |
thread_id | AGENTVISOR_THREAD_ID | Thread ID for the current conversation |
run_id | AGENTVISOR_RUN_ID | Unique run identifier |
These are injected automatically — no code changes needed.
Adding Custom Fields
Pass extra fields via the extra parameter:
logger.info("Tool call completed", extra={
"tool": "web_search",
"duration_ms": 342,
"results": 5,
})
In plain format:
2026-02-07T12:34:56.123Z INFO [agent.tools] Tool call completed (agent_name=qa-agent, thread_id=abc123, tool=web_search, duration_ms=342, results=5)
In JSON format, extra fields are added as top-level keys.
Migrating from print()
For existing agents that use print() for diagnostic output:
# Before
# (nothing — no logger set up)
# After - add at the top of your module
import logging
logger = logging.getLogger(__name__)
Then replace print calls:
# Before # After
print("Starting agent") logger.info("Starting agent")
print(f"Error: {e}") logger.error("Failed: %s", e)
print(f"Debug: {state}") logger.debug("State: %s", state)
print(f"Warning: {msg}") logger.warning("%s", msg)
Advanced: agentvisor.get_logger()
For most agents, idiomatic logging.getLogger(__name__) is all you need — it's captured automatically as described above. The AgentVisor Python SDK also provides agentvisor.get_logger(), a thin convenience wrapper:
import agentvisor
logger = agentvisor.get_logger(__name__)
logger.info("Processing query")
This returns a standard logging.Logger under the agentvisor.agent.<name> hierarchy (with propagate=False), attached directly to the gRPC forwarding handler rather than relying on root-logger capture. Level control works exactly the same way as idiomatic logging — the agent umbrella and agent.<name> overrides both apply to it.
The one situation where it matters: if you've set AGENTVISOR_LOG_CAPTURE_ROOT=false to disable root-logger capture (see Opting Out) but still want a specific logger's output forwarded to the host rather than falling through to stderr, use agentvisor.get_logger() for that logger. Otherwise, prefer idiomatic logging.getLogger(__name__) — it keeps your agent code framework-agnostic and free of AgentVisor-specific imports.
Configuration Reference
| Variable | Default | Description |
|---|---|---|
AGENTVISOR_LOG_LEVEL | info | Log level grammar: <global>[,<component>=<level>]* — use agent component to target agent logs (e.g., info,agent=debug), and agent.<name> to override an individual Python logger (e.g., agent.httpx=debug) |
AGENTVISOR_LOG_CAPTURE_ROOT | true | Capture idiomatic/third-party Python logging via the root logger; set to false to disable (only agentvisor.get_logger()-based loggers are captured) |
AGENTVISOR_LOG_OUTPUT | stderr | Host-side log destination (applies to all host log output, including forwarded agent logs): stderr, stdout, discard, /path/to/file, rotate:/path |
AGENTVISOR_LOG_FORMAT | plain | Host-side output format: plain or json |