Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add autoinstrumentation for Python #532

Merged
merged 4 commits into from
Nov 17, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
100 changes: 100 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,9 @@ 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 func() {
_ = 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

python does not support OTLP GRPC?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does but uses a native extension wrapping gRPC-C. This means the extension can only be loaded on the same python version as when pip install was invoked.

HTTP transport is pure python so avoids causing potential version problems.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way related comment here

#524 (comment)

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