diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 5d4aea3a4..e4003f18b 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -1,9 +1,11 @@
name: build
on:
+ merge_group:
pull_request:
branches:
- main
+ - stable
jobs:
build-autoscaler:
diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml
index cd3a3dbb0..eec69175e 100644
--- a/.github/workflows/e2e.yaml
+++ b/.github/workflows/e2e.yaml
@@ -1,9 +1,11 @@
name: E2E Tests
on:
+ merge_group:
pull_request:
branches:
- main
+ - stable
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
diff --git a/.github/workflows/go-mod-tidy.yml b/.github/workflows/go-mod-tidy.yml
index ae75b75e7..d2734936f 100644
--- a/.github/workflows/go-mod-tidy.yml
+++ b/.github/workflows/go-mod-tidy.yml
@@ -1,5 +1,6 @@
name: go mod tidy
on:
+ merge_group:
pull_request:
jobs:
diff --git a/.gitignore b/.gitignore
index c12b445e0..7fde39a2c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,7 @@ node_modules
.DS_Store
go.work.sum
cli/odigos
-.venv
\ No newline at end of file
+.venv
+**/__pycache__/
+**/*.pyc
+serving-certs/
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
index e51bde1ec..aec4ea6f8 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -17,7 +17,10 @@
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}/instrumentor",
- "cwd": "${workspaceFolder}/instrumentor"
+ "cwd": "${workspaceFolder}/instrumentor",
+ "env": {
+ "LOCAL_MUTATING_WEBHOOK_CERT_DIR": "${workspaceFolder}/serving-certs"
+ }
},
{
"name": "frontend",
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 28224c7c9..c2327488a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -167,3 +167,36 @@ make debug-odiglet
Then, you can attach a debugger to the Odiglet pod. For example, if you are using Goland, you can follow the instructions [here](https://www.jetbrains.com/help/go/attach-to-running-go-processes-with-debugger.html#step-3-create-the-remote-run-debug-configuration-on-the-client-computer) to attach to a remote process.
For Visual Studio Code, you can use the `.vscode/launch.json` file in this repo to attach to the Odiglet pod.
+
+
+
+## Instrumentor
+
+### Debugging
+If the Mutating Webhook is enabled, follow these steps:
+
+1. Copy the TLS certificate and key:
+Create a local directory and extract the certificate and key by running the following command:
+```
+mkdir -p serving-certs && kubectl get secret instrumentor-webhook-cert -n odigos-system -o jsonpath='{.data.tls\.crt}' | base64 -d > serving-certs/tls.crt && kubectl get secret instrumentor-webhook-cert -n odigos-system -o jsonpath='{.data.tls\.key}' | base64 -d > serving-certs/tls.key
+```
+
+
+2. Apply this service to the cluster, it will replace the existing `odigos-instrumentor` service:
+
+```
+apiVersion: v1
+kind: Service
+metadata:
+ name: odigos-instrumentor
+ namespace: odigos-system
+spec:
+ type: ExternalName
+ externalName: host.docker.internal
+ ports:
+ - name: webhook-server
+ port: 9443
+ protocol: TCP
+```
+
+Once this is done, you can use the .vscode/launch.json configuration and run instrumentor local for debugging.
\ No newline at end of file
diff --git a/agents/python/configurator/__init__.py b/agents/python/configurator/__init__.py
index 30d9e83b1..501dc3ebd 100644
--- a/agents/python/configurator/__init__.py
+++ b/agents/python/configurator/__init__.py
@@ -5,11 +5,10 @@
MINIMUM_PYTHON_SUPPORTED_VERSION = (3, 8)
class OdigosPythonConfigurator(sdk_config._BaseConfigurator):
-
def _configure(self, **kwargs):
trace_exporters, metric_exporters, log_exporters = sdk_config._import_exporters(
- sdk_config._get_exporter_names("traces"),
- sdk_config._get_exporter_names("metrics"),
- sdk_config._get_exporter_names("logs"),
+ ['otlp_proto_http'] if sdk_config._get_exporter_names("traces") else [],
+ [],
+ [],
)
initialize_components(trace_exporters, metric_exporters, log_exporters)
\ No newline at end of file
diff --git a/cli/cmd/resources/autoscaler.go b/cli/cmd/resources/autoscaler.go
index a0047fa40..69c1aa99c 100644
--- a/cli/cmd/resources/autoscaler.go
+++ b/cli/cmd/resources/autoscaler.go
@@ -467,7 +467,7 @@ func NewAutoscalerDeployment(ns string, version string, imagePrefix string, imag
Resources: corev1.ResourceRequirements{
Limits: corev1.ResourceList{
"cpu": resource.MustParse("500m"),
- "memory": *resource.NewQuantity(134217728, resource.BinarySI),
+ "memory": *resource.NewQuantity(536870912, resource.BinarySI),
},
Requests: corev1.ResourceList{
"cpu": resource.MustParse("10m"),
diff --git a/cli/cmd/resources/instrumentor.go b/cli/cmd/resources/instrumentor.go
index 358a18c6a..238363e0b 100644
--- a/cli/cmd/resources/instrumentor.go
+++ b/cli/cmd/resources/instrumentor.go
@@ -501,7 +501,7 @@ func NewInstrumentorDeployment(ns string, version string, telemetryEnabled bool,
Resources: corev1.ResourceRequirements{
Limits: corev1.ResourceList{
"cpu": resource.MustParse("500m"),
- "memory": *resource.NewQuantity(134217728, resource.BinarySI),
+ "memory": *resource.NewQuantity(536870912, resource.BinarySI),
},
Requests: corev1.ResourceList{
"cpu": resource.MustParse("10m"),
diff --git a/cli/cmd/resources/scheduler.go b/cli/cmd/resources/scheduler.go
index 6cdf42be5..2b8bbc35f 100644
--- a/cli/cmd/resources/scheduler.go
+++ b/cli/cmd/resources/scheduler.go
@@ -259,7 +259,7 @@ func NewSchedulerDeployment(ns string, version string, imagePrefix string) *apps
Resources: corev1.ResourceRequirements{
Limits: corev1.ResourceList{
"cpu": resource.MustParse("500m"),
- "memory": *resource.NewQuantity(134217728, resource.BinarySI),
+ "memory": *resource.NewQuantity(536870912, resource.BinarySI),
},
Requests: corev1.ResourceList{
"cpu": resource.MustParse("10m"),
diff --git a/cli/cmd/resources/ui.go b/cli/cmd/resources/ui.go
index dbd8bc6e6..6cd464c23 100644
--- a/cli/cmd/resources/ui.go
+++ b/cli/cmd/resources/ui.go
@@ -94,7 +94,7 @@ func NewUIDeployment(ns string, version string, imagePrefix string) *appsv1.Depl
Resources: corev1.ResourceRequirements{
Limits: corev1.ResourceList{
"cpu": resource.MustParse("500m"),
- "memory": *resource.NewQuantity(134217728, resource.BinarySI),
+ "memory": *resource.NewQuantity(536870912, resource.BinarySI),
},
Requests: corev1.ResourceList{
"cpu": resource.MustParse("10m"),
diff --git a/docs/instrumentations/python/python.mdx b/docs/instrumentations/python/python.mdx
index bb70d05c1..21b4eb6c3 100644
--- a/docs/instrumentations/python/python.mdx
+++ b/docs/instrumentations/python/python.mdx
@@ -41,6 +41,7 @@ The following Python modules will be auto instrumented by Odigos:
### HTTP Frameworks
- [`asgi`](https://pypi.org/project/asgiref/) versions `asgiref >= 3.0.0, < 4.0.0`
- [`django`](https://pypi.org/project/Django/) versions `django >= 1.10.0`
+Please note that for Django instrumentation to work, you need to set the [DJANGO_SETTINGS_MODULE](https://docs.djangoproject.com/en/5.1/topics/settings/#envvar-DJANGO_SETTINGS_MODULE) environment variable. Make sure to set this in your Kubernetes manifest or Dockerfile to ensure proper configuration.
- [`fastapi`](https://pypi.org/project/fastapi/) versions `fastapi >= 0.58.0, < 0.59.0`, `fastapi-slim >= 0.111.0, < 0.112.0`
- [`flask`](https://pypi.org/project/Flask/) versions `flask >= 1.0.0`
- [`pyramid`](https://pypi.org/project/pyramid/) versions `pyramid >= 1.7.0`
diff --git a/helm/odigos/templates/autoscaler/deployment.yaml b/helm/odigos/templates/autoscaler/deployment.yaml
index 93e424262..f7ccf419e 100644
--- a/helm/odigos/templates/autoscaler/deployment.yaml
+++ b/helm/odigos/templates/autoscaler/deployment.yaml
@@ -55,7 +55,7 @@ spec:
resources:
limits:
cpu: 500m
- memory: 128Mi
+ memory: 512Mi
requests:
cpu: 10m
memory: 64Mi
diff --git a/helm/odigos/templates/instrumentor/deployment.yaml b/helm/odigos/templates/instrumentor/deployment.yaml
index 6093737de..43c106181 100644
--- a/helm/odigos/templates/instrumentor/deployment.yaml
+++ b/helm/odigos/templates/instrumentor/deployment.yaml
@@ -58,7 +58,7 @@ spec:
resources:
limits:
cpu: 500m
- memory: 128Mi
+ memory: 512Mi
requests:
cpu: 10m
memory: 64Mi
diff --git a/helm/odigos/templates/scheduler/deployment.yaml b/helm/odigos/templates/scheduler/deployment.yaml
index b238f3876..081d8d6e6 100644
--- a/helm/odigos/templates/scheduler/deployment.yaml
+++ b/helm/odigos/templates/scheduler/deployment.yaml
@@ -50,7 +50,7 @@ spec:
resources:
limits:
cpu: 500m
- memory: 128Mi
+ memory: 512Mi
requests:
cpu: 10m
memory: 64Mi
diff --git a/helm/odigos/templates/ui/deployment.yaml b/helm/odigos/templates/ui/deployment.yaml
index b071a7103..12a363e85 100644
--- a/helm/odigos/templates/ui/deployment.yaml
+++ b/helm/odigos/templates/ui/deployment.yaml
@@ -38,7 +38,7 @@ spec:
resources:
limits:
cpu: 500m
- memory: 128Mi
+ memory: 512Mi
requests:
cpu: 10m
memory: 64Mi
diff --git a/instrumentor/controllers/instrumentationdevice/pods_webhook.go b/instrumentor/controllers/instrumentationdevice/pods_webhook.go
index 45fb0594c..cbf4f231c 100644
--- a/instrumentor/controllers/instrumentationdevice/pods_webhook.go
+++ b/instrumentor/controllers/instrumentationdevice/pods_webhook.go
@@ -3,22 +3,26 @@ package instrumentationdevice
import (
"context"
"fmt"
+ "strings"
+ common "github.com/odigos-io/odigos/common"
"sigs.k8s.io/controller-runtime/pkg/webhook"
corev1 "k8s.io/api/core/v1"
- logf "sigs.k8s.io/controller-runtime/pkg/log"
-
"k8s.io/apimachinery/pkg/runtime"
)
+const (
+ EnvVarNamespace = "ODIGOS_WORKLOAD_NAMESPACE"
+ EnvVarContainerName = "ODIGOS_CONTAINER_NAME"
+ EnvVarPodName = "ODIGOS_POD_NAME"
+)
+
type PodsWebhook struct{}
var _ webhook.CustomDefaulter = &PodsWebhook{}
func (p *PodsWebhook) Default(ctx context.Context, obj runtime.Object) error {
- // TODO(edenfed): add object selector to mutatingwebhookconfiguration
- log := logf.FromContext(ctx)
pod, ok := obj.(*corev1.Pod)
if !ok {
return fmt.Errorf("expected a Pod but got a %T", obj)
@@ -28,7 +32,71 @@ func (p *PodsWebhook) Default(ctx context.Context, obj runtime.Object) error {
pod.Annotations = map[string]string{}
}
- //pod.Annotations["odigos.io/instrumented-webhook"] = "true"
- log.V(0).Info("Defaulted Pod", "name", pod.Name)
+ // Inject ODIGOS environment variables into all containers
+ injectOdigosEnvVars(pod)
+
return nil
}
+
+func injectOdigosEnvVars(pod *corev1.Pod) {
+ namespace := pod.Namespace
+
+ // Common environment variables that do not change across containers
+ commonEnvVars := []corev1.EnvVar{
+ {
+ Name: EnvVarNamespace,
+ Value: namespace,
+ },
+ {
+ Name: EnvVarPodName,
+ ValueFrom: &corev1.EnvVarSource{
+ FieldRef: &corev1.ObjectFieldSelector{
+ FieldPath: "metadata.name",
+ },
+ },
+ },
+ }
+
+ for i := range pod.Spec.Containers {
+ container := &pod.Spec.Containers[i]
+
+ // Check if the container does NOT have device in conatiner limits. If so, skip the environment injection.
+ if !hasOdigosInstrumentationInLimits(container.Resources) {
+ continue
+ }
+
+ // Check if the environment variables are already present, if so skip inject them again.
+ if envVarsExist(container.Env, commonEnvVars) {
+ continue
+ }
+
+ container.Env = append(container.Env, append(commonEnvVars, corev1.EnvVar{
+ Name: EnvVarContainerName,
+ Value: container.Name,
+ })...)
+ }
+}
+
+func envVarsExist(containerEnv []corev1.EnvVar, commonEnvVars []corev1.EnvVar) bool {
+ envMap := make(map[string]struct{})
+ for _, envVar := range containerEnv {
+ envMap[envVar.Name] = struct{}{} // Inserting empty struct as value
+ }
+
+ for _, commonEnvVar := range commonEnvVars {
+ if _, exists := envMap[commonEnvVar.Name]; exists { // Checking if key exists
+ return true
+ }
+ }
+ return false
+}
+
+// Helper function to check if a container's resource limits have a key starting with the specified namespace
+func hasOdigosInstrumentationInLimits(resources corev1.ResourceRequirements) bool {
+ for resourceName := range resources.Limits {
+ if strings.HasPrefix(string(resourceName), common.OdigosResourceNamespace) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/instrumentor/controllers/startlangdetection/manager.go b/instrumentor/controllers/startlangdetection/manager.go
index 16b90af9c..e39e88ad7 100644
--- a/instrumentor/controllers/startlangdetection/manager.go
+++ b/instrumentor/controllers/startlangdetection/manager.go
@@ -1,6 +1,7 @@
package startlangdetection
import (
+ "github.com/odigos-io/odigos/instrumentor/controllers/utils"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
ctrl "sigs.k8s.io/controller-runtime"
@@ -59,18 +60,18 @@ func SetupWithManager(mgr ctrl.Manager) error {
return err
}
- //err = builder.
- // ControllerManagedBy(mgr).
- // Named("startlangdetection-configmaps").
- // For(&corev1.ConfigMap{}).
- // WithEventFilter(&utils.OnlyUpdatesPredicate{}).
- // Complete(&OdigosConfigReconciler{
- // Client: mgr.GetClient(),
- // Scheme: mgr.GetScheme(),
- // })
- //if err != nil {
- // return err
- //}
+ err = builder.
+ ControllerManagedBy(mgr).
+ Named("startlangdetection-configmaps").
+ For(&corev1.ConfigMap{}).
+ WithEventFilter(&utils.OnlyUpdatesPredicate{}).
+ Complete(&OdigosConfigReconciler{
+ Client: mgr.GetClient(),
+ Scheme: mgr.GetScheme(),
+ })
+ if err != nil {
+ return err
+ }
return nil
}
diff --git a/instrumentor/main.go b/instrumentor/main.go
index f892d1745..67a9494b9 100644
--- a/instrumentor/main.go
+++ b/instrumentor/main.go
@@ -20,6 +20,8 @@ import (
"flag"
"os"
+ "github.com/odigos-io/odigos/k8sutils/pkg/env"
+
"github.com/odigos-io/odigos/instrumentor/controllers/instrumentationconfig"
"github.com/odigos-io/odigos/instrumentor/controllers/startlangdetection"
@@ -33,6 +35,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/webhook"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
@@ -98,7 +101,7 @@ func main() {
logger := zapr.NewLogger(zapLogger)
ctrl.SetLogger(logger)
- mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
+ mgrOptions := ctrl.Options{
Scheme: scheme,
Metrics: metricsserver.Options{
BindAddress: metricsAddr,
@@ -171,9 +174,25 @@ func main() {
&corev1.Namespace{}: {
Label: labels.Set{consts.OdigosInstrumentationLabel: consts.InstrumentationEnabled}.AsSelector(),
},
+ &corev1.ConfigMap{}: {
+ Field: client.InNamespace(env.GetCurrentNamespace()).AsSelector(),
+ },
},
},
- })
+ }
+
+ // Check if the environment variable `LOCAL_WEBHOOK_CERT_DIR` is set.
+ // If defined, add WebhookServer options with the specified certificate directory.
+ // This is used primarily for local development environments to provide a custom path for serving TLS certificates.
+ localCertDir := os.Getenv("LOCAL_MUTATING_WEBHOOK_CERT_DIR")
+ if localCertDir != "" {
+ mgrOptions.WebhookServer = webhook.NewServer(webhook.Options{
+ CertDir: localCertDir,
+ })
+ }
+
+ mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), mgrOptions)
+
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
diff --git a/k8sutils/pkg/workload/ownerreference.go b/k8sutils/pkg/workload/ownerreference.go
index f5bacf863..fbb061ee9 100644
--- a/k8sutils/pkg/workload/ownerreference.go
+++ b/k8sutils/pkg/workload/ownerreference.go
@@ -1,36 +1,43 @@
package workload
import (
- "errors"
+ "fmt"
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
+// GetWorkloadFromOwnerReference retrieves both the workload name and workload kind
+// from the provided owner reference.
func GetWorkloadFromOwnerReference(ownerReference metav1.OwnerReference) (workloadName string, workloadKind WorkloadKind, err error) {
- ownerName := ownerReference.Name
- ownerKind := ownerReference.Kind
+
+ return GetWorkloadNameAndKind(ownerReference.Name, ownerReference.Kind)
+}
+
+func GetWorkloadNameAndKind(ownerName, ownerKind string) (string, WorkloadKind, error) {
if ownerKind == "ReplicaSet" {
- // ReplicaSet name is in the format -
- hyphenIndex := strings.LastIndex(ownerName, "-")
- if hyphenIndex == -1 {
- // It is possible for a user to define a bare ReplicaSet without a deployment, currently not supporting this
- err = errors.New("replicaset name does not contain a hyphen")
- return
- }
- // Extract deployment name from ReplicaSet name
- workloadName = ownerName[:hyphenIndex]
- workloadKind = WorkloadKindDeployment
- return
+ return extractDeploymentInfo(ownerName)
+ }
+ return handleNonReplicaSet(ownerName, ownerKind)
+}
+
+// extractDeploymentInfo extracts deployment information from a ReplicaSet name
+func extractDeploymentInfo(replicaSetName string) (string, WorkloadKind, error) {
+ hyphenIndex := strings.LastIndex(replicaSetName, "-")
+ if hyphenIndex == -1 {
+ return "", "", fmt.Errorf("replicaset name '%s' does not contain a hyphen", replicaSetName)
}
- workloadKind = WorkloadKindFromString(ownerKind)
+ deploymentName := replicaSetName[:hyphenIndex]
+ return deploymentName, WorkloadKindDeployment, nil
+}
+
+// handleNonReplicaSet processes non-ReplicaSet workload types
+func handleNonReplicaSet(ownerName, ownerKind string) (string, WorkloadKind, error) {
+ workloadKind := WorkloadKindFromString(ownerKind)
if workloadKind == "" {
- err = ErrKindNotSupported
- return
+ return "", "", ErrKindNotSupported
}
- workloadName = ownerName
- err = nil
- return
+ return ownerName, workloadKind, nil
}
diff --git a/odiglet/pkg/ebpf/director.go b/odiglet/pkg/ebpf/director.go
index 4855dd582..0d508056c 100644
--- a/odiglet/pkg/ebpf/director.go
+++ b/odiglet/pkg/ebpf/director.go
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"os"
+ "reflect"
"sync"
"sync/atomic"
"syscall"
@@ -175,6 +176,11 @@ var IsProcessExists = func(pid int) bool {
return false
}
+// Since OtelEbpfSdk is a generic type, we can't simply check it is nil with inst == nil
+func isNil[T OtelEbpfSdk](inst T) bool {
+ return reflect.ValueOf(&inst).Elem().IsZero()
+}
+
func (d *EbpfDirector[T]) periodicCleanup(ctx context.Context) {
ticker := time.NewTicker(CleanupInterval)
defer ticker.Stop()
@@ -189,7 +195,10 @@ func (d *EbpfDirector[T]) periodicCleanup(ctx context.Context) {
newInstrumentedProcesses := make([]*InstrumentedProcess[T], 0, len(details.InstrumentedProcesses))
for i := range details.InstrumentedProcesses {
ip := details.InstrumentedProcesses[i]
- if !IsProcessExists(ip.PID) && any(ip.inst) != nil {
+ // if the process does not exist, we should make sure we clean the instrumentation resources.
+ // Also making sure the instrumentation itself is not nil to avoid closing it here.
+ // This can happen if the process exits while the instrumentation is initializing.
+ if !IsProcessExists(ip.PID) && !isNil(ip.inst) {
log.Logger.V(0).Info("Instrumented process does not exist, cleaning up", "pid", ip.PID)
d.cleanProcess(ctx, pod, ip)
} else {
@@ -428,7 +437,7 @@ func (d *EbpfDirector[T]) GetWorkloadInstrumentations(workload *workload.PodWork
}
for _, ip := range details.InstrumentedProcesses {
- if any(ip.inst) != nil {
+ if !isNil(ip.inst) {
insts = append(insts, ip.inst)
}
}
diff --git a/odiglet/pkg/ebpf/test/director_test.go b/odiglet/pkg/ebpf/director_test.go
similarity index 70%
rename from odiglet/pkg/ebpf/test/director_test.go
rename to odiglet/pkg/ebpf/director_test.go
index d8437f815..18baed701 100644
--- a/odiglet/pkg/ebpf/test/director_test.go
+++ b/odiglet/pkg/ebpf/director_test.go
@@ -1,4 +1,4 @@
-package test
+package ebpf
import (
"context"
@@ -11,7 +11,6 @@ import (
"github.com/odigos-io/odigos/common"
"github.com/odigos-io/odigos/k8sutils/pkg/instrumentation_instance"
"github.com/odigos-io/odigos/k8sutils/pkg/workload"
- "github.com/odigos-io/odigos/odiglet/pkg/ebpf"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -32,7 +31,7 @@ type FakeEbpfSdk struct {
}
// compile-time check that FakeEbpfSdk implements ConfigurableOtelEbpfSdk
-var _ ebpf.ConfigurableOtelEbpfSdk = (*FakeEbpfSdk)(nil)
+var _ ConfigurableOtelEbpfSdk = (*FakeEbpfSdk)(nil)
func (f *FakeEbpfSdk) ApplyConfig(ctx context.Context, config *odigosv1.InstrumentationConfig) error {
return nil
@@ -63,24 +62,27 @@ func (f *FakeEbpfSdk) Run(ctx context.Context) error {
}
type FakeInstrumentationFactory struct {
+ timeToSetup time.Duration
kubeclient client.Client
}
-func NewFakeInstrumentationFactory(kubeclient client.Client) ebpf.InstrumentationFactory[*FakeEbpfSdk] {
+func NewFakeInstrumentationFactory(kubeclient client.Client, setupDuration time.Duration) InstrumentationFactory[*FakeEbpfSdk] {
return &FakeInstrumentationFactory{
kubeclient: kubeclient,
+ timeToSetup: setupDuration,
}
}
func (f *FakeInstrumentationFactory) CreateEbpfInstrumentation(ctx context.Context, pid int, serviceName string, podWorkload *workload.PodWorkload, containerName string, podName string, loadedIndicator chan struct{}) (*FakeEbpfSdk, error) {
+ <-time.After(f.timeToSetup)
return &FakeEbpfSdk{
loadedIndicator: loadedIndicator,
pid: pid,
}, nil
}
-func newFakeDirector(ctx context.Context, client client.Client) ebpf.Director {
- dir := ebpf.NewEbpfDirector(ctx, client, client.Scheme(), common.GoProgrammingLanguage, NewFakeInstrumentationFactory(client))
+func newFakeDirector(ctx context.Context, client client.Client, setupDuration time.Duration) Director {
+ dir := NewEbpfDirector(ctx, client, client.Scheme(), common.GoProgrammingLanguage, NewFakeInstrumentationFactory(client, setupDuration))
return dir
}
@@ -113,7 +115,7 @@ func assertHealthyInstrumentationInstance(t *testing.T, client client.Client, po
return assert.False(t, *instance.Status.Healthy)
}
-func assertInstrumentationInstanceDeleted(t *testing.T, client client.Client, pod types.NamespacedName, pid int) bool {
+func assertInstrumentationInstanceNotExisting(t *testing.T, client client.Client, pod types.NamespacedName, pid int) bool {
// instrumentation instance is deleted
return assert.Eventually(t, func() bool {
return getInstrumentationInstance(client, pod, pid) == nil
@@ -145,15 +147,15 @@ func TestSingleInstrumentation(t *testing.T) {
WithRuntimeObjects(&pod).
Build()
- origIsProcessExists := ebpf.IsProcessExists
- ebpf.IsProcessExists = func(pid int) bool {
+ origIsProcessExists := IsProcessExists
+ IsProcessExists = func(pid int) bool {
return true
}
- t.Cleanup(func() { ebpf.IsProcessExists = origIsProcessExists })
+ t.Cleanup(func() { IsProcessExists = origIsProcessExists })
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- dir := newFakeDirector(ctx, client).(*ebpf.EbpfDirector[*FakeEbpfSdk])
+ dir := newFakeDirector(ctx, client, time.Millisecond).(*EbpfDirector[*FakeEbpfSdk])
err := dir.Instrument(ctx, 1, pod_id, workload, "test-app", "test-container")
assert.NoError(t, err)
@@ -171,7 +173,7 @@ func TestSingleInstrumentation(t *testing.T) {
// cleanup
dir.Cleanup(pod_id)
// the instrumentation instance is deleted
- if !assertInstrumentationInstanceDeleted(t, client, pod_id, 1) {
+ if !assertInstrumentationInstanceNotExisting(t, client, pod_id, 1) {
t.FailNow()
}
@@ -210,18 +212,18 @@ func TestInstrumentNotExistingProcess(t *testing.T) {
WithRuntimeObjects(&pod).
Build()
- origIsProcessExists := ebpf.IsProcessExists
- ebpf.IsProcessExists = func(pid int) bool {
+ origIsProcessExists := IsProcessExists
+ IsProcessExists = func(pid int) bool {
return true
}
- t.Cleanup(func() { ebpf.IsProcessExists = origIsProcessExists })
+ t.Cleanup(func() { IsProcessExists = origIsProcessExists })
// setup the cleanup interval to be very short for the test to be responsive
- origCleanupInterval := ebpf.CleanupInterval
- ebpf.CleanupInterval = 10 * time.Millisecond
- t.Cleanup(func() { ebpf.CleanupInterval = origCleanupInterval })
+ origCleanupInterval := CleanupInterval
+ CleanupInterval = 10 * time.Millisecond
+ t.Cleanup(func() { CleanupInterval = origCleanupInterval })
- dir := newFakeDirector(ctx, client).(*ebpf.EbpfDirector[*FakeEbpfSdk])
+ dir := newFakeDirector(ctx, client, time.Millisecond).(*EbpfDirector[*FakeEbpfSdk])
err := dir.Instrument(ctx, 1, pod_id, workload, "test-app", "test-container")
assert.NoError(t, err)
@@ -238,11 +240,11 @@ func TestInstrumentNotExistingProcess(t *testing.T) {
assert.False(t, inst.closed)
// "kill" the process
- ebpf.IsProcessExists = func(pid int) bool {
+ IsProcessExists = func(pid int) bool {
return false
}
// the instrumentation instance is deleted
- if !assertInstrumentationInstanceDeleted(t, client, pod_id, 1) {
+ if !assertInstrumentationInstanceNotExisting(t, client, pod_id, 1) {
t.FailNow()
}
// the director stopped tracking the instrumentation
@@ -254,6 +256,67 @@ func TestInstrumentNotExistingProcess(t *testing.T) {
assert.True(t, inst.closed)
}
+func TestInstrumentNotExistingProcessWithSlowInstrumentation(t *testing.T) {
+ ctx := context.Background()
+ scheme := runtime.NewScheme()
+ corev1.AddToScheme(scheme)
+ odigosv1.AddToScheme(scheme)
+
+ workload := &workload.PodWorkload{
+ Name: "test-workload",
+ Namespace: "default",
+ Kind: "Deployment",
+ }
+ pod_id := types.NamespacedName{Name: "test", Namespace: "default"}
+ pod := corev1.Pod{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: pod_id.Name,
+ Namespace: pod_id.Namespace,
+ },
+ }
+
+ client := fake.
+ NewClientBuilder().
+ WithScheme(scheme).
+ WithStatusSubresource(&odigosv1.InstrumentationInstance{}).
+ WithRuntimeObjects(&pod).
+ Build()
+
+ origIsProcessExists := IsProcessExists
+ IsProcessExists = func(pid int) bool {
+ return true
+ }
+ t.Cleanup(func() { IsProcessExists = origIsProcessExists })
+
+ // setup the cleanup interval to be very short for the test to be responsive
+ origCleanupInterval := CleanupInterval
+ CleanupInterval = 10 * time.Millisecond
+ t.Cleanup(func() { CleanupInterval = origCleanupInterval })
+
+ dir := newFakeDirector(ctx, client, time.Second).(*EbpfDirector[*FakeEbpfSdk])
+ err := dir.Instrument(ctx, 1, pod_id, workload, "test-app", "test-container")
+ assert.NoError(t, err)
+
+ <-time.After(100 * time.Millisecond)
+ // "kill" the process while the instrumentation is still setting up
+ IsProcessExists = func(pid int) bool {
+ return false
+ }
+
+ // wait for the instrumentation to initialize
+ <-time.After(1 * time.Second)
+
+ // the instrumentation instance is not existing
+ if !assertInstrumentationInstanceNotExisting(t, client, pod_id, 1) {
+ t.FailNow()
+ }
+ // the director stopped tracking the instrumentation
+ insts := dir.GetWorkloadInstrumentations(workload)
+ if !assert.Len(t, insts, 0) {
+ t.FailNow()
+ }
+}
+
func TestMultiplePodsInstrumentation(t *testing.T) {
ctx := context.Background()
scheme := runtime.NewScheme()
@@ -287,13 +350,13 @@ func TestMultiplePodsInstrumentation(t *testing.T) {
WithLists(&podList).
Build()
- origIsProcessExists := ebpf.IsProcessExists
- ebpf.IsProcessExists = func(pid int) bool {
+ origIsProcessExists := IsProcessExists
+ IsProcessExists = func(pid int) bool {
return true
}
- t.Cleanup(func() { ebpf.IsProcessExists = origIsProcessExists })
+ t.Cleanup(func() { IsProcessExists = origIsProcessExists })
- dir := newFakeDirector(ctx, client).(*ebpf.EbpfDirector[*FakeEbpfSdk])
+ dir := newFakeDirector(ctx, client, time.Millisecond).(*EbpfDirector[*FakeEbpfSdk])
for i := 0; i < numOfPods; i++ {
err := dir.Instrument(ctx, i+1, pod_ids[i], workload, "test-app", "test-container")
assert.NoError(t, err)
@@ -327,7 +390,7 @@ func TestMultiplePodsInstrumentation(t *testing.T) {
// the instrumentation instances are deleted
for i := 0; i < numOfPods - 1; i++ {
- if !assertInstrumentationInstanceDeleted(t, client, pod_ids[i], i+1) {
+ if !assertInstrumentationInstanceNotExisting(t, client, pod_ids[i], i+1) {
t.FailNow()
}
}
@@ -354,3 +417,14 @@ func TestMultiplePodsInstrumentation(t *testing.T) {
// The last instrumentation is the one returned
assert.Equal(t, insts[0].pid, numOfPods)
}
+
+func TestIsNil(t *testing.T) {
+ var e OtelEbpfSdk
+ assert.True(t, isNil(e))
+
+ e = &FakeEbpfSdk{}
+ assert.False(t, isNil(e))
+
+ var e2 *FakeEbpfSdk
+ assert.True(t, isNil(e2))
+}
diff --git a/opampserver/pkg/server/handlers.go b/opampserver/pkg/server/handlers.go
index ca4af1045..1d8092689 100644
--- a/opampserver/pkg/server/handlers.go
+++ b/opampserver/pkg/server/handlers.go
@@ -11,24 +11,35 @@ import (
"github.com/odigos-io/odigos/k8sutils/pkg/instrumentation_instance"
"github.com/odigos-io/odigos/k8sutils/pkg/workload"
"github.com/odigos-io/odigos/opampserver/pkg/connection"
- "github.com/odigos-io/odigos/opampserver/pkg/deviceid"
+ di "github.com/odigos-io/odigos/opampserver/pkg/deviceid"
"github.com/odigos-io/odigos/opampserver/pkg/sdkconfig"
"github.com/odigos-io/odigos/opampserver/pkg/sdkconfig/configresolvers"
"github.com/odigos-io/odigos/opampserver/protobufs"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/client-go/kubernetes"
"sigs.k8s.io/controller-runtime/pkg/client"
)
type ConnectionHandlers struct {
- deviceIdCache *deviceid.DeviceIdCache
+ deviceIdCache *di.DeviceIdCache
sdkConfig *sdkconfig.SdkConfigManager
logger logr.Logger
kubeclient client.Client
+ kubeClientSet *kubernetes.Clientset
scheme *runtime.Scheme // TODO: revisit this, we should not depend on controller runtime
nodeName string
}
+type opampAgentAttributesKeys struct {
+ ProgrammingLanguage string
+ ContainerName string
+ PodName string
+ Namespace string
+}
+
func (c *ConnectionHandlers) OnNewConnection(ctx context.Context, deviceId string, firstMessage *protobufs.AgentToServer) (*connection.ConnectionInfo, *protobufs.ServerToAgent, error) {
if firstMessage.AgentDescription == nil {
@@ -53,25 +64,19 @@ func (c *ConnectionHandlers) OnNewConnection(ctx context.Context, deviceId strin
return nil, nil, fmt.Errorf("missing pid in agent description")
}
- var programmingLanguage string
- for _, attr := range firstMessage.AgentDescription.IdentifyingAttributes {
- if attr.Key == string(semconv.TelemetrySDKLanguageKey) {
- programmingLanguage = attr.Value.GetStringValue()
- break
- }
- }
- if programmingLanguage == "" {
+ attrs := extractOpampAgentAttributes(firstMessage.AgentDescription)
+
+ if attrs.ProgrammingLanguage == "" {
return nil, nil, fmt.Errorf("missing programming language in agent description")
}
- k8sAttributes, pod, err := c.deviceIdCache.GetAttributesFromDevice(ctx, deviceId)
+ k8sAttributes, pod, err := c.resolveK8sAttributes(ctx, attrs, deviceId, c.logger)
if err != nil {
- c.logger.Error(err, "failed to get attributes from device", "deviceId", deviceId)
- return nil, nil, err
+ return nil, nil, fmt.Errorf("failed to process k8s attributes: %w", err)
}
podWorkload := workload.PodWorkload{
- Namespace: pod.GetNamespace(),
+ Namespace: k8sAttributes.Namespace,
Kind: workload.WorkloadKind(k8sAttributes.WorkloadKind),
Name: k8sAttributes.WorkloadName,
}
@@ -83,7 +88,7 @@ func (c *ConnectionHandlers) OnNewConnection(ctx context.Context, deviceId strin
return nil, nil, err
}
- fullRemoteConfig, err := c.sdkConfig.GetFullConfig(ctx, remoteResourceAttributes, &podWorkload, instrumentedAppName, programmingLanguage)
+ fullRemoteConfig, err := c.sdkConfig.GetFullConfig(ctx, remoteResourceAttributes, &podWorkload, instrumentedAppName, attrs.ProgrammingLanguage)
if err != nil {
c.logger.Error(err, "failed to get full config", "k8sAttributes", k8sAttributes)
return nil, nil, err
@@ -96,7 +101,7 @@ func (c *ConnectionHandlers) OnNewConnection(ctx context.Context, deviceId strin
Pod: pod,
ContainerName: k8sAttributes.ContainerName,
Pid: pid,
- ProgrammingLanguage: programmingLanguage,
+ ProgrammingLanguage: attrs.ProgrammingLanguage,
InstrumentedAppName: instrumentedAppName,
AgentRemoteConfig: fullRemoteConfig,
RemoteResourceAttributes: remoteResourceAttributes,
@@ -191,3 +196,72 @@ func (c *ConnectionHandlers) UpdateInstrumentationInstanceStatus(ctx context.Con
return nil
}
+
+// resolveK8sAttributes resolves K8s resource attributes using either direct attributes from opamp agent or device cache
+func (c *ConnectionHandlers) resolveK8sAttributes(ctx context.Context, attrs opampAgentAttributesKeys,
+ deviceId string, logger logr.Logger) (*di.K8sResourceAttributes, *corev1.Pod, error) {
+
+ if attrs.hasRequiredAttributes() {
+ podInfoResolver := di.NewK8sPodInfoResolver(logger, c.kubeClientSet)
+ return resolveFromDirectAttributes(ctx, attrs, podInfoResolver, c.kubeClientSet)
+ }
+ return c.deviceIdCache.GetAttributesFromDevice(ctx, deviceId)
+}
+
+func extractOpampAgentAttributes(agentDescription *protobufs.AgentDescription) opampAgentAttributesKeys {
+ result := opampAgentAttributesKeys{}
+
+ for _, attr := range agentDescription.IdentifyingAttributes {
+ switch attr.Key {
+ case string(semconv.TelemetrySDKLanguageKey):
+ result.ProgrammingLanguage = attr.Value.GetStringValue()
+ case string(semconv.K8SContainerNameKey):
+ result.ContainerName = attr.Value.GetStringValue()
+ case string(semconv.K8SPodNameKey):
+ result.PodName = attr.Value.GetStringValue()
+ case string(semconv.K8SNamespaceNameKey):
+ result.Namespace = attr.Value.GetStringValue()
+ }
+ }
+
+ return result
+}
+
+func (k opampAgentAttributesKeys) hasRequiredAttributes() bool {
+ return k.ContainerName != "" && k.PodName != "" && k.Namespace != ""
+}
+
+func resolveFromDirectAttributes(ctx context.Context, attrs opampAgentAttributesKeys,
+ podInfoResolver *di.K8sPodInfoResolver, kubeClient *kubernetes.Clientset) (*di.K8sResourceAttributes, *corev1.Pod, error) {
+
+ pod, err := kubeClient.CoreV1().Pods(attrs.Namespace).Get(ctx, attrs.PodName, metav1.GetOptions{})
+ if err != nil {
+ return nil, nil, err
+ }
+
+ var workloadName string
+ var workloadKind workload.WorkloadKind
+
+ ownerRefs := pod.GetOwnerReferences()
+ for _, ownerRef := range ownerRefs {
+ workloadName, workloadKind, err = workload.GetWorkloadFromOwnerReference(ownerRef)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to get workload from owner reference: %w", err)
+ }
+ }
+
+ serviceName := podInfoResolver.ResolveServiceName(ctx, workloadName, string(workloadKind), &di.ContainerDetails{
+ PodNamespace: attrs.Namespace,
+ })
+
+ k8sAttributes := &di.K8sResourceAttributes{
+ Namespace: attrs.Namespace,
+ PodName: attrs.PodName,
+ ContainerName: attrs.ContainerName,
+ WorkloadKind: string(workloadKind),
+ WorkloadName: workloadName,
+ OtelServiceName: serviceName,
+ }
+
+ return k8sAttributes, pod, nil
+}
diff --git a/opampserver/pkg/server/server.go b/opampserver/pkg/server/server.go
index 4d8f0fcfb..c8c6b0dbf 100644
--- a/opampserver/pkg/server/server.go
+++ b/opampserver/pkg/server/server.go
@@ -17,12 +17,12 @@ import (
ctrl "sigs.k8s.io/controller-runtime"
)
-func StartOpAmpServer(ctx context.Context, logger logr.Logger, mgr ctrl.Manager, kubeClient *kubernetes.Clientset, nodeName string, odigosNs string) error {
+func StartOpAmpServer(ctx context.Context, logger logr.Logger, mgr ctrl.Manager, kubeClientSet *kubernetes.Clientset, nodeName string, odigosNs string) error {
listenEndpoint := fmt.Sprintf("0.0.0.0:%d", OpAmpServerDefaultPort)
logger.Info("Starting opamp server", "listenEndpoint", listenEndpoint)
- deviceidCache, err := deviceid.NewDeviceIdCache(logger, kubeClient)
+ deviceidCache, err := deviceid.NewDeviceIdCache(logger, kubeClientSet)
if err != nil {
return err
}
@@ -36,6 +36,7 @@ func StartOpAmpServer(ctx context.Context, logger logr.Logger, mgr ctrl.Manager,
deviceIdCache: deviceidCache,
sdkConfig: sdkConfig,
kubeclient: mgr.GetClient(),
+ kubeClientSet: kubeClientSet,
scheme: mgr.GetScheme(),
nodeName: nodeName,
}
@@ -116,7 +117,11 @@ func StartOpAmpServer(ctx context.Context, logger logr.Logger, mgr ctrl.Manager,
}
if isAgentDisconnect {
- logger.Info("Agent disconnected", "workloadNamespace", connectionInfo.Workload.Namespace, "workloadName", connectionInfo.Workload.Name, "workloadKind", connectionInfo.Workload.Kind)
+
+ // This may occurs when Odiglet restarts, and a previously connected pod sends a disconnect message right after reconnecting.
+ if connectionInfo != nil {
+ logger.Info("Agent disconnected", "workloadNamespace", connectionInfo.Workload.Namespace, "workloadName", connectionInfo.Workload.Name, "workloadKind", connectionInfo.Workload.Kind)
+ }
// if agent disconnects, remove the connection from the cache
// as it is not expected to send additional messages
connectionCache.RemoveConnection(instanceUid)
diff --git a/tests/common/traceql_runner.sh b/tests/common/traceql_runner.sh
index a77d02f6d..df7eb129e 100755
--- a/tests/common/traceql_runner.sh
+++ b/tests/common/traceql_runner.sh
@@ -47,7 +47,6 @@ function process_yaml_file() {
# if num_of_traces not equal to expected_count
if [ "$num_of_traces" -ne "$expected_count" ]; then
echo "Test FAILED: expected $expected_count got $num_of_traces"
- echo "$response" | jq
exit 1
else
echo "Test PASSED"
diff --git a/tests/e2e/README.md b/tests/e2e/README.md
index c6c9bce1e..5948adbc2 100644
--- a/tests/e2e/README.md
+++ b/tests/e2e/README.md
@@ -133,8 +133,15 @@ When tests fail, and it's related to some traceql query not succeeding, it can b
- Install grafana with helm:
```bash
-helm install -n traces grafana grafana/grafana --set adminPassword='odigos'
-```
+helm install -n traces grafana grafana/grafana \
+ --set "env.GF_AUTH_ANONYMOUS_ENABLED=true" \
+ --set "env.GF_AUTH_ANONYMOUS_ORG_ROLE=Admin" \
+ --set "datasources.datasources\.yaml.apiVersion=1" \
+ --set "datasources.datasources\.yaml.datasources[0].name=Tempo" \
+ --set "datasources.datasources\.yaml.datasources[0].type=tempo" \
+ --set "datasources.datasources\.yaml.datasources[0].url=http://e2e-tests-tempo:3100" \
+ --set "datasources.datasources\.yaml.datasources[0].access=proxy" \
+ --set "datasources.datasources\.yaml.datasources[0].isDefault=true"```
- Port forward to the grafana service:
@@ -142,11 +149,9 @@ helm install -n traces grafana grafana/grafana --set adminPassword='odigos'
kubectl port-forward svc/grafana 3080:80 -n traces
```
-- Browse to `http://localhost:3080` and login with `admin` and `odigos`.
-
-- Add tempo as a datasource, by going to `Connections -> Data Sources -> Add data source` and selecting `Tempo` as the type. Set the URL to `http://e2e-tests-tempo:3100` and save.
+- Browse to `http://localhost:3080/explore`.
-- In grafana left side menu, go to `Explore` and select the tempo datasource. You can now write queries, run them, and see the traces that are stored in tempo to troubleshoot your test issues. example query:
+- You can now write queries, run them, and see the traces that are stored in tempo to troubleshoot your test issues. example query:
```
{resource.service.name = "coupon"}
diff --git a/tests/e2e/workload-lifecycle/01-assert-apps-installed.yaml b/tests/e2e/workload-lifecycle/01-assert-apps-installed.yaml
index c5c739814..35b3666b4 100644
--- a/tests/e2e/workload-lifecycle/01-assert-apps-installed.yaml
+++ b/tests/e2e/workload-lifecycle/01-assert-apps-installed.yaml
@@ -194,4 +194,82 @@ status:
restartCount: 0
started: true
phase: Running
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ labels:
+ app: python-latest-version
+ namespace: default
+spec:
+ containers:
+ - env:
+ - name: DJANGO_SETTINGS_MODULE
+ (value != null): true
+status:
+ containerStatuses:
+ - name: python-latest-version
+ ready: true
+ restartCount: 0
+ started: true
+ phase: Running
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ labels:
+ app: python-alpine
+ namespace: default
+spec:
+ containers:
+ - env:
+ - name: DJANGO_SETTINGS_MODULE
+ (value != null): true
+ - name: PYTHONPATH
+ (value != null): true
+status:
+ containerStatuses:
+ - name: python-alpine
+ ready: true
+ restartCount: 0
+ started: true
+ phase: Running
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ labels:
+ app: python-min-version
+ namespace: default
+spec:
+ containers:
+ - env:
+ - name: DJANGO_SETTINGS_MODULE
+ (value != null): true
+status:
+ containerStatuses:
+ - name: python-min-version
+ ready: true
+ restartCount: 0
+ started: true
+ phase: Running
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ labels:
+ app: python-not-supported
+ namespace: default
+spec:
+ containers:
+ - env:
+ - name: DJANGO_SETTINGS_MODULE
+ (value != null): true
+status:
+ containerStatuses:
+ - name: python-not-supported
+ ready: true
+ restartCount: 0
+ started: true
+ phase: Running
---
\ No newline at end of file
diff --git a/tests/e2e/workload-lifecycle/01-assert-instrumented.yaml b/tests/e2e/workload-lifecycle/01-assert-instrumented.yaml
index 3c5c69e53..527003a5f 100644
--- a/tests/e2e/workload-lifecycle/01-assert-instrumented.yaml
+++ b/tests/e2e/workload-lifecycle/01-assert-instrumented.yaml
@@ -273,4 +273,108 @@ status:
reason: InstrumentationDeviceApplied
status: "True"
type: AppliedInstrumentationDevice
----
\ No newline at end of file
+---
+apiVersion: odigos.io/v1alpha1
+kind: InstrumentedApplication
+metadata:
+ namespace: default
+ name: deployment-python-alpine
+spec:
+ runtimeDetails:
+ - containerName: python-alpine
+status:
+ conditions:
+ - message: "Instrumentation device applied successfully"
+ observedGeneration: 1
+ reason: InstrumentationDeviceApplied
+ status: "True"
+ type: AppliedInstrumentationDevice
+---
+apiVersion: odigos.io/v1alpha1
+kind: InstrumentedApplication
+metadata:
+ namespace: default
+ name: deployment-python-latest-version
+status:
+ conditions:
+ - message: "Instrumentation device applied successfully"
+ observedGeneration: 1
+ reason: InstrumentationDeviceApplied
+ status: "True"
+ type: AppliedInstrumentationDevice
+---
+apiVersion: odigos.io/v1alpha1
+kind: InstrumentedApplication
+metadata:
+ namespace: default
+ name: deployment-python-min-version
+status:
+ conditions:
+ - message: "Instrumentation device applied successfully"
+ observedGeneration: 1
+ reason: InstrumentationDeviceApplied
+ status: "True"
+ type: AppliedInstrumentationDevice
+---
+apiVersion: odigos.io/v1alpha1
+kind: InstrumentedApplication
+metadata:
+ namespace: default
+ name: deployment-python-not-supported
+status:
+ conditions:
+ - message: "python runtime version not supported by OpenTelemetry SDK. Found: 3.6.15, supports: 3.8.0"
+ observedGeneration: 1
+ reason: RuntimeVersionNotSupported
+ status: "False"
+ type: AppliedInstrumentationDevice
+
+
+---
+apiVersion: odigos.io/v1alpha1
+kind: InstrumentationInstance
+metadata:
+ namespace: default
+ labels:
+ instrumented-app: deployment-python-alpine
+status:
+ healthy: true
+ identifyingAttributes:
+ - key: service.instance.id
+ (value != null): true
+ - key: process.pid
+ (value != null): true
+ - key: telemetry.sdk.language
+ value: python
+---
+apiVersion: odigos.io/v1alpha1
+kind: InstrumentationInstance
+metadata:
+ namespace: default
+ labels:
+ instrumented-app: deployment-python-latest-version
+status:
+ healthy: true
+ identifyingAttributes:
+ - key: service.instance.id
+ (value != null): true
+ - key: process.pid
+ (value != null): true
+ - key: telemetry.sdk.language
+ value: python
+---
+apiVersion: odigos.io/v1alpha1
+kind: InstrumentationInstance
+metadata:
+ namespace: default
+ labels:
+ instrumented-app: deployment-python-min-version
+status:
+ healthy: true
+ identifyingAttributes:
+ - key: service.instance.id
+ (value != null): true
+ - key: process.pid
+ (value != null): true
+ - key: telemetry.sdk.language
+ value: python
\ No newline at end of file
diff --git a/tests/e2e/workload-lifecycle/01-assert-runtime-detected.yaml b/tests/e2e/workload-lifecycle/01-assert-runtime-detected.yaml
index 42a62b05f..bf0b38a33 100644
--- a/tests/e2e/workload-lifecycle/01-assert-runtime-detected.yaml
+++ b/tests/e2e/workload-lifecycle/01-assert-runtime-detected.yaml
@@ -248,4 +248,75 @@ spec:
runtimeDetails:
- containerName: java-old-version
language: java
----
\ No newline at end of file
+---
+## Python
+apiVersion: odigos.io/v1alpha1
+kind: InstrumentedApplication
+metadata:
+ name: deployment-python-alpine
+ namespace: default
+ ownerReferences:
+ - apiVersion: apps/v1
+ blockOwnerDeletion: true
+ controller: true
+ kind: Deployment
+ name: python-alpine
+spec:
+ runtimeDetails:
+ - containerName: python-alpine
+ envVars:
+ - name: PYTHONPATH
+ value: "/app"
+ language: python
+ runtimeVersion: 3.10.15
+---
+apiVersion: odigos.io/v1alpha1
+kind: InstrumentedApplication
+metadata:
+ name: deployment-python-latest-version
+ namespace: default
+ ownerReferences:
+ - apiVersion: apps/v1
+ blockOwnerDeletion: true
+ controller: true
+ kind: Deployment
+ name: python-latest-version
+spec:
+ runtimeDetails:
+ - containerName: python-latest-version
+ language: python
+ (runtimeVersion != null): true
+---
+apiVersion: odigos.io/v1alpha1
+kind: InstrumentedApplication
+metadata:
+ name: deployment-python-min-version
+ namespace: default
+ ownerReferences:
+ - apiVersion: apps/v1
+ blockOwnerDeletion: true
+ controller: true
+ kind: Deployment
+ name: python-min-version
+spec:
+ runtimeDetails:
+ - containerName: python-min-version
+ language: python
+ runtimeVersion: 3.8.0
+---
+apiVersion: odigos.io/v1alpha1
+kind: InstrumentedApplication
+metadata:
+ name: deployment-python-not-supported
+ namespace: default
+ ownerReferences:
+ - apiVersion: apps/v1
+ blockOwnerDeletion: true
+ controller: true
+ kind: Deployment
+ name: python-not-supported
+spec:
+ runtimeDetails:
+ - containerName: python-not-supported
+ language: python
+ runtimeVersion: 3.6.15
\ No newline at end of file
diff --git a/tests/e2e/workload-lifecycle/01-assert-workloads.yaml b/tests/e2e/workload-lifecycle/01-assert-workloads.yaml
index da7d3a206..be4488f08 100644
--- a/tests/e2e/workload-lifecycle/01-assert-workloads.yaml
+++ b/tests/e2e/workload-lifecycle/01-assert-workloads.yaml
@@ -428,4 +428,122 @@ status:
observedGeneration: 2
readyReplicas: 1
replicas: 1
+ updatedReplicas: 1
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ annotations:
+ deployment.kubernetes.io/revision: "2" # the deployment spec changed when odigos resource was added
+ generation: 2 # the deployment spec changed when odigos resource was added
+ labels:
+ app: python-latest-version
+ name: python-latest-version
+ namespace: default
+spec:
+ selector:
+ matchLabels:
+ app: python-latest-version
+ template:
+ spec:
+ containers:
+ - image: python-latest-version:v0.0.1
+ name: python-latest-version
+ resources:
+ limits:
+ instrumentation.odigos.io/python-native-community: "1"
+status:
+ availableReplicas: 1
+ observedGeneration: 2 # the deployment spec changed when odigos resource was added
+ readyReplicas: 1
+ replicas: 1
+ updatedReplicas: 1
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ annotations:
+ deployment.kubernetes.io/revision: "2" # the deployment spec changed when odigos resource was added
+ generation: 2 # the deployment spec changed when odigos resource was added
+ labels:
+ app: python-alpine
+ name: python-alpine
+ namespace: default
+spec:
+ selector:
+ matchLabels:
+ app: python-alpine
+ template:
+ spec:
+ containers:
+ - image: python-alpine:v0.0.1
+ name: python-alpine
+ resources:
+ limits:
+ instrumentation.odigos.io/python-native-community: "1"
+ env:
+ - name: DJANGO_SETTINGS_MODULE
+ value: "myapp.settings"
+ - name: PYTHONPATH
+ value: "/app:/var/odigos/python:/var/odigos/python/opentelemetry/instrumentation/auto_instrumentation"
+status:
+ availableReplicas: 1
+ observedGeneration: 2 # the deployment spec changed when odigos resource was added
+ readyReplicas: 1
+ replicas: 1
+ updatedReplicas: 1
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ annotations:
+ deployment.kubernetes.io/revision: "1"
+ generation: 1
+ labels:
+ app: python-not-supported
+ name: python-not-supported
+ namespace: default
+spec:
+ selector:
+ matchLabels:
+ app: python-not-supported
+ template:
+ spec:
+ containers:
+ - image: python-not-supported:v0.0.1
+ name: python-not-supported
+status:
+ availableReplicas: 1
+ observedGeneration: 1
+ readyReplicas: 1
+ replicas: 1
+ updatedReplicas: 1
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ annotations:
+ deployment.kubernetes.io/revision: "2" # the deployment spec changed when odigos resource was added
+ generation: 2 # the deployment spec changed when odigos resource was added
+ labels:
+ app: python-min-version
+ name: python-min-version
+ namespace: default
+spec:
+ selector:
+ matchLabels:
+ app: python-min-version
+ template:
+ spec:
+ containers:
+ - image: python-min-version:v0.0.1
+ name: python-min-version
+ resources:
+ limits:
+ instrumentation.odigos.io/python-native-community: "1"
+status:
+ availableReplicas: 1
+ observedGeneration: 2 # the deployment spec changed when odigos resource was added
+ readyReplicas: 1
+ replicas: 1
updatedReplicas: 1
\ No newline at end of file
diff --git a/tests/e2e/workload-lifecycle/01-generate-traffic.yaml b/tests/e2e/workload-lifecycle/01-generate-traffic.yaml
index 63c5ab05f..5aa7bd7af 100644
--- a/tests/e2e/workload-lifecycle/01-generate-traffic.yaml
+++ b/tests/e2e/workload-lifecycle/01-generate-traffic.yaml
@@ -29,11 +29,19 @@ spec:
curl -s --fail http://nodejs-latest-version:3000
curl -s --fail http://nodejs-dockerfile-env:3000
curl -s --fail http://nodejs-manifest-env:3000
+
curl -s --fail http://cpp-http-server:3000
+
curl -s --fail http://language-change:3000
+
curl -s --fail http://java-supported-version:3000
curl -s --fail http://java-supported-docker-env:3000
curl -s --fail http://java-azul:3000
curl -s --fail http://java-supported-manifest-env:3000
curl -s --fail http://java-latest-version:3000
curl -s --fail http://java-old-version:3000
+
+ curl -s --fail http://python-alpine:3000/insert-random/
+ curl -s --fail http://python-latest-version:3000/insert-random/
+ curl -s --fail http://python-min-version:3000/insert-random/
+ curl -s --fail http://python-not-supported:3000/insert-random/
diff --git a/tests/e2e/workload-lifecycle/01-install-test-apps.yaml b/tests/e2e/workload-lifecycle/01-install-test-apps.yaml
index 1257a17b4..c91317dfb 100644
--- a/tests/e2e/workload-lifecycle/01-install-test-apps.yaml
+++ b/tests/e2e/workload-lifecycle/01-install-test-apps.yaml
@@ -521,4 +521,197 @@ spec:
ports:
- protocol: TCP
port: 3000
----
\ No newline at end of file
+---
+kind: Deployment
+apiVersion: apps/v1
+metadata:
+ name: python-latest-version
+ namespace: default
+ labels:
+ app: python-latest-version
+spec:
+ selector:
+ matchLabels:
+ app: python-latest-version
+ template:
+ metadata:
+ labels:
+ app: python-latest-version
+ spec:
+ containers:
+ - name: python-latest-version
+ image: python-latest-version:v0.0.1
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 8000
+ env:
+ - name: DJANGO_SETTINGS_MODULE
+ value: "myapp.settings"
+ livenessProbe:
+ httpGet:
+ path: /health/
+ port: 8000
+ initialDelaySeconds: 10
+ periodSeconds: 30
+ timeoutSeconds: 5
+ failureThreshold: 3
+ successThreshold: 1
+---
+kind: Service
+apiVersion: v1
+metadata:
+ name: python-latest-version
+ namespace: default
+spec:
+ selector:
+ app: python-latest-version
+ ports:
+ - protocol: TCP
+ port: 3000
+ targetPort: 8000
+---
+kind: Deployment
+apiVersion: apps/v1
+metadata:
+ name: python-alpine
+ namespace: default
+ labels:
+ app: python-alpine
+spec:
+ selector:
+ matchLabels:
+ app: python-alpine
+ template:
+ metadata:
+ labels:
+ app: python-alpine
+ spec:
+ containers:
+ - name: python-alpine
+ image: python-alpine:v0.0.1
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 8000
+ env:
+ - name: DJANGO_SETTINGS_MODULE
+ value: "myapp.settings"
+ - name: PYTHONPATH
+ value: "/app"
+ livenessProbe:
+ httpGet:
+ path: /health/
+ port: 8000
+ initialDelaySeconds: 10
+ periodSeconds: 30
+ timeoutSeconds: 5
+ failureThreshold: 3
+ successThreshold: 1
+---
+kind: Service
+apiVersion: v1
+metadata:
+ name: python-alpine
+ namespace: default
+spec:
+ selector:
+ app: python-alpine
+ ports:
+ - protocol: TCP
+ port: 3000
+ targetPort: 8000
+---
+kind: Deployment
+apiVersion: apps/v1
+metadata:
+ name: python-not-supported
+ namespace: default
+ labels:
+ app: python-not-supported
+spec:
+ selector:
+ matchLabels:
+ app: python-not-supported
+ template:
+ metadata:
+ labels:
+ app: python-not-supported
+ spec:
+ containers:
+ - name: python-not-supported
+ image: python-not-supported:v0.0.1
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 8000
+ env:
+ - name: DJANGO_SETTINGS_MODULE
+ value: "myapp.settings"
+ livenessProbe:
+ httpGet:
+ path: /health/
+ port: 8000
+ initialDelaySeconds: 10
+ periodSeconds: 30
+ timeoutSeconds: 5
+ failureThreshold: 3
+ successThreshold: 1
+---
+kind: Service
+apiVersion: v1
+metadata:
+ name: python-not-supported
+ namespace: default
+spec:
+ selector:
+ app: python-not-supported
+ ports:
+ - protocol: TCP
+ port: 3000
+ targetPort: 8000
+---
+kind: Deployment
+apiVersion: apps/v1
+metadata:
+ name: python-min-version
+ namespace: default
+ labels:
+ app: python-min-version
+spec:
+ selector:
+ matchLabels:
+ app: python-min-version
+ template:
+ metadata:
+ labels:
+ app: python-min-version
+ spec:
+ containers:
+ - name: python-min-version
+ image: python-min-version:v0.0.1
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 8000
+ env:
+ - name: DJANGO_SETTINGS_MODULE
+ value: "myapp.settings"
+ livenessProbe:
+ httpGet:
+ path: /health/
+ port: 8000
+ initialDelaySeconds: 10
+ periodSeconds: 30
+ timeoutSeconds: 5
+ failureThreshold: 3
+ successThreshold: 1
+---
+kind: Service
+apiVersion: v1
+metadata:
+ name: python-min-version
+ namespace: default
+spec:
+ selector:
+ app: python-min-version
+ ports:
+ - protocol: TCP
+ port: 3000
+ targetPort: 8000
\ No newline at end of file
diff --git a/tests/e2e/workload-lifecycle/01-wait-for-trace.yaml b/tests/e2e/workload-lifecycle/01-wait-for-trace.yaml
index 19a88c112..fedeb6d17 100644
--- a/tests/e2e/workload-lifecycle/01-wait-for-trace.yaml
+++ b/tests/e2e/workload-lifecycle/01-wait-for-trace.yaml
@@ -12,6 +12,10 @@ query: |
{ resource.service.name = "java-old-version" } ||
{ resource.service.name = "java-supported-docker-env" } ||
{ resource.service.name = "java-supported-manifest-env" } ||
- { resource.service.name = "java-azul" }
+ { resource.service.name = "java-azul" } ||
+ { resource.service.name = "python-latest-version" && span.http.route = "insert-random/" } ||
+ { resource.service.name = "python-alpine" && span.http.route = "insert-random/" } ||
+ { resource.service.name = "python-not-supported" && span.http.route = "insert-random/" } ||
+ { resource.service.name = "python-min-version" && span.http.route = "insert-random/" }
expected:
- count: 11
+ count: 14
diff --git a/tests/e2e/workload-lifecycle/02-assert-workload-update.yaml b/tests/e2e/workload-lifecycle/02-assert-workload-update.yaml
index 98294993e..345692fb7 100644
--- a/tests/e2e/workload-lifecycle/02-assert-workload-update.yaml
+++ b/tests/e2e/workload-lifecycle/02-assert-workload-update.yaml
@@ -375,4 +375,110 @@ status:
observedGeneration: 4
readyReplicas: 1
replicas: 1
+ updatedReplicas: 1
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ generation: 4 # on step 2, the manifest was updated (1->2)
+ labels:
+ app: python-latest-version
+ name: python-latest-version
+ namespace: default
+spec:
+ selector:
+ matchLabels:
+ app: python-latest-version
+ template:
+ spec:
+ containers:
+ - image: python-latest-version:v0.0.1
+ name: python-latest-version
+ resources:
+ limits:
+ instrumentation.odigos.io/python-native-community: "1"
+status:
+ availableReplicas: 1
+ observedGeneration: 4
+ readyReplicas: 1
+ replicas: 1
+ updatedReplicas: 1
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ generation: 4 # on step 2, the manifest was updated (1->2)
+ labels:
+ app: python-alpine
+ name: python-alpine
+ namespace: default
+spec:
+ selector:
+ matchLabels:
+ app: python-alpine
+ template:
+ spec:
+ containers:
+ - image: python-alpine:v0.0.1
+ name: python-alpine
+ resources:
+ limits:
+ instrumentation.odigos.io/python-native-community: "1"
+status:
+ availableReplicas: 1
+ observedGeneration: 4
+ readyReplicas: 1
+ replicas: 1
+ updatedReplicas: 1
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ generation: 4 # on step 2, the manifest was updated (1->2)
+ labels:
+ app: python-min-version
+ name: python-min-version
+ namespace: default
+spec:
+ selector:
+ matchLabels:
+ app: python-min-version
+ template:
+ spec:
+ containers:
+ - image: python-min-version:v0.0.1
+ name: python-min-version
+ resources:
+ limits:
+ instrumentation.odigos.io/python-native-community: "1"
+status:
+ availableReplicas: 1
+ observedGeneration: 4
+ readyReplicas: 1
+ replicas: 1
+ updatedReplicas: 1
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ generation: 2 # on step 2, the manifest was updated (1->2)
+ labels:
+ app: python-not-supported
+ name: python-not-supported
+ namespace: default
+spec:
+ selector:
+ matchLabels:
+ app: python-not-supported
+ template:
+ spec:
+ containers:
+ - image: python-not-supported:v0.0.1
+ name: python-not-supported
+ resources: {}
+status:
+ availableReplicas: 1
+ observedGeneration: 2
+ readyReplicas: 1
+ replicas: 1
updatedReplicas: 1
\ No newline at end of file
diff --git a/tests/e2e/workload-lifecycle/02-update-workload-manifests.yaml b/tests/e2e/workload-lifecycle/02-update-workload-manifests.yaml
index b34785929..062681ba4 100644
--- a/tests/e2e/workload-lifecycle/02-update-workload-manifests.yaml
+++ b/tests/e2e/workload-lifecycle/02-update-workload-manifests.yaml
@@ -376,4 +376,153 @@ spec:
readinessProbe:
tcpSocket:
port: 3000
- initialDelaySeconds: 20
\ No newline at end of file
+ initialDelaySeconds: 20
+---
+kind: Deployment
+apiVersion: apps/v1
+metadata:
+ name: python-latest-version
+ namespace: default
+ labels:
+ app: python-latest-version
+spec:
+ selector:
+ matchLabels:
+ app: python-latest-version
+ template:
+ metadata:
+ labels:
+ app: python-latest-version
+ annotations:
+ odigos-test-step: "2"
+ spec:
+ containers:
+ - name: python-latest-version
+ image: python-latest-version:v0.0.1
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 8000
+ env:
+ - name: DJANGO_SETTINGS_MODULE
+ value: "myapp.settings"
+ livenessProbe:
+ httpGet:
+ path: /health/
+ port: 8000
+ initialDelaySeconds: 10
+ periodSeconds: 30
+ timeoutSeconds: 5
+ failureThreshold: 3
+ successThreshold: 1
+---
+kind: Deployment
+apiVersion: apps/v1
+metadata:
+ name: python-alpine
+ namespace: default
+ labels:
+ app: python-alpine
+spec:
+ selector:
+ matchLabels:
+ app: python-alpine
+ template:
+ metadata:
+ labels:
+ app: python-alpine
+ annotations:
+ odigos-test-step: "2"
+ spec:
+ containers:
+ - name: python-alpine
+ image: python-alpine:v0.0.1
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 8000
+ env:
+ - name: DJANGO_SETTINGS_MODULE
+ value: "myapp.settings"
+ livenessProbe:
+ httpGet:
+ path: /health/
+ port: 8000
+ initialDelaySeconds: 10
+ periodSeconds: 30
+ timeoutSeconds: 5
+ failureThreshold: 3
+ successThreshold: 1
+---
+kind: Deployment
+apiVersion: apps/v1
+metadata:
+ name: python-not-supported
+ namespace: default
+ labels:
+ app: python-not-supported
+spec:
+ selector:
+ matchLabels:
+ app: python-not-supported
+ template:
+ metadata:
+ labels:
+ app: python-not-supported
+ annotations:
+ odigos-test-step: "2"
+ spec:
+ containers:
+ - name: python-not-supported
+ image: python-not-supported:v0.0.1
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 8000
+ env:
+ - name: DJANGO_SETTINGS_MODULE
+ value: "myapp.settings"
+ livenessProbe:
+ httpGet:
+ path: /health/
+ port: 8000
+ initialDelaySeconds: 10
+ periodSeconds: 30
+ timeoutSeconds: 5
+ failureThreshold: 3
+ successThreshold: 1
+---
+kind: Deployment
+apiVersion: apps/v1
+metadata:
+ name: python-min-version
+ namespace: default
+ labels:
+ app: python-min-version
+spec:
+ selector:
+ matchLabels:
+ app: python-min-version
+ template:
+ metadata:
+ labels:
+ app: python-min-version
+ annotations:
+ odigos-test-step: "2"
+ spec:
+ containers:
+ - name: python-min-version
+ image: python-min-version:v0.0.1
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 8000
+ env:
+ - name: DJANGO_SETTINGS_MODULE
+ value: "myapp.settings"
+ livenessProbe:
+ httpGet:
+ path: /health/
+ port: 8000
+ initialDelaySeconds: 10
+ periodSeconds: 30
+ timeoutSeconds: 5
+ failureThreshold: 3
+ successThreshold: 1
+---
diff --git a/tests/e2e/workload-lifecycle/02-wait-for-trace.yaml b/tests/e2e/workload-lifecycle/02-wait-for-trace.yaml
index a984e8109..2b25b6e35 100644
--- a/tests/e2e/workload-lifecycle/02-wait-for-trace.yaml
+++ b/tests/e2e/workload-lifecycle/02-wait-for-trace.yaml
@@ -12,6 +12,10 @@ query: |
{ resource.service.name = "java-old-version" } ||
{ resource.service.name = "java-supported-docker-env" } ||
{ resource.service.name = "java-supported-manifest-env" } ||
- { resource.service.name = "java-azul" }
+ { resource.service.name = "java-azul" } ||
+ { resource.service.name = "python-latest-version" && span.http.route = "insert-random/" } ||
+ { resource.service.name = "python-alpine" && span.http.route = "insert-random/" } ||
+ { resource.service.name = "python-not-supported" && span.http.route = "insert-random/" } ||
+ { resource.service.name = "python-min-version" && span.http.route = "insert-random/" }
expected:
- count: 21 # 11 before +10 new ones
+ count: 27 # 14 before +13 new ones
diff --git a/tests/e2e/workload-lifecycle/README.md b/tests/e2e/workload-lifecycle/README.md
index e51cd0123..d8336a5d6 100644
--- a/tests/e2e/workload-lifecycle/README.md
+++ b/tests/e2e/workload-lifecycle/README.md
@@ -78,6 +78,28 @@ This e2e test verify various scenarios related to the lifecycle of workloads in
- Application uses JAVA_OPTS environment variable in the k8s deployment manifest.
- This workload verifies that after instrumentation is applied, those 2 options still works as expected.
+## Python Workloads
+
+### python-latest-version
+- Runs on the latest Python version.
+- The instrumentation device should be added, and the agent must load and report traces as expected.
+- We ensure that odigos-opentelemetry-python does not conflict with or overwrite the application's dependencies.
+
+### python-alpine
+- Runs Python 3.10 on the Alpine Linux distribution.
+- The instrumentation device should be added, and the agent must load and report traces as expected.
+- We ensure that odigos-opentelemetry-python does not conflict with or overwrite the application's dependencies.
+- The application utilizes the `PYTHONPATH` environment variable in the Kubernetes deployment manifest.
+
+### python-minimum-supported-version
+- Runs Python 3.8 (minimum supported version).
+- The instrumentation device should be added, and the agent must load and report traces as expected.
+- We ensure that odigos-opentelemetry-python does not conflict with or overwrite the application's dependencies.
+
+### python-not-supported-version
+- Runs Python 3.6.
+- This version is not supported for instrumentation.
+- We ensure that odigos-opentelemetry-python does not conflict with or overwrite the application's dependencies.
## CPP Workloads
diff --git a/tests/e2e/workload-lifecycle/chainsaw-test.yaml b/tests/e2e/workload-lifecycle/chainsaw-test.yaml
index e2f229572..027df8308 100644
--- a/tests/e2e/workload-lifecycle/chainsaw-test.yaml
+++ b/tests/e2e/workload-lifecycle/chainsaw-test.yaml
@@ -9,7 +9,7 @@ spec:
- name: Build and Load Test App Images
try:
- script:
- timeout: 200s
+ timeout: 350s
content: |
docker build -t nodejs-unsupported-version:v0.0.1 -f services/nodejs-http-server/unsupported-version.Dockerfile services/nodejs-http-server
kind load docker-image nodejs-unsupported-version:v0.0.1
@@ -23,8 +23,10 @@ spec:
kind load docker-image nodejs-dockerfile-env:v0.0.1
docker build -t nodejs-manifest-env:v0.0.1 -f services/nodejs-http-server/manifest-env.Dockerfile services/nodejs-http-server
kind load docker-image nodejs-manifest-env:v0.0.1
+
docker build -t cpp-http-server:v0.0.1 -f services/cpp-http-server/Dockerfile services/cpp-http-server
kind load docker-image cpp-http-server:v0.0.1
+
docker build -t java-supported-version:v0.0.1 -f services/java-http-server/java-supported-version.Dockerfile services/java-http-server
kind load docker-image java-supported-version:v0.0.1
docker build -t java-azul:v0.0.1 -f services/java-http-server/java-azul.Dockerfile services/java-http-server
@@ -38,6 +40,15 @@ spec:
docker build -t java-old-version:v0.0.1 -f services/java-http-server/java-old-version.Dockerfile services/java-http-server
kind load docker-image java-old-version:v0.0.1
+ docker build -t python-latest-version:v0.0.1 -f services/python-http-server/Dockerfile.python-latest services/python-http-server
+ kind load docker-image python-latest-version:v0.0.1
+ docker build -t python-alpine:v0.0.1 -f services/python-http-server/Dockerfile.python-alpine services/python-http-server
+ kind load docker-image python-alpine:v0.0.1
+ docker build -t python-not-supported:v0.0.1 -f services/python-http-server/Dockerfile.python-not-supported-version services/python-http-server
+ kind load docker-image python-not-supported:v0.0.1
+ docker build -t python-min-version:v0.0.1 -f services/python-http-server/Dockerfile.python-min-version services/python-http-server
+ kind load docker-image python-min-version:v0.0.1
+
- name: Prepare destination
try:
- script:
@@ -69,12 +80,19 @@ spec:
try:
- apply:
file: 01-install-test-apps.yaml
+
+ - name: '01 - Assert Apps installed'
+ try:
- assert:
file: 01-assert-apps-installed.yaml
+
- name: '01 Instrument Namespaces'
- try:
+ try:
- apply:
file: 01-instrument-ns.yaml
+
+ - name: '01 Assert runtime detection'
+ try:
- assert:
file: 01-assert-runtime-detected.yaml
@@ -82,15 +100,18 @@ spec:
try:
- apply:
file: 01-add-destination.yaml
- - name: Assert Pipeline
+
+ - name: "01 Assert Pipeline"
try:
- - assert:
- file: 01-assert-pipeline.yaml
- - name: Assert instrumented
+ - assert:
+ file: 01-assert-pipeline.yaml
+
+ - name: "01 Assert Instrumented"
try:
- assert:
file: 01-assert-instrumented.yaml
- - name: Assert workload
+
+ - name: "01 Assert Workloads"
try:
- assert:
file: 01-assert-workloads.yaml
@@ -100,7 +121,7 @@ spec:
# send traffic to all services to verify they are working as expected and verify traces for instrumented ones
try:
- script:
- timeout: 60s
+ timeout: 200s
content: |
set -e
@@ -163,7 +184,7 @@ spec:
- name: '02 - Generate Traffic'
try:
- script:
- timeout: 60s
+ timeout: 200s
content: |
set -e
@@ -191,8 +212,9 @@ spec:
- script:
timeout: 60s
content: |
- sleep 20
+ sleep 20
+
while true; do
../../common/traceql_runner.sh 02-wait-for-trace.yaml
if [ $? -eq 0 ]; then
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/Dockerfile.python-alpine b/tests/e2e/workload-lifecycle/services/python-http-server/Dockerfile.python-alpine
new file mode 100644
index 000000000..45d9ec9f1
--- /dev/null
+++ b/tests/e2e/workload-lifecycle/services/python-http-server/Dockerfile.python-alpine
@@ -0,0 +1,21 @@
+# Dockerfile.alpine
+FROM python:3.10-alpine
+WORKDIR /app
+COPY . /app
+RUN pip install --no-cache-dir django
+
+# Install dependencies from requirements.txt
+COPY requirements.txt /app/requirements.txt
+
+RUN pip install --no-cache-dir -r requirements.txt
+
+# Supress health check endpoint from tracing
+ENV OTEL_PYTHON_DJANGO_EXCLUDED_URLS=health/
+
+RUN apk add sqlite
+
+COPY entrypoint.sh /entrypoint.sh
+
+RUN chmod +x /entrypoint.sh
+
+ENTRYPOINT ["/entrypoint.sh"]
\ No newline at end of file
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/Dockerfile.python-latest b/tests/e2e/workload-lifecycle/services/python-http-server/Dockerfile.python-latest
new file mode 100644
index 000000000..97fe5125f
--- /dev/null
+++ b/tests/e2e/workload-lifecycle/services/python-http-server/Dockerfile.python-latest
@@ -0,0 +1,20 @@
+FROM python
+
+WORKDIR /app
+COPY . /app
+
+# Install dependencies from requirements.txt
+COPY requirements.txt /app/requirements.txt
+
+RUN apt-get update && apt-get install sqlite3 -y
+
+RUN pip install --no-cache-dir -r requirements.txt
+
+# Supress health check endpoint from tracing
+ENV OTEL_PYTHON_DJANGO_EXCLUDED_URLS=health/
+
+COPY entrypoint.sh /entrypoint.sh
+
+RUN chmod +x /entrypoint.sh
+
+ENTRYPOINT ["/entrypoint.sh"]
\ No newline at end of file
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/Dockerfile.python-min-version b/tests/e2e/workload-lifecycle/services/python-http-server/Dockerfile.python-min-version
new file mode 100644
index 000000000..f26428354
--- /dev/null
+++ b/tests/e2e/workload-lifecycle/services/python-http-server/Dockerfile.python-min-version
@@ -0,0 +1,21 @@
+# Dockerfile.python38-slim
+FROM python:3.8.0-slim
+WORKDIR /app
+COPY . /app
+
+# Install dependencies from requirements.txt
+COPY requirements.txt /app/requirements.txt
+# Install build tools for compiling packages with C extensions
+RUN apt-get update && apt-get install -y \
+ build-essential \
+ gcc \
+ sqlite3
+
+RUN pip install --no-cache-dir -r requirements.txt
+
+# Supress health check endpoint from tracing
+ENV OTEL_PYTHON_DJANGO_EXCLUDED_URLS=health/
+
+COPY entrypoint.sh /entrypoint.sh
+RUN chmod +x /entrypoint.sh
+ENTRYPOINT ["/entrypoint.sh"]
\ No newline at end of file
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/Dockerfile.python-not-supported-version b/tests/e2e/workload-lifecycle/services/python-http-server/Dockerfile.python-not-supported-version
new file mode 100644
index 000000000..3698c02a8
--- /dev/null
+++ b/tests/e2e/workload-lifecycle/services/python-http-server/Dockerfile.python-not-supported-version
@@ -0,0 +1,17 @@
+# Dockerfile.python37-slim
+FROM python:3.6-slim
+WORKDIR /app
+COPY . /app
+
+RUN apt-get update && apt-get install sqlite3 -y
+
+RUN pip install --no-cache-dir -r requirements-legacy.txt
+
+# Supress health check endpoint from tracing
+ENV OTEL_PYTHON_DJANGO_EXCLUDED_URLS=health/
+
+COPY entrypoint.sh /entrypoint.sh
+
+RUN chmod +x /entrypoint.sh
+
+ENTRYPOINT ["/entrypoint.sh"]
\ No newline at end of file
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/db.sqlite3 b/tests/e2e/workload-lifecycle/services/python-http-server/db.sqlite3
new file mode 100644
index 000000000..e41a46532
Binary files /dev/null and b/tests/e2e/workload-lifecycle/services/python-http-server/db.sqlite3 differ
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/entrypoint.sh b/tests/e2e/workload-lifecycle/services/python-http-server/entrypoint.sh
new file mode 100755
index 000000000..f6f72d4f9
--- /dev/null
+++ b/tests/e2e/workload-lifecycle/services/python-http-server/entrypoint.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+# entrypoint.sh
+
+set -e
+# Run Django database migrations
+echo "Running migrations..."
+python manage.py migrate
+
+# Start the Django development server
+echo "Starting server..."
+exec python manage.py runserver 0.0.0.0:8000 --noreload
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/manage.py b/tests/e2e/workload-lifecycle/services/python-http-server/manage.py
new file mode 100755
index 000000000..a53204a11
--- /dev/null
+++ b/tests/e2e/workload-lifecycle/services/python-http-server/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+ """Run administrative tasks."""
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myapp.settings')
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/myapp/__init__.py b/tests/e2e/workload-lifecycle/services/python-http-server/myapp/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/myapp/asgi.py b/tests/e2e/workload-lifecycle/services/python-http-server/myapp/asgi.py
new file mode 100644
index 000000000..5221a56c7
--- /dev/null
+++ b/tests/e2e/workload-lifecycle/services/python-http-server/myapp/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for myapp project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myapp.settings')
+
+application = get_asgi_application()
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/myapp/settings.py b/tests/e2e/workload-lifecycle/services/python-http-server/myapp/settings.py
new file mode 100644
index 000000000..0176f41f4
--- /dev/null
+++ b/tests/e2e/workload-lifecycle/services/python-http-server/myapp/settings.py
@@ -0,0 +1,164 @@
+"""
+Django settings for myapp project.
+
+Generated by 'django-admin startproject' using Django 5.1.1.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.1/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/5.1/ref/settings/
+"""
+
+from pathlib import Path
+import os
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = 'django-insecure-6ua&dv4pj*%-vfxn1r62&n^_!&$x&ovn_xtom!h@_54j8os56+'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+APPEND_SLASH = False
+
+
+ALLOWED_HOSTS = ['*']
+
+# Application definition
+INSTALLED_APPS = [
+ 'django.contrib.admin',
+ 'myapp_core',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ 'health_check'
+]
+
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'myapp.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'myapp.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': BASE_DIR / 'db.sqlite3',
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/5.1/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/5.1/howto/static-files/
+
+STATIC_URL = 'static/'
+
+# Default primary key field type
+# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
+
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+
+
+
+# settings.py (default for SQLite)
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': BASE_DIR / 'db.sqlite3',
+ }
+}
+
+# settings.py
+LOGGING = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'handlers': {
+ 'console': {
+ 'level': 'DEBUG',
+ 'class': 'logging.StreamHandler',
+ },
+ },
+ 'loggers': {
+ 'opentelemetry.instrumentation.django': {
+ 'handlers': ['console'],
+ 'level': 'DEBUG',
+ 'propagate': False,
+ },
+ 'django': {
+ 'handlers': ['console'],
+ 'level': 'DEBUG',
+ 'propagate': False,
+ },
+ },
+}
+
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/myapp/urls.py b/tests/e2e/workload-lifecycle/services/python-http-server/myapp/urls.py
new file mode 100644
index 000000000..8b02b4c7c
--- /dev/null
+++ b/tests/e2e/workload-lifecycle/services/python-http-server/myapp/urls.py
@@ -0,0 +1,11 @@
+# myapp/urls.py
+from django.contrib import admin
+from django.urls import path, include
+from myapp_core import views
+
+urlpatterns = [
+ path('admin/', admin.site.urls),
+ path('insert-random/', views.insert_random_row, name='insert-random'),
+ path('fetch-all/', views.fetch_all_rows, name='fetch-all'),
+ path('health/', include('health_check.urls')),
+]
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/myapp/views.py b/tests/e2e/workload-lifecycle/services/python-http-server/myapp/views.py
new file mode 100644
index 000000000..a023127a7
--- /dev/null
+++ b/tests/e2e/workload-lifecycle/services/python-http-server/myapp/views.py
@@ -0,0 +1,4 @@
+from django.http import HttpResponse
+
+def index(request):
+ return HttpResponse("Hello, this is a test app!")
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/myapp/wsgi.py b/tests/e2e/workload-lifecycle/services/python-http-server/myapp/wsgi.py
new file mode 100644
index 000000000..4b4a883b1
--- /dev/null
+++ b/tests/e2e/workload-lifecycle/services/python-http-server/myapp/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for myapp project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myapp.settings')
+
+application = get_wsgi_application()
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/__init__.py b/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/admin.py b/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/admin.py
new file mode 100644
index 000000000..8c38f3f3d
--- /dev/null
+++ b/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/apps.py b/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/apps.py
new file mode 100644
index 000000000..2a92c0b52
--- /dev/null
+++ b/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class MyappCoreConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'myapp_core'
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/migrations/0001_initial.py b/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/migrations/0001_initial.py
new file mode 100644
index 000000000..a7b7e05c1
--- /dev/null
+++ b/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/migrations/0001_initial.py
@@ -0,0 +1,22 @@
+# Generated by Django 5.1.1 on 2024-09-24 13:48
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ExampleModel',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=100)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ],
+ ),
+ ]
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/migrations/__init__.py b/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/models.py b/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/models.py
new file mode 100644
index 000000000..938b059d2
--- /dev/null
+++ b/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/models.py
@@ -0,0 +1,9 @@
+# myapp_core/models.py
+from django.db import models
+
+class ExampleModel(models.Model):
+ name = models.CharField(max_length=100)
+ created_at = models.DateTimeField(auto_now_add=True)
+
+ def __str__(self):
+ return self.name
\ No newline at end of file
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/proto/__init__.py b/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/proto/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/proto/example.proto b/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/proto/example.proto
new file mode 100644
index 000000000..2bd123fda
--- /dev/null
+++ b/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/proto/example.proto
@@ -0,0 +1,9 @@
+syntax = "proto3";
+
+package myapp_core;
+
+message ExampleMessage {
+ string name = 1;
+ int32 id = 2;
+ string created_at = 3;
+}
\ No newline at end of file
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/proto/example_pb2.py b/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/proto/example_pb2.py
new file mode 100644
index 000000000..c7c469506
--- /dev/null
+++ b/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/proto/example_pb2.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: example.proto
+# Protobuf Python Version: 5.28.1
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import runtime_version as _runtime_version
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+_runtime_version.ValidateProtobufRuntimeVersion(
+ _runtime_version.Domain.PUBLIC,
+ 5,
+ 28,
+ 1,
+ '',
+ 'example.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rexample.proto\x12\nmyapp_core\">\n\x0e\x45xampleMessage\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\n\n\x02id\x18\x02 \x01(\x05\x12\x12\n\ncreated_at\x18\x03 \x01(\tb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'example_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_EXAMPLEMESSAGE']._serialized_start=29
+ _globals['_EXAMPLEMESSAGE']._serialized_end=91
+# @@protoc_insertion_point(module_scope)
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/proto/example_pb2_legacy.py b/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/proto/example_pb2_legacy.py
new file mode 100644
index 000000000..7c16727b9
--- /dev/null
+++ b/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/proto/example_pb2_legacy.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: proto/example.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13proto/example.proto\x12\nmyapp_core\">\n\x0e\x45xampleMessage\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\n\n\x02id\x18\x02 \x01(\x05\x12\x12\n\ncreated_at\x18\x03 \x01(\tb\x06proto3')
+
+
+
+_EXAMPLEMESSAGE = DESCRIPTOR.message_types_by_name['ExampleMessage']
+ExampleMessage = _reflection.GeneratedProtocolMessageType('ExampleMessage', (_message.Message,), {
+ 'DESCRIPTOR' : _EXAMPLEMESSAGE,
+ '__module__' : 'proto.example_pb2'
+ # @@protoc_insertion_point(class_scope:myapp_core.ExampleMessage)
+ })
+_sym_db.RegisterMessage(ExampleMessage)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ _EXAMPLEMESSAGE._serialized_start=35
+ _EXAMPLEMESSAGE._serialized_end=97
+# @@protoc_insertion_point(module_scope)
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/views.py b/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/views.py
new file mode 100644
index 000000000..38babd542
--- /dev/null
+++ b/tests/e2e/workload-lifecycle/services/python-http-server/myapp_core/views.py
@@ -0,0 +1,101 @@
+# myapp_core/views.py
+import random
+from django.http import HttpResponse
+from .models import ExampleModel
+import logging
+from packaging.version import parse, Version
+import importlib_metadata
+import asyncio
+from asgiref.sync import async_to_sync
+
+import sys
+
+
+# Import the protobuf message based on the Python version
+# In our min version test (3.6) protobuf 3.19.6 is used
+if sys.version_info < (3, 8):
+ from .proto import example_pb2_legacy as example_pb2
+else:
+ from .proto import example_pb2
+ import google.protobuf
+ # Assert python protobuf version is the same as pinned in the requirements.txt
+ if google.protobuf.__version__ != '5.28.2':
+ raise ImportError(f"Expected protobuf version 5.28.2, got {google.protobuf.__version__}")
+
+logger = logging.getLogger()
+
+
+async def async_greeting():
+ await asyncio.sleep(1)
+ return "Hello from async!"
+
+
+def insert_random_row(request):
+ try:
+ # Uses some 3rd packages that conflict with odigos-opentelemetry-python
+ # These few lines wont have business logic, but just to try to reproduce the conflicts
+
+ ### importlib_metadata
+ package_name = "Django"
+ metadata = importlib_metadata.metadata(package_name)
+ django_version = metadata['Version']
+ logger.info(f"Using Django version: {django_version}")
+
+ ### packaging
+ v1 = parse("3.1.4")
+ v2 = parse(django_version)
+ if v1 < v2:
+ version_comparison_result = f"v1 ({v1}) is older than Django version ({v2})"
+ else:
+ version_comparison_result = f"v1 ({v1}) is newer than Django version ({v2})"
+ logger.info(version_comparison_result)
+
+ ### asgiref
+ greeting = async_to_sync(async_greeting)()
+ logger.info(f"Async greeting: {greeting}")
+
+
+ # Create a new entry in the database
+ new_entry = ExampleModel.objects.create(name=f"RandomName{random.randint(1, 1000)}")
+
+ # google.protobuf
+ message = example_pb2.ExampleMessage(
+ name=new_entry.name,
+ id=new_entry.id,
+ created_at=str(new_entry.created_at)
+ )
+ serialized_message = message.SerializeToString()
+ logger.info(f"Serialized protobuf message: {serialized_message}")
+
+ return HttpResponse(f"Inserted random row and serialized protobuf: {new_entry.name}")
+ except Exception as e:
+ logger.error(f"Error inserting random row: {e}")
+ return HttpResponse(f"Error inserting random row: {e}", status=500)
+
+
+# Create a view to fetch all rows
+def fetch_all_rows(request):
+ try:
+ all_entries = ExampleModel.objects.all()
+
+ # Deserialize protobuf data from each entry
+ entries_list = []
+ for entry in all_entries:
+ message = example_pb2.ExampleMessage(
+ name=entry.name,
+ id=entry.id,
+ created_at=str(entry.created_at)
+ )
+ serialized_message = message.SerializeToString()
+
+ # Deserialize the message back to the object
+ deserialized_message = example_pb2.ExampleMessage()
+ deserialized_message.ParseFromString(serialized_message)
+
+ entries_list.append(f"{deserialized_message.name} (ID: {deserialized_message.id})")
+
+ logger.info(f"Fetched and deserialized rows: {entries_list}")
+ return HttpResponse(f"All rows: {', '.join(entries_list)}")
+ except Exception as e:
+ logger.error(f"Error fetching rows: {e}")
+ return HttpResponse(f"Error fetching rows: {e}", status=500)
\ No newline at end of file
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/requirements-legacy.txt b/tests/e2e/workload-lifecycle/services/python-http-server/requirements-legacy.txt
new file mode 100644
index 000000000..a6c874b02
--- /dev/null
+++ b/tests/e2e/workload-lifecycle/services/python-http-server/requirements-legacy.txt
@@ -0,0 +1,5 @@
+django==3.2.25
+protobuf==3.19.6
+packaging
+importlib_metadata
+django-health-check
\ No newline at end of file
diff --git a/tests/e2e/workload-lifecycle/services/python-http-server/requirements.txt b/tests/e2e/workload-lifecycle/services/python-http-server/requirements.txt
new file mode 100644
index 000000000..549c25564
--- /dev/null
+++ b/tests/e2e/workload-lifecycle/services/python-http-server/requirements.txt
@@ -0,0 +1,7 @@
+Django
+protobuf==5.28.2
+importlib_metadata
+packaging
+asgiref
+asyncio
+django-health-check
\ No newline at end of file