Logs & Debugging
Capture output from custom code and use run context, traces, and Run Again to inspect failures.
On this page
Logging from Custom Code
Anything you print or log inside a tool, middleware, hook, or guardrail is captured per run. Logs appear in the project Logs tab, where you can filter across runs, and in the detail view for the individual run. You do not need a Connic-specific logger: stdout, stderr, and Python's standard logging module are captured automatically.
What Gets Captured
| You Write | Shown As |
|---|---|
| print("hello") | A log line at info level. |
| print("boom", file=sys.stderr) | A log line at error level. |
| logging.getLogger("tools.x").info(...) | A line at the exact debug, info, warning, or error level you called. |
Standard-library logger names must begin with tools., middleware., hooks., or guardrails.. The usual logging.getLogger(__name__) pattern already creates those names because Connic imports files under the matching package roots.
Source Labels
Every line is tagged with the code path that emitted it:
| Source | Produced By |
|---|---|
| Tool | A custom tool, tagged with its tool name. |
| Middleware | Before and after middleware, tagged as before or after. |
| Hook | A tool hook, tagged with the tool it wraps. |
| Guardrail | A custom guardrail, tagged with its guardrail name. |
Example
"""A custom tool that emits logs Connic will surface in the dashboard."""
import logging
import sys
from typing import Any, Dict
log = logging.getLogger(__name__) # -> "tools.lookup_customer"
async def lookup_customer(args: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
customer_id = args["customer_id"]
# Plain print -> captured as an info-level log line
print(f"Looking up customer {customer_id}")
# stdlib logging -> captured at the level you called
log.warning("cache miss for %s", customer_id)
try:
record = await fetch_from_crm(customer_id)
except Exception:
# Writes to stderr -> captured as an error-level log line
print(f"CRM lookup failed for {customer_id}", file=sys.stderr)
raise
return recordThis produces an info line from print, a warning from the standard logger, and an error from stderr if the CRM call fails. The Logs table shows source tool with detail lookup_customer, and retains the lines' emission order.
- Connic retains up to 500 log lines per run; additional lines are dropped.
- Unhandled exceptions from tools, middleware, hooks, and guardrails are logged with their traceback before normal error handling continues.
StopProcessingandAbortToolare intentional control flow and are not logged as errors.- Use
logging.exception(...)inside a caught exception when you still want the traceback. - Lines flush on newlines. Output without a trailing newline may remain buffered until the code returns.
- For structured logs, serialize the value with
json.dumps(...)and log the resulting string.
Debugging an Agent
Filter Agent Runs to a comparable set, then open individual traces to inspect inputs, outputs, context, model requests, tool calls, and errors.
Tag runs in middleware with stable keys such as customer_id, request_type, plan, locale, or region. Agent Runs supports expressions such as context.request_type == 'invoice_validation'; Logs search matches messages, sources, and agent names.
from typing import Any, Dict
async def before(content: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
payload = context.get("payload", {})
context["request_type"] = "invoice_validation"
context["customer_tier"] = payload.get("customer_tier", "unknown")
context["region"] = payload.get("region", "unknown")
context["debug_bucket"] = "high-risk"
return contentIsolate the Failing Pattern
Compare Like-for-Like Runs
Inspect the Decision Path
Run Again
Accessing Run Data in Code
The middleware after() hook receives system fields after the agent finishes. Use them to send run metadata to your own monitoring or analytics service.
| Context Key | Description |
|---|---|
| run_id | Unique identifier for the run. |
| agent_name | Name of the agent that executed. |
| duration_ms | Execution time excluding approval wait time. |
| token_usage | Input, output, thinking, cached input, and total token counts for the run. |
import httpx
from typing import Any, Dict
async def after(response: str, context: Dict[str, Any]) -> str:
"""Send run metadata to an external monitoring system."""
async with httpx.AsyncClient() as client:
result = await client.post(
"https://monitoring.example.com/events",
json={
"run_id": context.get("run_id"),
"agent": context.get("agent_name"),
"duration_ms": context.get("duration_ms"),
"tokens": context.get("token_usage", {}),
"request_type": context.get("request_type"),
}
)
result.raise_for_status()
return responsebefore() are also available in after(). Include identifiers such as request_type or customer_id in outbound monitoring events. See Context for the full reference.