Skip to content

Auto-Instrumented Endpoint

This example proves that ordinary FastAPI and HTTPX auto-instrumentation can produce an AppEndpoint. The application files contain no OpenTelemetry imports, manual spans, or manually assigned span attributes.

The flow is:

HTTPX CLIENT span
  -> FastAPI SERVER span
  -> trace-affine Collector service-graph connector
  -> service-graph delta metric
  -> Flink graph contributions
  -> app.endpoint upsert
  -> ArangoDB

The example sends three identical requests so the asynchronous Collector flush and cumulative-to-delta stages are visible immediately. Every request uses the same normal HTTPX call.

Prepare the application environment

Run these PowerShell commands from the repository root:

py -3.12 -m venv .tmp/auto-instrumentation-venv
& .tmp/auto-instrumentation-venv/Scripts/python.exe -m pip install `
  --requirement examples/auto-instrumentation/requirements.txt
& .tmp/auto-instrumentation-venv/Scripts/opentelemetry-bootstrap.exe -a install

The bootstrap command discovers FastAPI and HTTPX and installs their matching OpenTelemetry instrumentors. The OTLP exporter is explicitly installed because instrumentation discovery does not choose an exporter.

Forward the Collector router

The complete Collector, Kafka, Flink, ArangoDB, indexer, and Gremlin deployment must already be running. Find the router Service and forward OTLP gRPC:

kubectl get services --all-namespaces | Select-String "servicegraph-collector.*router"
kubectl port-forward --namespace <namespace> `
  service/<collector-router-service> 4317:4317

Start the server

Open another terminal in the repository root:

$env:OTEL_SERVICE_NAME = "checkout-api"
$env:OTEL_RESOURCE_ATTRIBUTES = "service.namespace=shop"
$env:OTEL_EXPORTER_OTLP_ENDPOINT = "http://127.0.0.1:4317"
$env:OTEL_EXPORTER_OTLP_PROTOCOL = "grpc"
$env:OTEL_TRACES_EXPORTER = "otlp_proto_grpc"
$env:OTEL_METRICS_EXPORTER = "none"
$env:OTEL_LOGS_EXPORTER = "none"
$env:OTEL_SEMCONV_STABILITY_OPT_IN = "http"
$env:OTEL_PYTHON_DISABLED_INSTRUMENTATIONS = "click,logging,threading"

& .tmp/auto-instrumentation-venv/Scripts/opentelemetry-instrument.exe `
  .tmp/auto-instrumentation-venv/Scripts/python.exe -m uvicorn `
  --app-dir examples/auto-instrumentation server:app `
  --host 127.0.0.1 --port 18081

click is disabled because instrumenting Uvicorn's process-wide Click command can leave one active span around the server. FastAPI then observes that span and may create INTERNAL request spans instead of extracting the incoming HTTP context. Disabling it does not disable FastAPI or HTTPX instrumentation.

Send requests

Open one more terminal and set the client resource and exporter configuration:

$env:OTEL_SERVICE_NAME = "frontend"
$env:OTEL_RESOURCE_ATTRIBUTES = "service.namespace=shop"
$env:OTEL_EXPORTER_OTLP_ENDPOINT = "http://127.0.0.1:4317"
$env:OTEL_EXPORTER_OTLP_PROTOCOL = "grpc"
$env:OTEL_TRACES_EXPORTER = "otlp_proto_grpc"
$env:OTEL_METRICS_EXPORTER = "none"
$env:OTEL_LOGS_EXPORTER = "none"
$env:OTEL_SEMCONV_STABILITY_OPT_IN = "http"
$env:OTEL_PYTHON_DISABLED_INSTRUMENTATIONS = "click,logging,threading"

& .tmp/auto-instrumentation-venv/Scripts/opentelemetry-instrument.exe `
  .tmp/auto-instrumentation-venv/Scripts/python.exe `
  examples/auto-instrumentation/client.py

HTTPX supplies the CLIENT span and propagates its context. FastAPI supplies the correlated SERVER span and its http.route=/users/{user_id} attribute. The service namespace comes from the standard resource environment variable.

Query the endpoint

Forward the Gremlin Service and use the typed SDK:

kubectl port-forward --namespace <namespace> service/<gremlin-service> 8182:8182
& .venv/Scripts/python.exe
from extended_otel_semconv import AppEndpoint
from extended_otel_semconv.gremlin import SemanticGremlinClient

with SemanticGremlinClient("ws://127.0.0.1:8182/gremlin") as client:
    elements = client.query(
        lambda g: g.V()
        .has_label("app_endpoint")
        .has("service_name", "checkout-api")
    )

endpoint = next(element for element in elements if isinstance(element, AppEndpoint))
print(endpoint.model_dump_json(indent=2))

The expected identifying attributes are:

{
  "service.name": "checkout-api",
  "service.namespace": "shop",
  "http.request.method": "GET",
  "http.route": "/users/{user_id}"
}

If no endpoint appears, inspect the raw spans before changing the entity model. The server span must be SERVER, share the client's trace ID, use the client span ID as its parent, and contain http.route. Both spans must reach the same stateful Collector backend.

Add ETL execution context

ETL Pipeline identity is process-static when one deployment executes one pipeline. Configure it as resource data on the ETL worker:

$env:OTEL_SERVICE_NAME = "etl-worker"
$env:OTEL_RESOURCE_ATTRIBUTES = "service.namespace=data,etl.pipeline.id=extract-customers,etl.pipeline.name=Extract Customers"

Run and Part Run identities change without replacing the process, so they must be span attributes. The reference examples/auto-instrumentation/etl_context.py registers one span processor at process startup and copies process-local execution context onto every span started inside the wrappers. It does not create a tracer provider per execution and does not propagate ETL ownership into downstream services.

examples/auto-instrumentation/etl_client.py demonstrates the intended application boundary:

configure_etl_instrumentation()

with (
    etl_run(run_id="demo-run-001", name="Scheduled customer extraction"),
    etl_part(part_run_id="load-customers", name="Load customers"),
):
    httpx.get("http://127.0.0.1:18081/users/123")

Launch it through the same opentelemetry-instrument command used by the plain client. The HTTPX client spans receive the dynamic fields while the Pipeline fields remain on the resource. The service-graph connector can use configured dimensions from both locations.