In the dynamic landscape of modern cloud-native applications, understanding the behavior and performance of your services is paramount. As applications become increasingly distributed and complex, traditional monitoring approaches often fall short. This is where observability, specifically through distributed tracing, metrics, and logs, becomes indispensable. OpenTelemetry has emerged as the de facto standard for instrumenting applications, providing a vendor-agnostic way to generate, collect, and export telemetry data.
However, manually instrumenting every service in a microservices architecture can be a daunting, time-consuming, and error-prone task. This challenge is amplified in Kubernetes environments where applications are constantly scaling, deploying, and evolving. OpenTelemetry auto-instrumentation for Kubernetes offers a powerful solution, allowing you to automatically inject OpenTelemetry SDKs into your applications without modifying their source code. This guide will walk you through the process of setting up OpenTelemetry auto-instrumentation in your Kubernetes cluster, ensuring your applications are fully observable with minimal effort.
TL;DR: OpenTelemetry Auto-Instrumentation for Kubernetes
Automate OpenTelemetry instrumentation in your Kubernetes cluster using an Admission Controller and an OpenTelemetry Operator. This setup injects OpenTelemetry SDKs into your applications without code changes, enabling distributed tracing, metrics, and logs.
Key Commands:
# Install OpenTelemetry Operator (if not already present)
helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
helm repo update
helm upgrade --install opentelemetry-operator open-telemetry/opentelemetry-operator \
--namespace opentelemetry-operator --create-namespace
# Deploy an OpenTelemetry Collector
kubectl apply -f collector.yaml
# Enable auto-instrumentation for a namespace
kubectl label namespace <your-namespace> opentelemetry-injection=enabled
# Deploy a sample application with auto-instrumentation
kubectl apply -f sample-app.yaml
# Verify traces (e.g., using Jaeger)
kubectl port-forward svc/jaeger-query 16686:16686 -n opentelemetry-collector
Prerequisites
Before diving into OpenTelemetry auto-instrumentation, ensure you have the following:
- A running Kubernetes cluster (v1.20+ recommended). You can use Minikube, Kind, or any cloud provider’s managed Kubernetes service (EKS, GKE, AKS).
kubectlinstalled and configured to connect to your cluster. For installation instructions, refer to the official Kubernetes documentation.helminstalled (v3.0+). Helm is crucial for deploying the OpenTelemetry Operator and other components. Check the Helm installation guide if you don’t have it.- Basic understanding of Kubernetes concepts like Pods, Deployments, Services, and Namespaces.
- Familiarity with observability concepts (metrics, traces, logs).
Step-by-Step Guide
1. Install the OpenTelemetry Operator
The OpenTelemetry Operator for Kubernetes provides a powerful way to manage OpenTelemetry components within your cluster. It includes an Admission Webhook that handles the auto-instrumentation injection. The operator watches for specific annotations or labels on Pods/Namespaces and automatically mutates Pod specifications to include the necessary OpenTelemetry SDKs and environment variables.
This operator simplifies the deployment and lifecycle management of OpenTelemetry Collectors, allowing you to define them as custom resources. It also streamlines the auto-instrumentation process by injecting sidecar containers or init containers, depending on the language and configuration. For instance, it can inject Java agents, Python auto-instrumentation libraries, or .NET profilers, ensuring your applications start sending telemetry data without requiring manual code changes or Dockerfile modifications.
# Add the OpenTelemetry Helm repository
helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
helm repo update
# Install the OpenTelemetry Operator
# We'll install it in its own namespace for better isolation.
helm upgrade --install opentelemetry-operator open-telemetry/opentelemetry-operator \
--namespace opentelemetry-operator --create-namespace
Verify
Check if the operator’s Pods are running and the Custom Resource Definitions (CRDs) are installed. You should see a deployment for the operator and its webhook.
kubectl get pods -n opentelemetry-operator
kubectl get crd | grep opentelemetry
Expected Output:
NAME READY STATUS RESTARTS AGE
opentelemetry-operator-controller-manager-... 1/1 Running 0 2m
NAME CREATED AT
opentelemetrycollectors.opentelemetry.io 2023-10-27T10:00:00Z
instrumentations.opentelemetry.io 2023-10-27T10:00:00Z
2. Deploy an OpenTelemetry Collector
The OpenTelemetry Collector is a vendor-agnostic proxy that receives, processes, and exports telemetry data. It’s a critical component in any OpenTelemetry setup, acting as an intermediary between your instrumented applications and your telemetry backend (e.g., Jaeger, Prometheus, Loki, your cloud provider’s observability suite). The collector can be deployed as an agent (sidecar or DaemonSet) or a gateway (Deployment).
For auto-instrumentation, applications will typically send their telemetry data to a collector endpoint. This collector then performs tasks like batching, filtering, enriching, and ultimately exporting the data to your chosen backend. Deploying it as a Deployment with a Service allows all instrumented applications to easily discover and send data to it. The collector’s configuration defines its receivers (how it gets data), processors (how it manipulates data), and exporters (where it sends data). In this example, we’ll configure it to export to Jaeger for distributed tracing visualization.
# collector.yaml
apiVersion: opentelemetry.io/v1alpha1
kind: OpenTelemetryCollector
metadata:
name: otel-collector
namespace: opentelemetry-collector # Create this namespace if it doesn't exist
spec:
mode: deployment
image: otel/opentelemetry-collector-contrib:0.87.0 # Use a stable contrib image
replicas: 1
config: |
receivers:
otlp:
protocols:
grpc:
http:
processors:
batch:
send_batch_size: 100
timeout: 10s
exporters:
jaeger:
endpoint: jaeger-collector:14250
tls:
insecure: true
logging:
loglevel: debug # Useful for debugging
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [jaeger, logging]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [logging] # For simplicity, not exporting metrics to Jaeger in this example
logs:
receivers: [otlp]
processors: [batch]
exporters: [logging] # For simplicity, not exporting logs in this example
---
apiVersion: v1
kind: Service
metadata:
name: jaeger-collector
namespace: opentelemetry-collector
spec:
selector:
app.kubernetes.io/component: collector
app.kubernetes.io/instance: jaeger
app.kubernetes.io/name: jaeger
ports:
- protocol: TCP
name: grpc
port: 14250
targetPort: 14250
- protocol: TCP
name: http
port: 14268
targetPort: 14268
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: jaeger
namespace: opentelemetry-collector
labels:
app.kubernetes.io/name: jaeger
app.kubernetes.io/instance: jaeger
spec:
selector:
matchLabels:
app.kubernetes.io/name: jaeger
app.kubernetes.io/instance: jaeger
replicas: 1
template:
metadata:
labels:
app.kubernetes.io/name: jaeger
app.kubernetes.io/instance: jaeger
spec:
containers:
- name: jaeger
image: jaegertracing/all-in-one:1.49 # All-in-one for simplicity
ports:
- containerPort: 16686 # UI
name: query
- containerPort: 14250 # gRPC collector
name: grpc
- containerPort: 14268 # HTTP collector
name: http
env:
- name: COLLECTOR_ZIPKIN_HOST_PORT
value: ":9411"
---
apiVersion: v1
kind: Service
metadata:
name: jaeger-query
namespace: opentelemetry-collector
spec:
selector:
app.kubernetes.io/name: jaeger
app.kubernetes.io/instance: jaeger
ports:
- protocol: TCP
name: http
port: 16686
targetPort: 16686
type: ClusterIP # Use LoadBalancer for external access in production
kubectl create namespace opentelemetry-collector
kubectl apply -f collector.yaml
Verify
Check if the collector and Jaeger Pods are running and their services are exposed.
kubectl get pods -n opentelemetry-collector
kubectl get svc -n opentelemetry-collector
Expected Output:
NAME READY STATUS RESTARTS AGE
otel-collector-56... 1/1 Running 0 1m
jaeger-5c... 1/1 Running 0 1m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
otel-collector ClusterIP 10.96.10.10 <none> 4317/TCP,4318/TCP 1m
jaeger-collector ClusterIP 10.96.20.20 <none> 14250/TCP,14268/TCP 1m
jaeger-query ClusterIP 10.96.30.30 <none> 16686/TCP 1m
3. Enable Auto-Instrumentation for a Namespace
The OpenTelemetry Operator’s Admission Webhook is the magic behind auto-instrumentation. By labeling a namespace, you instruct the webhook to intercept Pod creation requests within that namespace. When a new Pod is created, the webhook modifies its definition to inject the necessary OpenTelemetry SDKs, agents, or environment variables. This typically involves adding an initContainer or a sidecar container that sets up the instrumentation.
This approach is powerful because it’s completely transparent to your application code and deployment manifests. You don’t need to recompile, repackage, or even touch your application’s Dockerfile. The operator handles the language-specific injection (e.g., Java agent, Python wrapper, Node.js module) and ensures that the application is configured to export telemetry to the OpenTelemetry Collector endpoint. This significantly reduces the operational overhead of enabling observability across many services.
# Label the default namespace for auto-instrumentation
kubectl label namespace default opentelemetry-injection=enabled
Verify
Check if the label has been applied to the namespace.
kubectl get namespace default -o yaml | grep opentelemetry-injection
Expected Output:
opentelemetry-injection: enabled
4. Deploy a Sample Application
Now, let’s deploy a simple application that will be automatically instrumented. We’ll use a basic Python Flask application. Notice that there are no OpenTelemetry specific libraries or configurations in the application’s manifest or code. The magic happens when the Pod is created in the labeled namespace.
When this application’s Pod starts, the OpenTelemetry Operator’s webhook will detect the opentelemetry-injection=enabled label on the namespace. It will then mutate the Pod definition to include an init container or sidecar that sets up the Python OpenTelemetry SDK. This typically involves mounting an auto-instrumentation library and setting environment variables like OTEL_SERVICE_NAME and OTEL_EXPORTER_OTLP_ENDPOINT. The application will then automatically generate traces for HTTP requests and send them to the OpenTelemetry Collector we deployed earlier.
# sample-app.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: python-sample-app
labels:
app: python-sample-app
spec:
replicas: 1
selector:
matchLabels:
app: python-sample-app
template:
metadata:
labels:
app: python-sample-app
annotations:
instrumentation.opentelemetry.io/inject-python: "true" # Specific annotation for Python injection
spec:
containers:
- name: python-sample-app
image: python:3.9-slim-buster
command: ["/bin/bash", "-c"]
args:
- |
pip install Flask opentelemetry-sdk opentelemetry-exporter-otlp opentelemetry-instrumentation-flask
cat < app.py
from flask import Flask, request
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.resources import SERVICE_NAME
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.flask import FlaskInstrumentor
import os
# This part is typically handled by auto-instrumentation
# but is shown for context if manual setup were needed.
# For auto-instrumentation, the operator injects these.
resource = Resource.create({SERVICE_NAME: os.getenv("OTEL_SERVICE_NAME", "python-sample-app")})
provider = TracerProvider(resource=resource)
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint=os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://otel-collector.opentelemetry-collector:4317")))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
app = Flask(__name__)
FlaskInstrumentor().instrument_app(app) # This is typically done by the auto-instrumentation agent
@app.route("/")
def hello():
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("hello-request"):
return f"Hello from {os.getenv('OTEL_SERVICE_NAME', 'python-sample-app')}!"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
EOF
python app.py
ports:
- containerPort: 5000
---
apiVersion: v1
kind: Service
metadata:
name: python-sample-app
spec:
selector:
app: python-sample-app
ports:
- protocol: TCP
port: 80
targetPort: 5000
type: ClusterIP
kubectl apply -f sample-app.yaml
Verify
Observe the Pod events and check its description. You should see an initContainer related to OpenTelemetry injection. The application Pod should transition to a Running state.
kubectl get pods -l app=python-sample-app
kubectl describe pod -l app=python-sample-app | grep -A 5 "Init Containers"
Expected Output (snippet from describe pod):
Init Containers:
opentelemetry-auto-instrumentation-python:
Container ID: containerd://...
Image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-python:latest
Image ID: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-python@sha256:...
Port: <none>
Host Port: <none>
Command:
cp
-r
/otel-python-agent
/var/opentelemetry-instrumentation/
5. Generate Traffic and View Traces
Now that our application is running and auto-instrumented, let’s generate some traffic to ensure it produces traces. We’ll access the application through a simple port-forward.
After generating traffic, the auto-instrumented application will send its traces to the OpenTelemetry Collector, which in turn forwards them to Jaeger. Jaeger provides a user interface to visualize these distributed traces, allowing you to see the flow of requests across services, identify bottlenecks, and debug performance issues. This end-to-end visibility is crucial for understanding the behavior of complex microservices architectures.
# Port-forward the sample application service
kubectl port-forward svc/python-sample-app 8080:80 &
# Generate some requests
curl http://localhost:8080
curl http://localhost:8080/
# Port-forward Jaeger UI
kubectl port-forward svc/jaeger-query 16686:16686 -n opentelemetry-collector &
Verify
Open your web browser and navigate to http://localhost:16686. In the Jaeger UI, select “python-sample-app” from the “Service” dropdown and click “Find Traces.” You should see traces corresponding to your HTTP requests.
Expected Output: You should see traces in the Jaeger UI for the python-sample-app service, showing spans like GET / and hello-request.
Production Considerations
While auto-instrumentation simplifies observability, deploying it in a production Kubernetes environment requires careful planning:
- Resource Management: OpenTelemetry agents and collectors consume CPU and memory. Monitor their resource usage and scale collectors appropriately. Use Karpenter for cost optimization and intelligent node scaling to manage node resources efficiently, especially with fluctuating telemetry loads.
- High Availability: Deploy multiple replicas of the OpenTelemetry Collector and Jaeger components. Consider using a persistent storage backend for Jaeger (e.g., Elasticsearch, Cassandra) instead of
all-in-onefor production. - Security:
- TLS/mTLS: Secure communication between applications, collectors, and backends using TLS. The OpenTelemetry Collector supports TLS configuration.
- Authentication: Implement authentication for exporters sending data to external systems.
- Network Policies: Restrict network access to your OpenTelemetry Collector and observability backends using Kubernetes Network Policies. This prevents unauthorized data exfiltration or ingestion.
- Image Security: Use trusted and scanned container images for OpenTelemetry components. Integrate with tools like Sigstore and Kyverno for supply chain security and policy enforcement.
- Scalability: For large clusters, consider deploying OpenTelemetry Collectors as a DaemonSet for agent-level collection and separate Deployments for gateway-level collection. This architecture scales horizontally.
- Configuration Management: Manage OpenTelemetry Collector configurations using GitOps principles. Use Helm charts with configurable values for different environments.
- Sampling: In high-throughput environments, sending every trace can be overwhelming. Implement intelligent sampling strategies (e.g., head-based, tail-based) within your OpenTelemetry Collector to reduce data volume while retaining critical traces.
- Data Retention: Plan your data retention policies for your observability backend (e.g., Jaeger, Prometheus, Loki) to manage storage costs and compliance.
- Monitoring and Alerting: Monitor the OpenTelemetry Collector itself for health, throughput, and error rates. Set up alerts for any issues that might impact telemetry data flow.
- Language-Specific Considerations: Be aware of the limitations and specific requirements for auto-instrumentation across different programming languages. Some languages might require specific environment variables or library versions.
- Integration with Service Mesh: If you’re using a service mesh like Istio (especially with Istio Ambient Mesh), understand how OpenTelemetry auto-instrumentation interacts with the mesh’s inherent observability features. You might choose to leverage the mesh’s tracing capabilities or augment them with OpenTelemetry.
Troubleshooting
-
Issue: Pods are stuck in
PendingorInit:0/1.Solution: This often indicates a problem with the init container injected by the OpenTelemetry Operator. Describe the Pod to check init container logs and events.
kubectl describe pod <pod-name> kubectl logs <pod-name> -c opentelemetry-auto-instrumentation-python # Or other language agentCommon causes include image pull failures, incorrect permissions, or issues with the injected agent’s startup script.
-
Issue: Application Pods are running, but no traces appear in Jaeger.
Solution:
- Check Namespace Label: Ensure the namespace has
opentelemetry-injection=enabled. - Check Pod Annotations: Verify the Pod has the correct language-specific annotation, e.g.,
instrumentation.opentelemetry.io/inject-python: "true". - Collector Reachability: Ensure the application can reach the OpenTelemetry Collector. Check the
OTEL_EXPORTER_OTLP_ENDPOINTenvironment variable in the application’s Pod. - Collector Logs: Check the OpenTelemetry Collector logs for errors or dropped spans.
- Jaeger Collector Logs: Check if Jaeger’s collector is receiving data.
kubectl logs -f otel-collector-... -n opentelemetry-collectorkubectl logs -f jaeger-... -n opentelemetry-collector - Check Namespace Label: Ensure the namespace has
-
Issue: OpenTelemetry Operator Pod is not running.
Solution: Check the operator’s logs and events for startup failures.
kubectl get pods -n opentelemetry-operator kubectl describe pod opentelemetry-operator-controller-manager-... -n opentelemetry-operator kubectl logs opentelemetry-operator-controller-manager-... -n opentelemetry-operatorLook for issues like RBAC permissions, misconfigured webhooks, or resource constraints.
-
Issue: Auto-instrumentation is not working for a specific language/framework.
Solution:
- Operator Version: Ensure you’re using a recent version of the OpenTelemetry Operator that supports the desired language’s auto-instrumentation.
- Agent Image: Verify the specific auto-instrumentation image for that language is correct and available (e.g.,
ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java). - Documentation: Refer to the OpenTelemetry documentation for language-specific auto-instrumentation requirements and known limitations.
-
Issue: High resource consumption by auto-instrumented applications or the collector.
Solution:
- Sampling: Implement head or tail-based sampling in your OpenTelemetry Collector configuration to reduce the volume of traces.
- Batching: Optimize batch processor settings in the collector (
send_batch_size,timeout). - Resource Limits: Set appropriate CPU and memory limits on your application containers and the collector Pods to prevent resource exhaustion.
- Collector Scaling: Scale up the number of OpenTelemetry Collector replicas if it’s a bottleneck. Consider a DaemonSet for agent-level collection on each node.
-
Issue: Missing attributes or incorrect service names in traces.
Solution:
- Environment Variables: Ensure
OTEL_SERVICE_NAMEand other relevantOTEL_RESOURCE_ATTRIBUTESenvironment variables are correctly set in the instrumented Pod. The operator usually injects these. - Collector Processors: Use OpenTelemetry Collector processors like
resourcedetectionorattributesto enrich or modify span attributes if they are incorrect or missing.
- Environment Variables: Ensure
FAQ Section
-
What is OpenTelemetry auto-instrumentation?
OpenTelemetry auto-instrumentation is a technique that allows you to automatically instrument your applications to collect telemetry data (traces, metrics, logs) without modifying their source code. In Kubernetes, this is typically achieved using an OpenTelemetry Operator that injects language-specific agents or libraries into your application Pods at runtime.
-
Which programming languages are supported for auto-instrumentation?
The OpenTelemetry Operator supports auto-instrumentation for several popular languages, including Java, Python, Node.js, .NET, and PHP. Support for other languages is continuously evolving. Always refer to the official OpenTelemetry documentation for the most up-to-date list and specific requirements.
-
What’s the difference between auto-instrumentation and manual instrumentation?
Manual instrumentation involves adding OpenTelemetry SDK code directly into your application’s source code. This gives you fine-grained control over what data is collected and how. Auto-instrumentation uses agents or bytecode manipulation to automatically capture common operations (like HTTP requests, database calls) without touching your code. Auto-instrumentation is quicker to deploy for basic observability, while manual instrumentation is better for capturing custom business logic or specific performance bottlenecks.
-
Can I combine auto-instrumentation with manual instrumentation?
Yes, absolutely! This is a common and recommended practice. Auto-instrumentation provides a baseline of observability for common framework operations. You can then use manual instrumentation to add custom spans, attributes, or metrics for specific business transactions or critical code paths that the auto-instrumentation might miss. The two approaches complement each other, providing comprehensive visibility.
-
How does auto-instrumentation impact application performance?
OpenTelemetry auto-instrumentation agents are designed to have a minimal performance overhead. However, like any additional process or library, they will consume some CPU and memory resources. The impact varies depending on the language, the agent’s implementation, and the volume of telemetry data generated. It’s crucial to perform load testing and monitor resource usage in a staging environment to understand the specific impact on your applications.
Cleanup Commands
To remove all resources created during this tutorial:
# Delete the sample application
kubectl delete -f sample-app.yaml
# Remove the namespace label
kubectl label namespace default opentelemetry-injection-
# Delete the OpenTelemetry Collector and Jaeger
kubectl delete -f collector.yaml
kubectl delete namespace opentelemetry-collector
# Uninstall the OpenTelemetry Operator
helm uninstall opentelemetry-operator -n opentelemetry-operator
kubectl delete namespace opentelemetry-operator
# Remove Helm repo (optional)
helm repo remove open-telemetry
Next Steps / Further Reading
You’ve successfully set up OpenTelemetry auto-instrumentation! Here are some next steps to deepen your understanding and enhance your observability setup:
- Explore OpenTelemetry Collector Processors: Learn about various OpenTelemetry Collector processors for data enrichment, filtering, batching, and sampling.
- Integrate with Other Backends: Configure your OpenTelemetry Collector to export to other observability backends like Prometheus for metrics, Loki for logs, or cloud-specific services like AWS X-Ray, Google Cloud Trace, or Azure Monitor.
- Advanced Sampling Strategies: Dive deeper into OpenTelemetry sampling to manage data volume in high-traffic scenarios.
- Custom Instrumentation: Learn how to add manual instrumentation for critical business logic in your applications.
- eBPF Observability: Explore how eBPF can provide even deeper insights into your Kubernetes workloads, especially for network and system-level observability. Check out our guide on eBPF Observability with Hubble.
- Service Mesh Integration: If you’re using a service mesh, understand how to integrate OpenTelemetry with tools like Istio Ambient Mesh for enhanced traffic management and observability.
- Security and Policy: Strengthen your cluster’s security posture by implementing Kubernetes Network Policies and integrating with tools like Sigstore and Kyverno for supply chain security.
Conclusion
OpenTelemetry auto-instrumentation for Kubernetes is a game-changer for achieving comprehensive observability in cloud-native environments. By leveraging the OpenTelemetry Operator, you can effortlessly inject tracing, metrics, and logging capabilities into your applications without modifying their source code. This not only accelerates your journey to full observability but also reduces the maintenance burden associated with manual instrumentation.
As your Kubernetes clusters grow in complexity, the ability to automatically gain insights into your services becomes invaluable. Coupled with robust collectors and powerful visualization tools like Jaeger, OpenTelemetry provides the foundation for understanding application behavior, diagnosing performance issues, and ensuring the health of your distributed systems. Embrace auto-instrumentation to unlock the full potential of observability in your Kubernetes deployments.
