Browse documentation

Instrument

Python

Instrument Python services with metrics, telemetry, errors, and request-safe customer identity.

The Python SDK supports plain Python plus Django, Flask, FastAPI, Celery, grpc.aio, standard-library logging, and opt-in HTTP client and LLM integrations.

Install and start

After the registry release:

pip install anectico
# Or include an integration extra:
# anectico[django], anectico[flask], anectico[fastapi], anectico[celery], anectico[grpc]

During early access, use the wheel supplied during onboarding or install the source package:

pip install "anectico @ git+https://github.com/anectico/anectico.git@main#subdirectory=sdks/python"
import os
import anectico

client = anectico.AnecticoClient(
    api_key=os.environ['ANECTICO_API_KEY'],
    service_name='orders-api',
)
client.start()

try:
    process_order()
except Exception as exc:
    client.capture_error(exc, tags={'component': 'checkout'})
    raise

Keep one client for the process and call client.stop() during graceful shutdown. Inspect its ShutdownResult when delivery matters: success is True only if every enabled provider flushed and shut down, False on a provider failure or aggregate deadline, and None when the client was already stopped. A context manager can start and stop the client for scripts that do not need to inspect that result.

Use a project-scoped key with ingest:write. Add analytics:write when calling identify, group, or diagnostic-event methods, as this guide does. Do not give an application key read or management scopes.

Keep an existing OpenTelemetry setup

Use existing-provider mode when the process already owns OpenTelemetry resources, sampling, batching, exporters or a Collector, instrumentation, and provider lifecycle. Add Anectico’s passive identity processors while building those providers, register the providers, and only then start Anectico:

from opentelemetry import _logs as otel_logs
from opentelemetry import metrics, trace
import anectico

# Keep your existing Resource, sampler, readers, exporters, and instrumentation.
tracer_provider.add_span_processor(anectico.BaggageIdentitySpanProcessor())
tracer_provider.add_span_processor(existing_span_export_processor)
logger_provider.add_log_record_processor(
    anectico.BaggageIdentityLogRecordProcessor()
)
logger_provider.add_log_record_processor(existing_log_export_processor)

trace.set_tracer_provider(tracer_provider)
metrics.set_meter_provider(meter_provider)
otel_logs.set_logger_provider(logger_provider)

client = anectico.AnecticoClient(
    api_key=os.environ["ANECTICO_API_KEY"],
    service_name="orders-api",
    open_telemetry_mode="existing",
)
client.start()

You can set ANECTICO_OTEL_MODE=existing instead. Anectico then creates no providers, exporters, export processors, metric readers, propagator, HTTP client instrumentation, or standard-library log bridge. Its span/error/AI/tool/agent and metric helpers use the application globals. log_event() uses the global Logger Provider and safely does nothing when no real one is registered.

The identity processors must precede the application’s export processors so spans and logs reach the exporter with anectico.distinct_id. They have no exporter or lifecycle behavior. Do not put person identity on metrics. Anectico’s managed resource, sampling, OTLP endpoint, batching, propagation, and logging options do not reconfigure application providers; the application sampler decides which helper spans export. Signal enable flags still gate helper emission.

Existing mode never flushes or shuts down application providers: client.flush() returns False, and client.stop() reports attempted=False, success=None. After the final helper call, stop Anectico and run the application’s existing provider flush/shutdown hook. To roll back only the Anectico layer, remove client startup and both identity processors. Restore the old exporter or Collector destination separately when rolling back OTLP delivery itself.

Add framework middleware

FastAPI:

from fastapi import FastAPI
from anectico import AnecticoClient
from anectico.integrations.fastapi import AnecticoFastAPIMiddleware

app = FastAPI()
client = AnecticoClient(api_key=os.environ['ANECTICO_API_KEY'], service_name='orders-api')
client.start()

async def authenticated_customer_id(scope):
    principal = await verify_bearer_token(scope)  # your server-side auth
    return principal.customer_id

app.add_middleware(
    AnecticoFastAPIMiddleware,
    client=client,
    request_identity_resolver=authenticated_customer_id,
)

The resolver runs before the server span starts and may be synchronous or asynchronous. Return only an identity established by server-side authentication; never copy a public header, query parameter, or caller-supplied baggage directly. The resolved identity is async-local, so interleaved requests, child spans, logs, errors, and trusted outbound calls stay with the correct customer.

Matched requests are named from the bounded FastAPI route template, such as POST /loans/{applicant_id}. Anectico exports the URL origin plus that template, never the concrete path, query string, or fragment; unmatched requests remain method-only and origin-only. Cancelled ASGI requests close with anectico.request.outcome=cancelled and a measured duration without creating an Issue. Pass skip_paths=set() when you intentionally want to instrument every path, including the normally excluded health/docs routes.

Flask uses AnecticoFlaskMiddleware(app, client). Register it after constructing the application and its routes; the middleware preserves Flask responses and error handlers, names matched requests from their bounded Werkzeug rule, and exports only the URL origin plus that rule. Concrete path identifiers, query values, and fragments are never exported; unmatched requests are named with the HTTP method only. Unhandled exceptions are captured before Flask writes its framework error log, so the diagnostic log and canonical Issue stay linked and the request span remains open through error handling.

from anectico.integrations.flask import AnecticoFlaskMiddleware

AnecticoFlaskMiddleware(app, client)

Outbound requests/httpx instrumentation remains opt-in. When a Flask route calls a trusted internal service, install the matching HTTP extra and allow only that service’s URL prefix; request headers and bodies are not captured by Anectico’s default configuration.

client = AnecticoClient(
    api_key=os.environ['ANECTICO_API_KEY'],
    service_name='legacy-policy-api',
    instrument_http_clients=True,
    propagate_trace_header_urls=('https://rating.internal',),
)

For Gunicorn, stop the process-local programmatic client from worker_exit so its final trace, log, and metric batches flush:

# gunicorn.conf.py
def worker_exit(server, worker):
    from insurance.app import client
    outcome = client.stop()
    if outcome.success is False:
        server.log.error('Anectico telemetry shutdown failed: %s', outcome.errors)

Create AnecticoCeleryIntegration(app, client) in every Celery producer and worker process. It injects W3C trace/person context into private broker headers and activates that context for each worker attempt, so retries, task children, logs, and terminal error capture stay on one causal trace and customer. Anectico does not copy task arguments, keyword arguments, return values, or retry exception messages into Celery span attributes. Do not accept broker messages from untrusted publishers.

Celery’s default worker logging setup may replace the root logger after Django or the application starts. The integration restores Anectico’s log bridge from Celery’s post-setup signals, so worker logs remain exported without disabling worker_hijack_root_logger.

Flush each prefork worker’s process-local client at shutdown:

import logging
from celery.signals import worker_process_shutdown

worker_logger = logging.getLogger(__name__)

@worker_process_shutdown.connect(weak=False)
def flush_anectico_worker(**_kwargs):
    if client.is_running:
        outcome = client.stop()
        if outcome.success is False:
            worker_logger.error(
                'Anectico telemetry shutdown failed: %s',
                outcome.errors,
            )

AnecticoCeleryIntegration.close() disconnects only its Celery signal receivers; the application still owns AnecticoClient.stop().

For Django, place Anectico after session and authentication middleware so the buyer is available before the view runs:

# settings.py
MIDDLEWARE = [
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'anectico.integrations.django.AnecticoDjangoMiddleware',
]

ANECTICO = {
    'API_KEY': os.environ['ANECTICO_API_KEY'],
    'SERVICE_NAME': 'marketplace-web',
    'SERVICE_VERSION': os.environ.get('APP_RELEASE', 'unknown'),
    'ENVIRONMENT': os.environ.get('ANECTICO_ENVIRONMENT', 'production'),
    'ENDPOINT': os.environ.get('ANECTICO_ENDPOINT', 'https://api.anectico.com'),
    # Optional callable or dotted path. The default uses authenticated user.pk.
    'USER_ID_RESOLVER': 'marketplace.observability.anectico_distinct_id',
}
# marketplace/observability.py
def anectico_distinct_id(request):
    user = getattr(request, 'user', None)
    if not user or not user.is_authenticated:
        return None
    return user.customer_id

The Django middleware creates one server span, privacy-safe ORM child spans, correlated standard logs, and one canonical capture for an unhandled view exception. It records the URL origin and path but never query values, fragments, usernames, or email addresses. Keep recognizable profile fields in sync_person(id, properties) at a trusted user creation/profile-update boundary; unlike identify, it does not change process-wide identity.

Gunicorn must flush each worker rather than only the master:

# gunicorn.conf.py
from anectico.integrations.django import shutdown_django_client

def worker_exit(server, worker):
    outcome = shutdown_django_client()
    if outcome.success is False:
        server.log.error('Anectico telemetry shutdown failed: %s', outcome.errors)

shutdown_django_client() returns the same delivery outcome and then detaches the process-local auto-created client. A repeated worker hook is harmless: attempted=False and success=None, rather than a false claim that another batch was flushed.

Framework middleware scopes identity and trace context to each request. Incoming identity baggage is ignored by default. Enable trust_incoming_identity=True only behind a gateway that strips and recreates untrusted baggage.

Connect trusted grpc.aio services

Install anectico[grpc], then add the client interceptor only to channels for trusted internal destinations. Add the server interceptor to each asynchronous gRPC service:

import grpc
from anectico.integrations.grpc import (
    AnecticoAioServerInterceptor,
    AnecticoAioUnaryUnaryClientInterceptor,
)

profile_channel = grpc.aio.insecure_channel(
    'dns:///profile.internal:50051',
    interceptors=[AnecticoAioUnaryUnaryClientInterceptor(client)],
)

grpc_server = grpc.aio.server(
    interceptors=[
        AnecticoAioServerInterceptor(client, trust_incoming_identity=True),
    ],
)

The unary-unary client span injects W3C traceparent, tracestate, and identity baggage into gRPC metadata; the server span extracts the same context, preserving one trace through FastAPI and each internal RPC. Propagation metadata and application messages are never copied into span attributes. The server rejects inbound identity by default. Enable trust_incoming_identity=True only on a private boundary that rejects or overwrites metadata from public callers. Typed non-OK gRPC results set rpc.grpc.status_code and an error span status without changing the application exception or retry behavior. These transport spans stay visible in traces and service health but do not create generic transport Issues. Call client.capture_error(...) once at the domain handling boundary when the failure should become a typed Issue, normally after retries are exhausted.

Scope identity safely

For a single-user process or an explicit login merge:

client.identify('user_8842', properties={'email': '[email protected]'})
client.group('company', 'acme', {'plan': 'enterprise'})
# On logout:
client.reset()

Reset clears the prior identity, groups, global error-user context, and uncaptured breadcrumbs. Telemetry captured before reset remains queued with its original attribution.

Do not switch process-wide identity per request in a concurrent server. Use middleware, or the async-safe shared identity context:

from anectico import reset_context_distinct_id, set_context_distinct_id

token = set_context_distinct_id(user_id)
try:
    handle_request()
finally:
    reset_context_distinct_id(token)

Record an application metric

client.record_metric(
    'checkout.queue_depth',
    queue.qsize(),
    labels={'region': 'eu-west', 'worker_pool': 'payments'},
)

record_metric records a gauge value. Use client.meter to create an explicit counter or histogram. Keep labels low-cardinality; do not add customer, order, request, trace, or session IDs.

Propagate only to trusted services

Install anectico[http] and, when needed, anectico[httpx]. Then explicitly allow the internal URL prefixes that may receive W3C trace context and customer identity:

client = AnecticoClient(
    api_key=os.environ['ANECTICO_API_KEY'],
    service_name='orders-api',
    instrument_http_clients=True,
    propagate_trace_header_urls=['https://billing.internal.example'],
)

The default allowlist is empty. Calls from uninstrumented clients remain unlinked.

Logs and AI calls

In managed mode, starting the client bridges standard-library logging at INFO and above by default, preserving the active trace and customer context:

import logging

logging.getLogger('orders').error(
    'payment failed',
    extra={
        'employee_id': 'employee-123',
        'payroll_period': '2026-07',
        'details': {
            'result': 'declined',
            'authorization': 'Bearer provider-secret',
        },
    },
)

If Django or another framework logs an exception that Anectico already captured, the log is linked to the canonical error occurrence and stays visible in Logs without creating a duplicate Issue. Independent error logs remain first-class Issues.

The logging bridge preserves safe extra fields and removes reserved LogRecord metadata. It recursively replaces built-in credential and financial-account fields with [REDACTED] before OTLP export; nested values are stored as bounded JSON when OpenTelemetry cannot represent them directly. Message and exception text also redact keyed credentials, Bearer/JWT values, credential URLs, IBANs, and valid payment-card-like values. Add application-specific field names through AnecticoClient(log_redaction_fields=[...]), AnecticoConfig(log_redaction_fields=(... ,)), or the comma-separated ANECTICO_LOG_REDACTION_FIELDS environment variable. Custom names extend the built-in protection; they never disable it.

Use anectico[openai] for synchronous, asynchronous, or streaming Chat Completions, and anectico[anthropic] for non-streaming messages. For an OpenAI stream, pass stream_options={"include_usage": True} and consume it to completion so Anectico can record the final token and cost fields. Closing early records one error-status call. Prompt and completion capture is opt-in and remains off by default.

Verify and recover

Generate one framework request for a known test customer. In Customers, confirm its trace, log, and test error appear together. If they do not, enable ANECTICO_DEBUG=true, verify the key and project, and inspect the result from client.stop().

The Python SDK rejects malformed or unsupported collector endpoints before starting. HTTP endpoints must be absolute http(s) URLs; gRPC endpoints must be an http(s) authority URL without a path or a host:port target. Signal-specific OTEL_EXPORTER_OTLP_*_ENDPOINT overrides are validated for enabled signals. HTTP certificate verification, TLS configuration, and client certificate failures are attempted once and reported with a sanitized diagnostic; verification stays enabled. Connection loss and retryable 408 or 5xx responses retain the same serialized batch through the configured export_timeout_ms window. Jittered exponential backoff is bounded by the remaining deadline, shutdown interrupts it, and exhaustion does not start a nested retry loop.