Skip to content

Commit

Permalink
Add autoinstrumentation for Python
Browse files Browse the repository at this point in the history
  • Loading branch information
Anuraag Agrawal committed Nov 11, 2021
1 parent bfe8637 commit e971264
Show file tree
Hide file tree
Showing 16 changed files with 669 additions and 2 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ EOF

### OpenTelemetry auto-instrumentation injection

The operator can inject and configure OpenTelemetry auto-instrumentation libraries. Currently Java and NodeJS are supported.
The operator can inject and configure OpenTelemetry auto-instrumentation libraries. Currently Java, NodeJS and Python are supported.

To use auto-instrumentation, configure an `Instrumentation` resource with the configuration for the SDK and instrumentation.

Expand All @@ -179,6 +179,8 @@ spec:
image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java:latest
nodejs:
image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-nodejs:latest
python:
image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-python:latest
EOF
```

Expand All @@ -198,6 +200,11 @@ NodeJS:
instrumentation.opentelemetry.io/inject-nodejs: "true"
```

Python:
```bash
instrumentation.opentelemetry.io/inject-python: "true"
```

The possible values for the annotation can be
* `"true"` - inject and `Instrumentation` resource from the namespace.
* `"my-instrumentation"` - name of `Instrumentation` CR instance.
Expand Down
13 changes: 13 additions & 0 deletions apis/instrumentation/v1alpha1/instrumentation_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ type InstrumentationSpec struct {
// +optional
// +operator-sdk:gen-csv:customresourcedefinitions.specDescriptors=true
NodeJS NodeJSSpec `json:"nodejs,omitempty"`

// Python defines configuration for python auto-instrumentation.
// +optional
// +operator-sdk:gen-csv:customresourcedefinitions.specDescriptors=true
Python PythonSpec `json:"python,omitempty"`
}

// JavaSpec defines Java SDK and instrumentation configuration.
Expand All @@ -65,6 +70,14 @@ type NodeJSSpec struct {
Image string `json:"image,omitempty"`
}

// PythonSpec defines Python SDK and instrumentation configuration.
type PythonSpec struct {
// Image is a container image with Python SDK and autoinstrumentation.
// +optional
// +operator-sdk:gen-csv:customresourcedefinitions.specDescriptors=true
Image string `json:"image,omitempty"`
}

// Exporter defines OTLP exporter configuration.
type Exporter struct {
// Endpoint is address of the collector with OTLP endpoint.
Expand Down
16 changes: 16 additions & 0 deletions apis/instrumentation/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions bundle/manifests/opentelemetry.io_instrumentations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ spec:
- "no"
type: string
type: array
python:
description: Python defines configuration for python auto-instrumentation.
properties:
image:
description: Image is a container image with Python SDK and autoinstrumentation.
type: string
type: object
resourceAttributes:
additionalProperties:
type: string
Expand Down
7 changes: 7 additions & 0 deletions config/crd/bases/opentelemetry.io_instrumentations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ spec:
- "no"
type: string
type: array
python:
description: Python defines configuration for python auto-instrumentation.
properties:
image:
description: Image is a container image with Python SDK and autoinstrumentation.
type: string
type: object
resourceAttributes:
additionalProperties:
type: string
Expand Down
1 change: 1 addition & 0 deletions pkg/instrumentation/annotation.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const (
// Possible values are "true", "false" or "<Instrumentation>" name.
annotationInjectJava = "instrumentation.opentelemetry.io/inject-java"
annotationInjectNodeJS = "instrumentation.opentelemetry.io/inject-nodejs"
annotationInjectPython = "instrumentation.opentelemetry.io/inject-python"
)

// annotationValue returns the effective annotationInjectJava value, based on the annotations from the pod and namespace.
Expand Down
10 changes: 9 additions & 1 deletion pkg/instrumentation/podmutator.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type instPodMutator struct {
type languageInstrumentations struct {
Java *v1alpha1.Instrumentation
NodeJS *v1alpha1.Instrumentation
Python *v1alpha1.Instrumentation
}

var _ webhookhandler.PodMutator = (*instPodMutator)(nil)
Expand Down Expand Up @@ -76,7 +77,14 @@ func (pm *instPodMutator) Mutate(ctx context.Context, ns corev1.Namespace, pod c
}
insts.NodeJS = inst

if insts.Java == nil && insts.NodeJS == nil {
if inst, err = pm.getInstrumentationInstance(ctx, ns, pod, annotationInjectPython); err != nil {
// we still allow the pod to be created, but we log a message to the operator's logs
logger.Error(err, "failed to select an OpenTelemetry Instrumentation instance for this pod")
return pod, err
}
insts.Python = inst

if insts.Java == nil && insts.NodeJS == nil && insts.Python == nil {
logger.V(1).Info("annotation not present in deployment, skipping instrumentation injection")
return pod, nil
}
Expand Down
98 changes: 98 additions & 0 deletions pkg/instrumentation/podmutator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,103 @@ func TestMutatePod(t *testing.T) {
},
},
},
{
name: "python injection, true",
ns: corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "python",
},
},
inst: v1alpha1.Instrumentation{
ObjectMeta: metav1.ObjectMeta{
Name: "example-inst",
Namespace: "python",
},
Spec: v1alpha1.InstrumentationSpec{
Python: v1alpha1.PythonSpec{
Image: "otel/python:1",
},
Exporter: v1alpha1.Exporter{
Endpoint: "http://collector:12345",
},
},
},
pod: corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
annotationInjectPython: "true",
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "app",
},
},
},
},
expected: corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
annotationInjectPython: "true",
},
},
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{
{
Name: "opentelemetry-auto-instrumentation",
VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{},
},
},
},
InitContainers: []corev1.Container{
{
Name: initContainerName,
Image: "otel/python:1",
Command: []string{"cp", "-a", "/autoinstrumentation/.", "/otel-auto-instrumentation/"},
VolumeMounts: []corev1.VolumeMount{{
Name: volumeName,
MountPath: "/otel-auto-instrumentation",
}},
},
},
Containers: []corev1.Container{
{
Name: "app",
Env: []corev1.EnvVar{
{
Name: "OTEL_SERVICE_NAME",
Value: "app",
},
{
Name: "OTEL_EXPORTER_OTLP_ENDPOINT",
Value: "http://collector:12345",
},
{
Name: "OTEL_RESOURCE_ATTRIBUTES",
Value: "k8s.container.name=app,k8s.namespace.name=python",
},
{
Name: "PYTHONPATH",
Value: fmt.Sprintf("%s:%s", pythonPathPrefix, pythonPathSuffix),
},
{
Name: "OTEL_TRACES_EXPORTER",
Value: "otlp_proto_http",
},
},
VolumeMounts: []corev1.VolumeMount{
{
Name: "opentelemetry-auto-instrumentation",
MountPath: "/otel-auto-instrumentation",
},
},
},
},
},
},
},
{
name: "missing annotation",
ns: corev1.Namespace{
Expand Down Expand Up @@ -401,6 +498,7 @@ func TestMutatePod(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
err := k8sClient.Create(context.Background(), &test.ns)
require.NoError(t, err)
defer k8sClient.Delete(context.Background(), &test.ns)
err = k8sClient.Create(context.Background(), &test.inst)
require.NoError(t, err)

Expand Down
82 changes: 82 additions & 0 deletions pkg/instrumentation/python.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package instrumentation

import (
"fmt"

"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"

"github.com/open-telemetry/opentelemetry-operator/apis/instrumentation/v1alpha1"
)

const (
envPythonPath = "PYTHONPATH"
envOtelTracesExporter = "OTEL_TRACES_EXPORTER"
pythonPathPrefix = "/otel-auto-instrumentation/opentelemetry/instrumentation/auto_instrumentation"
pythonPathSuffix = "/otel-auto-instrumentation"
)

func injectPythonSDK(logger logr.Logger, pythonSpec v1alpha1.PythonSpec, pod corev1.Pod) corev1.Pod {
// caller checks if there is at least one container
container := &pod.Spec.Containers[0]
idx := getIndexOfEnv(container.Env, envPythonPath)
if idx == -1 {
container.Env = append(container.Env, corev1.EnvVar{
Name: envPythonPath,
Value: fmt.Sprintf("%s:%s", pythonPathPrefix, pythonPathSuffix),
})
} else if idx > -1 {
if container.Env[idx].ValueFrom != nil {
// TODO add to status object or submit it as an event
logger.Info("Skipping Python SDK injection, the container defines PYTHONPATH env var value via ValueFrom", "container", container.Name)
return pod
}
container.Env[idx].Value = fmt.Sprintf("%s:%s:%s", pythonPathPrefix, container.Env[idx].Value, pythonPathSuffix)
}

// Set OTEL_TRACES_EXPORTER to HTTP exporter if not set by user because it is what our autoinstrumentation supports.
idx = getIndexOfEnv(container.Env, envOtelTracesExporter)
if idx == -1 {
container.Env = append(container.Env, corev1.EnvVar{
Name: envOtelTracesExporter,
Value: "otlp_proto_http",
})
}

container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{
Name: volumeName,
MountPath: "/otel-auto-instrumentation",
})

pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{
Name: volumeName,
VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{},
}})

pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{
Name: initContainerName,
Image: pythonSpec.Image,
Command: []string{"cp", "-a", "/autoinstrumentation/.", "/otel-auto-instrumentation/"},
VolumeMounts: []corev1.VolumeMount{{
Name: volumeName,
MountPath: "/otel-auto-instrumentation",
}},
})

return pod
}
Loading

0 comments on commit e971264

Please sign in to comment.