Injecting OpenTelemetry with DataKit Operator¶
Starting with v1.9.0, DataKit Operator supports injecting OpenTelemetry automatic instrumentation into Java, Python, and Node.js applications. This feature uses the official OpenTelemetry automatic instrumentation images and follows their approach for copying instrumentation and configuring the startup environment.
The distributed templates set otels to an empty list by default, so OpenTelemetry injection is not enabled automatically. To enable it, first add the rules shown on this page. Injection occurs only when a Pod is created. The Operator modifies all regular application containers in the Pod. It does not modify application init Containers or detect language versions or libc inside containers.
Before You Begin¶
Enable the DataKit OpenTelemetry Input¶
The opentelemetry input must be enabled in DataKit. For example, add opentelemetry to the default enabled input list in the DataKit DaemonSet:
OTLP Traces, Metrics, and Logs use the same DataKit Service and port 9529, but different request paths:
Trace: http://datakit-service.datakit.svc.cluster.local:9529/otel/v1/traces
Metric: http://datakit-service.datakit.svc.cluster.local:9529/otel/v1/metrics
Log: http://datakit-service.datakit.svc.cluster.local:9529/otel/v1/logs
The example rules on this page enable only Traces. To collect Metrics or Logs, change the corresponding OTEL_METRICS_EXPORTER or OTEL_LOGS_EXPORTER from none to otlp; no new DataKit address is required. The application and automatic instrumentation must also be able to produce the corresponding signals.
Confirm Language Support¶
| Language | Supported range | Default image |
|---|---|---|
| Java | JVM supported by the OpenTelemetry Java Agent | ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java:2.29.0 |
| Python | Python 3.10 to 3.14; glibc Linux only | ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-python:0.64b0 |
| Node.js | Node.js 20.6 and later; supports glibc and Alpine/musl | ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-nodejs:0.78.0 |
Python 2, Python 3.9 and earlier, and Alpine/musl Python images are unsupported. Node.js currently supports conventional CommonJS applications, but not ESM, bundlers, or custom loaders. The official OpenTelemetry Operator does not yet formally support PHP automatic injection. Although an official PHP automatic-instrumentation image is available, the Operator does not yet provide the corresponding Instrumentation configuration or a standard injection flow, so DataKit Operator does not currently support OTel injection for PHP.
The Operator does not detect these conditions automatically. Use mutually exclusive Namespace or Label Selectors so that only compatible Pods match each rule.
Operator Configuration¶
otels is at the same level as ddtraces, and its default value in the distributed templates is []. The following configuration contains Java, Python, and Node.js rules. When editing an existing jsonconfig, replace the entire otels array at admission_inject_v2.otels and retain the other settings under admission_inject_v2.
Every rule uses "namespace_selectors": ["*"], but only Pods with the corresponding language label match. The rules can therefore be used across Namespaces without injecting unlabelled Pods automatically. All three rules export only Traces by default.
{
"admission_inject_v2": {
"otels": [
{
"name": "otel-java",
"language": "java",
"namespace_selectors": ["*"],
"label_selectors": ["admission.datakit/otel-language=java"],
"check_annotation": false,
"image": "ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java:2.29.0",
"envs": {
"POD_NAME": "{fieldRef:metadata.name}",
"POD_NAMESPACE": "{fieldRef:metadata.namespace}",
"NODE_NAME": "{fieldRef:spec.nodeName}",
"OTEL_SERVICE_NAME": "{fieldRef:metadata.labels['app']}",
"OTEL_RESOURCE_ATTRIBUTES": "k8s.pod.name=$(POD_NAME),k8s.namespace.name=$(POD_NAMESPACE),k8s.node.name=$(NODE_NAME)",
"OTEL_TRACES_EXPORTER": "otlp",
"OTEL_LOGS_EXPORTER": "none",
"OTEL_METRICS_EXPORTER": "none",
"OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "http://datakit-service.datakit.svc.cluster.local:9529/otel/v1/traces",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": "http://datakit-service.datakit.svc.cluster.local:9529/otel/v1/logs",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "http://datakit-service.datakit.svc.cluster.local:9529/otel/v1/metrics"
},
"resources": {
"requests": {
"cpu": "100m",
"memory": "64Mi"
},
"limits": {
"cpu": "500m",
"memory": "512Mi"
}
}
},
{
"name": "otel-python",
"language": "python",
"namespace_selectors": ["*"],
"label_selectors": ["admission.datakit/otel-language=python"],
"check_annotation": false,
"image": "ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-python:0.64b0",
"envs": {
"POD_NAME": "{fieldRef:metadata.name}",
"POD_NAMESPACE": "{fieldRef:metadata.namespace}",
"NODE_NAME": "{fieldRef:spec.nodeName}",
"OTEL_SERVICE_NAME": "{fieldRef:metadata.labels['app']}",
"OTEL_RESOURCE_ATTRIBUTES": "k8s.pod.name=$(POD_NAME),k8s.namespace.name=$(POD_NAMESPACE),k8s.node.name=$(NODE_NAME)",
"OTEL_TRACES_EXPORTER": "otlp",
"OTEL_LOGS_EXPORTER": "none",
"OTEL_METRICS_EXPORTER": "none",
"OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "http://datakit-service.datakit.svc.cluster.local:9529/otel/v1/traces",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": "http://datakit-service.datakit.svc.cluster.local:9529/otel/v1/logs",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "http://datakit-service.datakit.svc.cluster.local:9529/otel/v1/metrics"
},
"resources": {
"requests": {
"cpu": "100m",
"memory": "64Mi"
},
"limits": {
"cpu": "500m",
"memory": "512Mi"
}
}
},
{
"name": "otel-nodejs",
"language": "nodejs",
"namespace_selectors": ["*"],
"label_selectors": ["admission.datakit/otel-language=nodejs"],
"check_annotation": false,
"image": "ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-nodejs:0.78.0",
"envs": {
"POD_NAME": "{fieldRef:metadata.name}",
"POD_NAMESPACE": "{fieldRef:metadata.namespace}",
"NODE_NAME": "{fieldRef:spec.nodeName}",
"OTEL_SERVICE_NAME": "{fieldRef:metadata.labels['app']}",
"OTEL_RESOURCE_ATTRIBUTES": "k8s.pod.name=$(POD_NAME),k8s.namespace.name=$(POD_NAMESPACE),k8s.node.name=$(NODE_NAME)",
"OTEL_TRACES_EXPORTER": "otlp",
"OTEL_LOGS_EXPORTER": "none",
"OTEL_METRICS_EXPORTER": "none",
"OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "http://datakit-service.datakit.svc.cluster.local:9529/otel/v1/traces",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": "http://datakit-service.datakit.svc.cluster.local:9529/otel/v1/logs",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "http://datakit-service.datakit.svc.cluster.local:9529/otel/v1/metrics"
},
"resources": {
"requests": {
"cpu": "100m",
"memory": "64Mi"
},
"limits": {
"cpu": "500m",
"memory": "512Mi"
}
}
}
]
}
}
Environment variables preserve configuration order. In every rule, POD_NAME, POD_NAMESPACE, and NODE_NAME must appear before OTEL_RESOURCE_ATTRIBUTES, which references them.
Common fields:
| Field | Description |
|---|---|
name |
Rule name, recommended for locating relevant logs |
language |
Required; one of java, python, or nodejs |
namespace_selectors |
Array of Namespace regular expressions |
label_selectors |
Array of Pod Label Selectors |
check_annotation |
Whether the Pod must provide a version Annotation for the corresponding language; defaults to false |
image |
Required; official OpenTelemetry image or a copy in a private registry |
envs |
Environment variables injected into all regular application containers |
resources |
Resource configuration for the init Container; defaults are used when missing or invalid |
The three rules use the following language labels and images:
| Language | Label | Image |
|---|---|---|
| Java | admission.datakit/otel-language=java |
ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java:2.29.0 |
| Python | admission.datakit/otel-language=python |
ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-python:0.64b0 |
| Node.js | admission.datakit/otel-language=nodejs |
ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-nodejs:0.78.0 |
Set exactly one corresponding language label on each Pod. The distributed template's default Java DDTrace rule matches Pods in the default Namespace, so OTel Pods should also set admission.datakit/ddtrace.enabled: "false" explicitly; see the complete Deployment later in this document.
For the general rules governing selectors and Annotations, see DataKit Operator injection rules. If a Pod matches multiple OTel rules, the Operator uses only the first rule that satisfies the Annotation conditions. It does not fall back if that rule has an invalid language or image configuration.
Injection by Language¶
All languages add:
- The
datakit-otel-lib-initinit Container. - The
datakit-otel-auto-instrumentEmptyDir volume. - The
OTEL_*and other environment variables configured in the rule.
The instrumentation loading method differs by language:
| Language | init Container copy method | Mount and startup environment |
|---|---|---|
| Java | Copies /javaagent.jar from the image to the shared volume |
Mounts /otel-auto-instrumentation-java and appends to JAVA_TOOL_OPTIONS the value -javaagent:/otel-auto-instrumentation-java/javaagent.jar |
| Python | Copies /autoinstrumentation/ from the image to the shared volume |
Mounts /otel-auto-instrumentation-python, adds the automatic initialization and instrumentation directories to the beginning and end of PYTHONPATH, and loads through sitecustomize.py |
| Node.js | Copies /autoinstrumentation/ from the image to the shared volume |
Mounts /otel-auto-instrumentation-nodejs and appends to NODE_OPTIONS the value --require /otel-auto-instrumentation-nodejs/autoinstrumentation.js |
The Operator preserves existing ordinary string values. If the corresponding startup environment uses valueFrom, contains duplicates, already loads Datadog instrumentation, or an existing OTel init Container, volume, or mount conflicts with the expected value, the Operator skips the entire OTel injection and logs a warning. Admission remains fail-open and does not block creation of the application Pod.
Annotations and Versions¶
admission.datakit/otel.enabled: "false" disables OTel for an individual Pod. This switch always applies, regardless of check_annotation.
When a rule sets check_annotation: true, the Pod must also provide the version Annotation for the corresponding language:
| Language | Version Annotation |
|---|---|
| Java | admission.datakit/otel-java-lib.version |
| Python | admission.datakit/otel-python-lib.version |
| Node.js | admission.datakit/otel-nodejs-lib.version |
The version value replaces only the tag in the rule's image; it does not change the image registry or name. When the platform manages versions centrally, keeping check_annotation: false is recommended.
Relationship with DDTrace¶
The same container should not load DDTrace and OpenTelemetry automatic instrumentation simultaneously. When DDTrace and OTel rules both match, DDTrace takes precedence. If DDTrace is selected but injection fails, the Operator does not fall back to OTel.
Explicitly disable DDTrace for OTel workloads:
runAsNonRoot Safeguard¶
The default user in official OpenTelemetry automatic instrumentation images may be incompatible with the application Pod's security policy. The Operator applies the SecurityContext of the first application container to the OTel init Container.
If the effective configuration of the init Container is runAsNonRoot: true but does not explicitly set a nonzero runAsUser, Kubernetes may refuse to start it because it cannot confirm that the image runs as a non-root user. To prevent injection from leaving the application Pod stuck during initialization, the Operator skips the entire OTel injection and logs a warning containing the following reason:
To enable OTel in such a Pod, explicitly configure a nonzero runAsUser compatible with the OTel image on the first application container or the Pod. When runAsNonRoot: true, the Operator conservatively skips injection without an explicit runAsUser, even if a private image actually runs as a non-root user.
Deployment Example¶
The following Java Deployment matches the preceding configuration:
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
namespace: production
spec:
replicas: 1
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
admission.datakit/otel-language: java
annotations:
admission.datakit/ddtrace.enabled: "false"
admission.datakit/otel.enabled: "true"
spec:
containers:
- name: app
image: example/order-service:1.0.0
ports:
- name: http
containerPort: 8080
Change the label value to python or nodejs to match the corresponding language rule. The application image must meet the support requirements described above.
Verification and Troubleshooting¶
After creating the Pod, inspect the injection result:
kubectl -n production get pod -l app=order-service -o yaml
kubectl logs -n datakit deployment/datakit-operator
The resulting Pod should contain datakit-otel-lib-init, datakit-otel-auto-instrument, the mounts and startup environment for the corresponding language, and the OTEL_* environment variables. Then send an actual request to the application and query the page with:
Common issues:
- A running Pod did not change: recreate it; the Operator processes only
CREATE. - Nothing was injected: check the Namespace, Label,
check_annotation, DDTrace precedence, and Operator warnings. - The init Container cannot pull its image: official images use
imagePullPolicy: Always. Check GHCR connectivity, or synchronize the image to a private registry and update the rule. - Injection succeeded but no data appears: confirm that the
opentelemetryinput is enabled in DataKit, the OTLP address is reachable, the application framework is supported by automatic instrumentation, and an actual request has been sent. - Rollback is required: remove or disable the OTel rule, then recreate Pods that were already injected.