Python SDK Observability
Record plan traces, policy decisions, tool activity, and model usage with armoriq-sdk 0.6.4.
The Python SDK records one trace for each IAP plan and sends completed traces to ArmorIQ without making telemetry part of the policy decision. Use the session API to get policy, tool, and model activity under one stable session ID.
Install the production package
You need an organization-scoped ArmorIQ API key before the SDK can ship telemetry.
python -m pip install "armoriq-sdk==0.6.4"The import package is armoriq_sdk. Customer applications should install the production package from PyPI, not a development package or a local checkout.
Start with the defaults
Observability is enabled by default when you start an ArmorIQSession. The recorder inherits the client's API key, HTTP client, and backend base URL.
from armoriq_sdk import ArmorIQClient, SessionOptions
client = ArmorIQClient(
api_key="YOUR_ARMORIQ_API_KEY",
agent_id="support-agent",
)
session = client.start_session(SessionOptions(mode="sdk"))The session creates one stable UUID for all of its traces. It uses a valid SessionOptions.session_id, then a valid context_id, or generates a UUID when neither value is valid. Set SessionOptions.session_id when you need to correlate the ArmorIQ session with an application-level UUID.
Telemetry is fail-open: an observability exception does not change an allow, deny, or hold result. flush_observability(), close(), and context-manager exit perform synchronous shipping, however, so network timeouts and retries can add latency at those boundaries.
Configure observability
SessionOptions.observability accepts ObservabilityConfig from armoriq_sdk.observability.
from armoriq_sdk import SessionOptions
from armoriq_sdk.observability import ObservabilityConfig
options = SessionOptions(
mode="sdk",
session_id="6f7bbf5e-3f5f-46b8-86aa-f3b9107df6bb",
observability=ObservabilityConfig(
enabled=True,
endpoint="https://api.armoriq.ai",
product="armoriq-sdk",
max_buffer_size=1000,
flush_interval_ms=5000,
batch_size=100,
),
)endpoint is the ArmorIQ backend base URL. Do not add /observability/spans; the shipper appends that path.
SessionOptions fields
| Field | Default | Observability effect |
|---|---|---|
tool_name_parser | None | Controls how tool names are split into MCP and action names |
default_mcp_name | None | Used for tool names without an <MCP>__<action> prefix |
validity_seconds | 3600 | Intent-token lifetime; does not configure telemetry retention |
llm | agent | LLM label used during plan capture |
mode | local | Selects local, proxy, or SDK enforcement and the corresponding spans |
observability | None | Uses default-on observability unless explicitly disabled |
session_id | None | Preferred stable observability session UUID |
context_id | None | UUID fallback when session_id is absent or invalid |
ObservabilityConfig fields
| Field | Dataclass default | Session behavior |
|---|---|---|
enabled | True | Creates the recorder and starts shipping; False disables all observability for the session |
endpoint | empty string | Falls back to ARMORIQ_OBSERVABILITY_ENDPOINT, then the client's backend_endpoint |
api_key | empty string | Falls back to the client's API key |
product | armoriq-sdk | Producer value sent with ingest requests |
session_id | None | Default for direct recorder use; session plan traces use the stable SessionOptions.session_id instead |
user_id | None | Falls back to the client identity; sent only when it is a UUID |
agent_id | None | Falls back to the client agent ID; free-form strings are supported |
http_client | None | Falls back to the client's httpx.Client |
max_buffer_size | 1000 | Maximum in-flight traces kept by the recorder; the oldest is dropped on overflow |
flush_interval_ms | 5000 | Background flush interval in milliseconds |
batch_size | 100 | Maximum ended trace batches in one ingest request |
on_span | None | Callback supported by a directly constructed recorder; the current session configuration merge does not forward it |
The Python configuration does not expose a durable-disk option. Session traces are buffered in memory; product integrations that need cross-process durability implement it separately.
Environment variables
| Variable | Accepted value | Effect |
|---|---|---|
ARMORIQ_OBSERVABILITY | false, 0, no, or off | Disables default observability when no explicit enabled value is supplied |
ARMORIQ_OBSERVABILITY_ENDPOINT | Backend base URL | Overrides the client's backend URL when the config does not set an endpoint |
ARMORIQ_OBSERVABILITY_PRODUCT | Product string | Overrides armoriq-sdk when no config block supplies a product |
ARMORIQ_API_KEY | ArmorIQ API key | Normal client credential fallback |
BACKEND_ENDPOINT | Backend base URL | Normal client backend fallback |
An explicit config value wins over its environment fallback. In particular, ObservabilityConfig() contains enabled=True and product="armoriq-sdk", so those values override the corresponding environment variables. Omit the config block when you want the environment to control the default behavior.
To disable one session regardless of the environment:
from armoriq_sdk import SessionOptions
from armoriq_sdk.observability import ObservabilityConfig
session = client.start_session(
SessionOptions(observability=ObservabilityConfig(enabled=False))
)Trace and span lifecycle
The session implements the current Model A lifecycle:
start_plan()ends the previous plan trace, if one exists, then starts aniap.plantrace.check()opens aniap.checkspan and records the selected local, SDK, or proxy enforcement path beneath it.- Policy results become
policy_callspans.report()anddispatch()create tool spans. record_generation()adds agenerationspan to the active plan trace.- The next
start_plan(),flush_observability(),close(),dispose(), or context-manager exit ends the active trace.
If an instrumented operation runs before start_plan(), the session lazily opens an iap.plan trace. A single session can therefore contain multiple plan traces without splitting their correlation ID.
Record policy and tool activity
Session methods instrument their own operations. You do not need to construct spans for the normal plan-check-execute-report loop.
from armoriq_sdk import ReportOptions
tool_name = "crm-mcp__search_contacts"
tool_input = {"query": "Acme"}
session.start_plan(
[{"name": tool_name, "args": tool_input}],
goal="Find contacts at Acme",
)
decision = session.check(tool_name, tool_input)
if decision.allowed:
result = {"matches": [{"id": "contact-1", "company": "Acme"}]}
session.report(
tool_name,
tool_input,
result,
ReportOptions(status="success", duration_ms=18),
)Tool inputs and outputs are truncated to a bounded preview before being attached to session spans, but arbitrary secrets are not automatically redacted. Avoid recording credentials or sensitive customer content.
Record model generations
Call record_generation() with usage from your model provider. Prompt and completion text are optional.
session.record_generation(
model="gpt-4o",
input_tokens=420,
output_tokens=96,
cache_read_tokens=0,
cache_write_tokens=0,
prompt=None,
completion=None,
finish_reason="stop",
)The SDK estimates cost from its built-in per-model table. Unknown model names currently produce costUsd: 0.0; treat that value as not estimated, not evidence that the call was free. Token counts still remain available. See the data model for generation fields and aggregates.
Buffering, retries, and failure behavior
The recorder protects its in-memory trace buffer with a lock. Ended traces move to a separate locked shipping queue, and a daemon thread wakes every five seconds by default to flush them in batches.
- Successful
2xxresponses return backend accepted and rejected counts. - Invalid local batches are dropped before the request.
4xxresponses are logged and dropped without retry.- Network failures and
5xxresponses use three retries after the first attempt, with 500 ms, 1 second, and 2 second backoff. - Exhausted batches are logged and dropped. They are not requeued or written to disk.
Both the recorder and shipper register atexit cleanup as a last safety net. Application code should still close sessions explicitly because interpreter shutdown ordering and forced process termination cannot guarantee delivery.
Flush and shut down
flush_observability() ends the active plan trace and synchronously flushes queued traces, but leaves the background thread running. close() ends the trace, stops the thread, and performs a final flush. dispose() is an alias for close().
The session is a context manager, which is the safest default:
with client.start_session(options) as session:
# Run one or more plans.
...If the block raises, context-manager exit ends the active trace with error. Otherwise it ends with ok. Close the session before closing the client so the shared HTTP client is still available for the final flush.
Complete example
import os
import time
import uuid
from armoriq_sdk import ArmorIQClient, ReportOptions, SessionOptions
from armoriq_sdk.observability import ObservabilityConfig
api_key = os.environ.get("ARMORIQ_API_KEY")
if not api_key:
raise RuntimeError("Set ARMORIQ_API_KEY")
client = ArmorIQClient(
api_key=api_key,
agent_id="crm-assistant",
)
options = SessionOptions(
mode="sdk",
default_mcp_name="crm-mcp",
llm="gpt-4o",
session_id=str(uuid.uuid4()),
observability=ObservabilityConfig(
enabled=True,
endpoint=os.environ.get(
"ARMORIQ_OBSERVABILITY_ENDPOINT",
"https://api.armoriq.ai",
),
product="armoriq-sdk",
max_buffer_size=1000,
flush_interval_ms=5000,
batch_size=100,
),
)
tool_name = "search_contacts"
tool_input = {"query": "Acme"}
try:
with client.start_session(options) as session:
session.start_plan(
[{"name": tool_name, "args": tool_input}],
goal="Find contacts at Acme",
)
decision = session.check(tool_name, tool_input)
if decision.allowed:
started = time.perf_counter()
result = {
"matches": [{"id": "contact-1", "company": "Acme"}],
}
session.report(
tool_name,
tool_input,
result,
ReportOptions(
status="success",
duration_ms=(time.perf_counter() - started) * 1000,
),
)
session.record_generation(
model="gpt-4o",
input_tokens=420,
output_tokens=96,
cache_read_tokens=0,
cache_write_tokens=0,
prompt=None,
completion=None,
finish_reason="stop",
)
# Optional request boundary. Context-manager exit also closes and flushes.
session.flush_observability()
finally:
client.close()Next: use the observability dashboard or troubleshoot delivery. For lower-level record shapes, see the data model.