Trace AI Agent Workflows Across Kubernetes
This guide shows you how to monitor and trace AI agent workflows running as microservices on Kubernetes. You’ll learn how to connect logs, metrics, and traces across multiple services, and how to follow a single agent request from user input to final response. You’ll need a Kubernetes cluster with your AI agent deployed, plus access to install monitoring components.
What You’ll Need
- A Kubernetes cluster (v1.21 or later) with kubectl configured
- Helm 3 installed for deploying monitoring tools
- An AI agent workflow deployed as one or more Kubernetes services (e.g., a FastAPI service, a vector DB, an LLM gateway)
- Basic familiarity with YAML and Kubernetes manifests
- Permissions to create namespaces, deployments, and services
Step-by-Step
- Decide on your tracing backend. For most setups, Jaeger or Tempo are good choices. Jaeger is simpler to start with; Tempo integrates well with Grafana. This guide uses Jaeger.
- Install Jaeger. Run
kubectl create namespace observabilitythenhelm repo add jaegertracing https://jaegertracing.github.io/helm-chartsandhelm install jaeger jaegertracing/jaeger -n observability. This deploys the all-in-one Jaeger instance with a UI. - Install OpenTelemetry Collector. This is the agent that receives traces and forwards them to Jaeger. Add the OpenTelemetry Helm repo:
helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-chartsand install withhelm install otel-collector open-telemetry/opentelemetry-collector -n observability --set mode=deployment --set config.exporters.jaeger.endpoint=jaeger-collector.observability:14250. You’ll need to adjust the exporter endpoint based on your Jaeger service name. - Instrument your AI agent services. Add OpenTelemetry SDKs to your application code. For Python, use
opentelemetry-apiandopentelemetry-sdk. For Node.js, use@opentelemetry/sdk-node. Initialize the tracer with a service name likeagent-orchestratororllm-gateway. - Configure the exporter to send traces to the Collector. In your code, set the OTLP endpoint to
http://otel-collector.observability:4317(or the appropriate service DNS). Use environment variables:OTEL_EXPORTER_OTLP_ENDPOINTandOTEL_SERVICE_NAME. - Add manual spans for agent-specific steps. For example, when your agent calls a retrieval function, create a span with
tracer.start_as_current_span("retrieve_context"). This lets you see exactly which step took time. - Propagate context across service boundaries. Use W3C Trace Context headers. In your HTTP client (e.g., requests, axios), inject the current span context into the headers. The OpenTelemetry instrumentation libraries do this automatically for supported frameworks.
- Deploy your updated services. Rebuild and push your images, then apply the new deployments. Ensure that the services can reach the collector via its Kubernetes service DNS.
- Verify traces are flowing. Port-forward to the Jaeger UI:
kubectl port-forward svc/jaeger-query -n observability 16686:16686. Openhttp://localhost:16686and select your service name. You should see traces appearing. - Set up log correlation. Add trace IDs to your application logs. Use the OpenTelemetry logging integration to include
trace_idandspan_idin each log line. This allows you to search logs by trace ID later. - Add metrics (optional but recommended). Use OpenTelemetry metrics to record request counts, latency, and token usage. Export to Prometheus via the collector. Then create a Grafana dashboard to visualize these alongside traces.
- Create a workflow diagram. In Jaeger, you can view the trace as a waterfall. Use the “Trace Timeline” view to see how long each service took and where bottlenecks are. For a higher-level view, use the “Trace Graph” feature to see service dependencies.
Common Mistakes
- Not propagating context. If you use a custom HTTP client without OpenTelemetry instrumentation, trace context is lost and spans appear as separate traces. Always inject headers manually or use the provided instrumentation.
- Wrong collector endpoint. Using
localhostinstead of the Kubernetes service DNS. The collector is a separate pod; useotel-collector.observability:4317. - Forgetting to set the service name. Without
OTEL_SERVICE_NAME, all spans appear under an empty service name and are hard to filter. - Sampling too aggressively. Default sampling may drop most traces. Set a sampler to keep all traces initially (e.g.,
AlwaysOn) to verify the pipeline, then adjust. - Ignoring network policies. If your cluster has network policies, they may block traffic from your app to the collector. Allow egress to the observability namespace.
Tips
- Use a dedicated namespace for monitoring tools to keep them separate from your application.
- Instrument at the boundaries: each service should create a span for its entire processing, then child spans for internal steps.
- Use semantic conventions for span names, like
db.queryorhttp.request, to make traces easier to read. - Set up a retention policy for traces. Jaeger all-in-one stores traces in memory; for production, configure Elasticsearch or Cassandra as storage.
- Test the tracing pipeline with a simple “hello world” service before instrumenting your full agent.
Troubleshooting
- No traces appear in Jaeger. Check if the collector is running:
kubectl get pods -n observability. Look at collector logs:kubectl logs -n observability deployment/otel-collector. Also verify your app can reach the collector:kubectl exec -it your-pod -- curl http://otel-collector.observability:4317(if curl is available). - Traces appear but are incomplete. This usually means context propagation is failing. Check that your HTTP client injects the trace headers. Look for missing parent span IDs in the Jaeger UI.
- High latency in traces. The trace shows where time is spent. If a span shows a long database query, that’s your bottleneck. If a span is unexpectedly long, it might be waiting for a downstream service that has a timeout.
- Collector is crashing. Check the config for syntax errors. Use
kubectl logsto see error messages. Common issues are wrong exporter endpoint or invalid YAML. - Jaeger UI doesn’t load. Port-forward to the correct service name. If you used the Helm chart, the service might be named
jaeger-queryorjaeger. Runkubectl get svc -n observabilityto see.
Frequently Asked Questions
What is the difference between tracing and logging?
Tracing shows the path of a single request across services, with timing for each step. Logging records discrete events. Tracing is better for understanding performance and dependencies; logging is better for debugging specific errors.
Do I need to instrument every microservice?
Yes, if you want to see the full workflow. If a service isn’t instrumented, it will appear as a single span with no internal detail, but you’ll still see that it was called.
Can I use OpenTelemetry with existing monitoring tools?
Yes, OpenTelemetry is vendor-agnostic. You can export traces to Jaeger, Tempo, Datadog, or others. You can also use the collector to transform data and send it to multiple backends.
How do I trace asynchronous or event-driven workflows?
Use the same trace context, but propagate it through message headers (e.g., Kafka headers, SQS message attributes). OpenTelemetry provides instrumentation for common message queues.
What if my agent uses a third-party API that doesn’t support tracing?
You can create a span around the API call manually. Use tracer.start_as_current_span("external_api_call") and set attributes like http.url and http.status_code.
How do I filter traces for a specific user session?
Add a custom attribute to your spans, such as user.id or session.id. Then in Jaeger, you can search by tags to find all traces for that session.
Related
Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.