Skip to main content
connic
Platform

Logs & Debugging

Custom code output, run context, traces, and Run Again support failure analysis. Run metadata can also be sent to external services.

Last updated

Logging from Custom Code

Anything printed or logged inside a tool, middleware, hook, or guardrail is captured per run. Logs appear in the project Logs tab, with filters across runs, and in the detail view for the individual run. No connic-specific logger is required: stdout, stderr, and Python's standard logging module are captured automatically. A log drain forwards these lines to Datadog, an OpenTelemetry backend, or a webhook.

What Gets Captured

CodeShown 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 used.

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:

SourceProduced By
ToolA custom tool, tagged with its tool name.
MiddlewareBefore, after, and cleanup middleware, tagged as before, after, or cleanup.
HookA tool hook, tagged with the tool it wraps.
GuardrailA custom guardrail, tagged with its guardrail name.

Example

tools/lookup_customer.py
"""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 selected level
    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 record

This 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.

Capture limits and behavior
  • 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.
  • StopProcessing and AbortTool are intentional control flow and are not logged as errors.
  • Use logging.exception(...) inside a caught exception to retain 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.

Each environment can replace selected JSON field values with [REDACTED] in new logs and traces. Configure sensitive data redaction to choose those fields, and log retention to control when runs are automatically deleted.

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.

Searchable run context

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.

middleware/invoice-processor.py
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 content
  1. Isolate the Failing Pattern

    Start in Agent Runs and narrow to the agent. Filter outright failures by status. For incorrect successful responses, use context expressions to isolate an input shape, customer segment, deployment, or workflow.

  2. Compare Like-for-Like Runs

    Open one incorrect run and one healthy run side by side. Choose the same agent and deployment with similar inputs and context, then compare their traces. Focus on the first span where they diverge.

  3. Inspect the Decision Path

    LLM spans show the model request and response, tool choices, arguments, and Thoughts when the provider returns reasoning content. Tool, middleware, hook, and guardrail spans show their inputs, outputs, status, duration, and error details.

  4. Run Again

    Use Run Again on a run to start another run with the same input and inspect its trace.

Accessing Run Data in Code

The middleware after() hook receives system fields after the agent finishes. Use them to send run metadata to an external monitoring or analytics service. A failed run does not reach after(); a run drain forwards every finished run, including failures.

Context KeyDescription
run_idUnique identifier for the run.
agent_nameName of the agent that executed.
duration_msActive execution time excluding approval and sequential child-run waits.
token_usageInput, output, thinking, cached input, and total token counts for the run.
middleware/invoice-processor.py
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 response
Values set in before() are also available in after(). Include identifiers such as request_type or customer_id in outbound monitoring events. See Context for the full reference.