From 84081078b8b3772f01e18d054542e96f744bf620 Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Wed, 10 Nov 2021 21:49:07 +0900 Subject: [PATCH] Add autoinstrumentation of NodeJS (#507) * Add autoinstrumentation of NodeJS * Drift * Switch to single annotation * Fix merge * Fix * Hurts * Format * Revert autogen * Drift * Revert autogen * autogen * Drift * Add doc * e2e * Less doc * Cleanup --- README.md | 42 ++-- .../v1alpha1/instrumentation_types.go | 13 ++ .../v1alpha1/zz_generated.deepcopy.go | 16 ++ .../opentelemetry.io_instrumentations.yaml | 7 + .../opentelemetry.io_instrumentations.yaml | 7 + pkg/instrumentation/annotation.go | 9 +- pkg/instrumentation/annotation_test.go | 2 +- pkg/instrumentation/nodejs.go | 68 +++++++ pkg/instrumentation/nodejs_test.go | 181 ++++++++++++++++++ pkg/instrumentation/podmutator.go | 74 +++---- pkg/instrumentation/podmutator_test.go | 93 +++++++++ pkg/instrumentation/sdk.go | 16 +- pkg/instrumentation/sdk_test.go | 86 ++++++++- .../00-install-collector.yaml | 25 +++ .../00-install-instrumentation.yaml | 12 ++ .../e2e/instrumentation-nodejs/01-assert.yaml | 30 +++ .../01-install-app.yaml | 20 ++ 17 files changed, 642 insertions(+), 59 deletions(-) create mode 100644 pkg/instrumentation/nodejs.go create mode 100644 pkg/instrumentation/nodejs_test.go create mode 100644 tests/e2e/instrumentation-nodejs/00-install-collector.yaml create mode 100644 tests/e2e/instrumentation-nodejs/00-install-instrumentation.yaml create mode 100644 tests/e2e/instrumentation-nodejs/01-assert.yaml create mode 100644 tests/e2e/instrumentation-nodejs/01-install-app.yaml diff --git a/README.md b/README.md index d85b94a64b..0e9d9ab108 100644 --- a/README.md +++ b/README.md @@ -155,27 +155,16 @@ EOF ### OpenTelemetry auto-instrumentation injection -The operator can inject and configure OpenTelemetry auto-instrumentation libraries. At this moment, the operator can inject only OpenTelemetry [Java auto-instrumentation](https://github.com/open-telemetry/opentelemetry-java-instrumentation). +The operator can inject and configure OpenTelemetry auto-instrumentation libraries. Currently Java and NodeJS are supported. -The injection of the Java agent can be enabled by adding an annotation to the namespace, so that all pods within that namespace will get the instrumentation, or by adding the annotation to individual PodSpec objects, available as part of Deployment, Statefulset, and other resources. - -```bash -instrumentation.opentelemetry.io/inject-java: "true" -``` - -The value can be -* `"false"` - do not inject -* `"true"` - inject and `Instrumentation` resource from the namespace. -* `"java-instrumentation"` - name of `Instrumentation` CR instance. - -In addition to the annotation, the following `CR` has to be created. The `Instrumentation` resource provides configuration for OpenTelemetry SDK and auto-instrumentation. +To use auto-instrumentation, configure an `Instrumentation` resource with the configuration for the SDK and instrumentation. ```yaml kubectl apply -f - < + image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java:latest + nodejs: + image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-nodejs:latest EOF ``` -1. Container image with [OpenTelemetry Java auto-instrumentation](https://github.com/open-telemetry/opentelemetry-java-instrumentation). The image must contain the Java agent JAR `/javaagent.jar`, and the operator will copy it to a shared volume mounted to the application container. - The above CR can be queried by `kubectl get otelinst`. +Then add an annotation to a pod to enable injection. The annotation can be added to a namespace, so that all pods within +that namespace wil get instrumentation, or by adding the annotation to individual PodSpec objects, available as part of +Deployment, Statefulset, and other resources. + +Java: +```bash +instrumentation.opentelemetry.io/inject-java: "true" +``` + +NodeJS: +```bash +instrumentation.opentelemetry.io/inject-nodejs: "true" +``` + +The possible values for the annotation can be +* `"true"` - inject and `Instrumentation` resource from the namespace. +* `"my-instrumentation"` - name of `Instrumentation` CR instance. +* `"false"` - do not inject + ## Compatibility matrix ### OpenTelemetry Operator vs. OpenTelemetry Collector diff --git a/apis/instrumentation/v1alpha1/instrumentation_types.go b/apis/instrumentation/v1alpha1/instrumentation_types.go index 48b29748fd..c38ee1de4f 100644 --- a/apis/instrumentation/v1alpha1/instrumentation_types.go +++ b/apis/instrumentation/v1alpha1/instrumentation_types.go @@ -42,6 +42,11 @@ type InstrumentationSpec struct { // +optional // +operator-sdk:gen-csv:customresourcedefinitions.specDescriptors=true Java JavaSpec `json:"java,omitempty"` + + // NodeJS defines configuration for nodejs auto-instrumentation. + // +optional + // +operator-sdk:gen-csv:customresourcedefinitions.specDescriptors=true + NodeJS NodeJSSpec `json:"nodejs,omitempty"` } // JavaSpec defines Java SDK and instrumentation configuration. @@ -52,6 +57,14 @@ type JavaSpec struct { Image string `json:"image,omitempty"` } +// NodeJSSpec defines NodeJS SDK and instrumentation configuration. +type NodeJSSpec struct { + // Image is a container image with NodeJS 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. diff --git a/apis/instrumentation/v1alpha1/zz_generated.deepcopy.go b/apis/instrumentation/v1alpha1/zz_generated.deepcopy.go index ff2ec25734..03c55c9eca 100644 --- a/apis/instrumentation/v1alpha1/zz_generated.deepcopy.go +++ b/apis/instrumentation/v1alpha1/zz_generated.deepcopy.go @@ -115,6 +115,7 @@ func (in *InstrumentationSpec) DeepCopyInto(out *InstrumentationSpec) { } out.Sampler = in.Sampler out.Java = in.Java + out.NodeJS = in.NodeJS } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstrumentationSpec. @@ -157,6 +158,21 @@ func (in *JavaSpec) DeepCopy() *JavaSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodeJSSpec) DeepCopyInto(out *NodeJSSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeJSSpec. +func (in *NodeJSSpec) DeepCopy() *NodeJSSpec { + if in == nil { + return nil + } + out := new(NodeJSSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Sampler) DeepCopyInto(out *Sampler) { *out = *in diff --git a/bundle/manifests/opentelemetry.io_instrumentations.yaml b/bundle/manifests/opentelemetry.io_instrumentations.yaml index 94072224a2..56417bd776 100644 --- a/bundle/manifests/opentelemetry.io_instrumentations.yaml +++ b/bundle/manifests/opentelemetry.io_instrumentations.yaml @@ -57,6 +57,13 @@ spec: description: Image is a container image with javaagent JAR. type: string type: object + nodejs: + description: NodeJS defines configuration for nodejs auto-instrumentation. + properties: + image: + description: Image is a container image with NodeJS SDK and autoinstrumentation. + type: string + type: object propagators: description: Propagators defines inter-process context propagation configuration. diff --git a/config/crd/bases/opentelemetry.io_instrumentations.yaml b/config/crd/bases/opentelemetry.io_instrumentations.yaml index bcd0cc196f..c3cbe5bf7c 100644 --- a/config/crd/bases/opentelemetry.io_instrumentations.yaml +++ b/config/crd/bases/opentelemetry.io_instrumentations.yaml @@ -59,6 +59,13 @@ spec: description: Image is a container image with javaagent JAR. type: string type: object + nodejs: + description: NodeJS defines configuration for nodejs auto-instrumentation. + properties: + image: + description: Image is a container image with NodeJS SDK and autoinstrumentation. + type: string + type: object propagators: description: Propagators defines inter-process context propagation configuration. diff --git a/pkg/instrumentation/annotation.go b/pkg/instrumentation/annotation.go index fd063f8a3f..6cc252a035 100644 --- a/pkg/instrumentation/annotation.go +++ b/pkg/instrumentation/annotation.go @@ -23,15 +23,16 @@ import ( const ( // annotationInjectJava indicates whether java auto-instrumentation should be injected or not. // Possible values are "true", "false" or "" name. - annotationInjectJava = "instrumentation.opentelemetry.io/inject-java" + annotationInjectJava = "instrumentation.opentelemetry.io/inject-java" + annotationInjectNodeJS = "instrumentation.opentelemetry.io/inject-nodejs" ) // annotationValue returns the effective annotationInjectJava value, based on the annotations from the pod and namespace. -func annotationValue(ns metav1.ObjectMeta, pod metav1.ObjectMeta) string { +func annotationValue(ns metav1.ObjectMeta, pod metav1.ObjectMeta, annotation string) string { // is the pod annotated with instructions to inject sidecars? is the namespace annotated? // if any of those is true, a sidecar might be desired. - podAnnValue := pod.Annotations[annotationInjectJava] - nsAnnValue := ns.Annotations[annotationInjectJava] + podAnnValue := pod.Annotations[annotation] + nsAnnValue := ns.Annotations[annotation] // if the namespace value is empty, the pod annotation should be used, whatever it is if len(nsAnnValue) == 0 { diff --git a/pkg/instrumentation/annotation_test.go b/pkg/instrumentation/annotation_test.go index d8f2ac2b52..511a71adea 100644 --- a/pkg/instrumentation/annotation_test.go +++ b/pkg/instrumentation/annotation_test.go @@ -133,7 +133,7 @@ func TestEffectiveAnnotationValue(t *testing.T) { } { t.Run(tt.desc, func(t *testing.T) { // test - annValue := annotationValue(tt.ns.ObjectMeta, tt.pod.ObjectMeta) + annValue := annotationValue(tt.ns.ObjectMeta, tt.pod.ObjectMeta, annotationInjectJava) // verify assert.Equal(t, tt.expected, annValue) diff --git a/pkg/instrumentation/nodejs.go b/pkg/instrumentation/nodejs.go new file mode 100644 index 0000000000..86a2cc20cc --- /dev/null +++ b/pkg/instrumentation/nodejs.go @@ -0,0 +1,68 @@ +// 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 ( + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + + "github.com/open-telemetry/opentelemetry-operator/apis/instrumentation/v1alpha1" +) + +const ( + envNodeOptions = "NODE_OPTIONS" + nodeRequireArgument = " --require /otel-auto-instrumentation/autoinstrumentation.js" +) + +func injectNodeJSSDK(logger logr.Logger, nodeJSSpec v1alpha1.NodeJSSpec, pod corev1.Pod) corev1.Pod { + // caller checks if there is at least one container + container := &pod.Spec.Containers[0] + idx := getIndexOfEnv(container.Env, envNodeOptions) + if idx == -1 { + container.Env = append(container.Env, corev1.EnvVar{ + Name: envNodeOptions, + Value: nodeRequireArgument, + }) + } else if idx > -1 { + if container.Env[idx].ValueFrom != nil { + // TODO add to status object or submit it as an event + logger.Info("Skipping NodeJS SDK injection, the container defines NODE_OPTIONS env var value via ValueFrom", "container", container.Name) + return pod + } + container.Env[idx].Value = container.Env[idx].Value + nodeRequireArgument + } + 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: nodeJSSpec.Image, + Command: []string{"cp", "-a", "/autoinstrumentation/.", "/otel-auto-instrumentation/"}, + VolumeMounts: []corev1.VolumeMount{{ + Name: volumeName, + MountPath: "/otel-auto-instrumentation", + }}, + }) + + return pod +} diff --git a/pkg/instrumentation/nodejs_test.go b/pkg/instrumentation/nodejs_test.go new file mode 100644 index 0000000000..37722ae32a --- /dev/null +++ b/pkg/instrumentation/nodejs_test.go @@ -0,0 +1,181 @@ +// 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 ( + "testing" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + + "github.com/open-telemetry/opentelemetry-operator/apis/instrumentation/v1alpha1" +) + +func TestInjectNodeJSSDK(t *testing.T) { + tests := []struct { + name string + v1alpha1.NodeJSSpec + pod corev1.Pod + expected corev1.Pod + }{ + { + name: "NODE_OPTIONS not defined", + NodeJSSpec: v1alpha1.NodeJSSpec{Image: "foo/bar:1"}, + pod: corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {}, + }, + }, + }, + expected: corev1.Pod{ + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + InitContainers: []corev1.Container{ + { + Name: initContainerName, + Image: "foo/bar:1", + Command: []string{"cp", "-a", "/autoinstrumentation/.", "/otel-auto-instrumentation/"}, + VolumeMounts: []corev1.VolumeMount{{ + Name: volumeName, + MountPath: "/otel-auto-instrumentation", + }}, + }, + }, + Containers: []corev1.Container{ + { + VolumeMounts: []corev1.VolumeMount{ + { + Name: volumeName, + MountPath: "/otel-auto-instrumentation", + }, + }, + Env: []corev1.EnvVar{ + { + Name: "NODE_OPTIONS", + Value: nodeRequireArgument, + }, + }, + }, + }, + }, + }, + }, + { + name: "NODE_OPTIONS defined", + NodeJSSpec: v1alpha1.NodeJSSpec{Image: "foo/bar:1"}, + pod: corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Env: []corev1.EnvVar{ + { + Name: "NODE_OPTIONS", + Value: "-Dbaz=bar", + }, + }, + }, + }, + }, + }, + expected: corev1.Pod{ + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + InitContainers: []corev1.Container{ + { + Name: initContainerName, + Image: "foo/bar:1", + Command: []string{"cp", "-a", "/autoinstrumentation/.", "/otel-auto-instrumentation/"}, + VolumeMounts: []corev1.VolumeMount{{ + Name: volumeName, + MountPath: "/otel-auto-instrumentation", + }}, + }, + }, + Containers: []corev1.Container{ + { + VolumeMounts: []corev1.VolumeMount{ + { + Name: volumeName, + MountPath: "/otel-auto-instrumentation", + }, + }, + Env: []corev1.EnvVar{ + { + Name: "NODE_OPTIONS", + Value: "-Dbaz=bar" + nodeRequireArgument, + }, + }, + }, + }, + }, + }, + }, + { + name: "NODE_OPTIONS defined as ValueFrom", + NodeJSSpec: v1alpha1.NodeJSSpec{Image: "foo/bar:1"}, + pod: corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Env: []corev1.EnvVar{ + { + Name: "NODE_OPTIONS", + ValueFrom: &corev1.EnvVarSource{}, + }, + }, + }, + }, + }, + }, + expected: corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Env: []corev1.EnvVar{ + { + Name: "NODE_OPTIONS", + ValueFrom: &corev1.EnvVarSource{}, + }, + }, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + pod := injectNodeJSSDK(logr.Discard(), test.NodeJSSpec, test.pod) + assert.Equal(t, test.expected, pod) + }) + } +} diff --git a/pkg/instrumentation/podmutator.go b/pkg/instrumentation/podmutator.go index 691dca1aa7..151a546a73 100644 --- a/pkg/instrumentation/podmutator.go +++ b/pkg/instrumentation/podmutator.go @@ -38,6 +38,11 @@ type instPodMutator struct { Client client.Client } +type languageInstrumentations struct { + Java *v1alpha1.Instrumentation + NodeJS *v1alpha1.Instrumentation +} + var _ webhookhandler.PodMutator = (*instPodMutator)(nil) func NewMutator(logger logr.Logger, client client.Client) *instPodMutator { @@ -50,64 +55,69 @@ func NewMutator(logger logr.Logger, client client.Client) *instPodMutator { func (pm *instPodMutator) Mutate(ctx context.Context, ns corev1.Namespace, pod corev1.Pod) (corev1.Pod, error) { logger := pm.Logger.WithValues("namespace", pod.Namespace, "name", pod.Name) - // if no annotations are found at all, just return the same pod - annValue := annotationValue(ns.ObjectMeta, pod.ObjectMeta) - if len(annValue) == 0 { - logger.V(1).Info("annotation not present in deployment, skipping instrumentation injection") - return pod, nil - } + var inst *v1alpha1.Instrumentation + var err error - // is the annotation value 'false'? if so, we need a pod without the instrumentation - if strings.EqualFold(annValue, "false") { - logger.V(1).Info("pod explicitly refuses instrumentation injection, attempting to remove instrumentation if it exists") - return pod, nil - } + insts := languageInstrumentations{} - // which instance should it talk to? - otelinst, err := pm.getInstrumentationInstance(ctx, ns, annValue) - if err != nil { - if err == errNoInstancesAvailable || err == errMultipleInstancesPossible { - // 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, nil - } + // We bail out if any annotation fails to process. + + if inst, err = pm.getInstrumentationInstance(ctx, ns, pod, annotationInjectJava); 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.Java = inst - // something else happened, better fail here + if inst, err = pm.getInstrumentationInstance(ctx, ns, pod, annotationInjectNodeJS); 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.NodeJS = inst + + if insts.Java == nil && insts.NodeJS == nil { + logger.V(1).Info("annotation not present in deployment, skipping instrumentation injection") + return pod, nil + } // once it's been determined that instrumentation is desired, none exists yet, and we know which instance it should talk to, // we should inject the instrumentation. - logger.V(1).Info("injecting instrumentation into pod", "otelinst-namespace", otelinst.Namespace, "otelinst-name", otelinst.Name) - return inject(pm.Logger, otelinst, ns, pod), nil + return inject(pm.Logger, insts, ns, pod), nil } -func (pm *instPodMutator) getInstrumentationInstance(ctx context.Context, ns corev1.Namespace, ann string) (v1alpha1.Instrumentation, error) { - if strings.EqualFold(ann, "true") { +func (pm *instPodMutator) getInstrumentationInstance(ctx context.Context, ns corev1.Namespace, pod corev1.Pod, instAnnotation string) (*v1alpha1.Instrumentation, error) { + instValue := annotationValue(ns.ObjectMeta, pod.ObjectMeta, instAnnotation) + + if len(instValue) == 0 || strings.EqualFold(instValue, "false") { + return nil, nil + } + + if strings.EqualFold(instValue, "true") { return pm.selectInstrumentationInstanceFromNamespace(ctx, ns) } - otelInst := v1alpha1.Instrumentation{} - err := pm.Client.Get(ctx, types.NamespacedName{Name: ann, Namespace: ns.Name}, &otelInst) + otelInst := &v1alpha1.Instrumentation{} + err := pm.Client.Get(ctx, types.NamespacedName{Name: instValue, Namespace: ns.Name}, otelInst) if err != nil { - return otelInst, err + return nil, err } return otelInst, nil } -func (pm *instPodMutator) selectInstrumentationInstanceFromNamespace(ctx context.Context, ns corev1.Namespace) (v1alpha1.Instrumentation, error) { +func (pm *instPodMutator) selectInstrumentationInstanceFromNamespace(ctx context.Context, ns corev1.Namespace) (*v1alpha1.Instrumentation, error) { var otelInsts v1alpha1.InstrumentationList if err := pm.Client.List(ctx, &otelInsts, client.InNamespace(ns.Name)); err != nil { - return v1alpha1.Instrumentation{}, err + return nil, err } switch s := len(otelInsts.Items); { case s == 0: - return v1alpha1.Instrumentation{}, errNoInstancesAvailable + return nil, errNoInstancesAvailable case s > 1: - return v1alpha1.Instrumentation{}, errMultipleInstancesPossible + return nil, errMultipleInstancesPossible default: - return otelInsts.Items[0], nil + return &otelInsts.Items[0], nil } } diff --git a/pkg/instrumentation/podmutator_test.go b/pkg/instrumentation/podmutator_test.go index 46fff2e3c7..247beec783 100644 --- a/pkg/instrumentation/podmutator_test.go +++ b/pkg/instrumentation/podmutator_test.go @@ -175,6 +175,99 @@ func TestMutatePod(t *testing.T) { }, }, }, + { + name: "nodejs injection, true", + ns: corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "nodejs", + }, + }, + inst: v1alpha1.Instrumentation{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-inst", + Namespace: "nodejs", + }, + Spec: v1alpha1.InstrumentationSpec{ + NodeJS: v1alpha1.NodeJSSpec{ + Image: "otel/nodejs:1", + }, + Exporter: v1alpha1.Exporter{ + Endpoint: "http://collector:12345", + }, + }, + }, + pod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annotationInjectNodeJS: "true", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "app", + }, + }, + }, + }, + expected: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annotationInjectNodeJS: "true", + }, + }, + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: "opentelemetry-auto-instrumentation", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + InitContainers: []corev1.Container{ + { + Name: initContainerName, + Image: "otel/nodejs: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=nodejs", + }, + { + Name: "NODE_OPTIONS", + Value: nodeRequireArgument, + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "opentelemetry-auto-instrumentation", + MountPath: "/otel-auto-instrumentation", + }, + }, + }, + }, + }, + }, + }, { name: "missing annotation", ns: corev1.Namespace{ diff --git a/pkg/instrumentation/sdk.go b/pkg/instrumentation/sdk.go index bf32506ca3..1b46fd3501 100644 --- a/pkg/instrumentation/sdk.go +++ b/pkg/instrumentation/sdk.go @@ -39,15 +39,25 @@ const ( ) // inject a new sidecar container to the given pod, based on the given OpenTelemetryCollector. -func inject(logger logr.Logger, otelinst v1alpha1.Instrumentation, ns corev1.Namespace, pod corev1.Pod) corev1.Pod { +func inject(logger logr.Logger, insts languageInstrumentations, ns corev1.Namespace, pod corev1.Pod) corev1.Pod { if len(pod.Spec.Containers) < 1 { return pod } // inject only to the first container for now // in the future we can define an annotation to configure this - pod = injectCommonSDKConfig(otelinst, ns, pod) - pod = injectJavaagent(logger, otelinst.Spec.Java, pod) + if insts.Java != nil { + otelinst := *insts.Java + logger.V(1).Info("injecting instrumentation into pod", "otelinst-namespace", otelinst.Namespace, "otelinst-name", otelinst.Name) + pod = injectCommonSDKConfig(otelinst, ns, pod) + pod = injectJavaagent(logger, otelinst.Spec.Java, pod) + } + if insts.NodeJS != nil { + otelinst := *insts.NodeJS + logger.V(1).Info("injecting instrumentation into pod", "otelinst-namespace", otelinst.Namespace, "otelinst-name", otelinst.Name) + pod = injectCommonSDKConfig(otelinst, ns, pod) + pod = injectNodeJSSDK(logger, otelinst.Spec.NodeJS, pod) + } return pod } diff --git a/pkg/instrumentation/sdk_test.go b/pkg/instrumentation/sdk_test.go index f421f69c0a..1b8cf98fce 100644 --- a/pkg/instrumentation/sdk_test.go +++ b/pkg/instrumentation/sdk_test.go @@ -195,7 +195,7 @@ func TestSDKInjection(t *testing.T) { } } -func TestInjection(t *testing.T) { +func TestInjectJava(t *testing.T) { inst := v1alpha1.Instrumentation{ Spec: v1alpha1.InstrumentationSpec{ Java: v1alpha1.JavaSpec{ @@ -206,7 +206,10 @@ func TestInjection(t *testing.T) { }, }, } - pod := inject(logr.Discard(), inst, + insts := languageInstrumentations{ + Java: &inst, + } + pod := inject(logr.Discard(), insts, corev1.Namespace{}, corev1.Pod{ Spec: corev1.PodSpec{ @@ -270,3 +273,82 @@ func TestInjection(t *testing.T) { }, }, pod) } + +func TestInjectNodeJS(t *testing.T) { + inst := v1alpha1.Instrumentation{ + Spec: v1alpha1.InstrumentationSpec{ + NodeJS: v1alpha1.NodeJSSpec{ + Image: "img:1", + }, + Exporter: v1alpha1.Exporter{ + Endpoint: "https://collector:4318", + }, + }, + } + insts := languageInstrumentations{ + NodeJS: &inst, + } + pod := inject(logr.Discard(), insts, + corev1.Namespace{}, + corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "app", + }, + }, + }, + }) + assert.Equal(t, corev1.Pod{ + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + InitContainers: []corev1.Container{ + { + Name: initContainerName, + Image: "img:1", + Command: []string{"cp", "-a", "/autoinstrumentation/.", "/otel-auto-instrumentation/"}, + VolumeMounts: []corev1.VolumeMount{{ + Name: volumeName, + MountPath: "/otel-auto-instrumentation", + }}, + }, + }, + Containers: []corev1.Container{ + { + Name: "app", + VolumeMounts: []corev1.VolumeMount{ + { + Name: volumeName, + MountPath: "/otel-auto-instrumentation", + }, + }, + Env: []corev1.EnvVar{ + { + Name: "OTEL_SERVICE_NAME", + Value: "app", + }, + { + Name: "OTEL_EXPORTER_OTLP_ENDPOINT", + Value: "https://collector:4318", + }, + { + Name: "OTEL_RESOURCE_ATTRIBUTES", + Value: "k8s.container.name=app,k8s.namespace.name=", + }, + { + Name: "NODE_OPTIONS", + Value: nodeRequireArgument, + }, + }, + }, + }, + }, + }, pod) +} diff --git a/tests/e2e/instrumentation-nodejs/00-install-collector.yaml b/tests/e2e/instrumentation-nodejs/00-install-collector.yaml new file mode 100644 index 0000000000..b823086361 --- /dev/null +++ b/tests/e2e/instrumentation-nodejs/00-install-collector.yaml @@ -0,0 +1,25 @@ +apiVersion: opentelemetry.io/v1alpha1 +kind: OpenTelemetryCollector +metadata: + name: sidecar +spec: + mode: sidecar + args: + metrics-level: detailed + config: | + receivers: + otlp: + protocols: + grpc: + http: + processors: + + exporters: + logging: + + service: + pipelines: + traces: + receivers: [otlp] + processors: [] + exporters: [logging] diff --git a/tests/e2e/instrumentation-nodejs/00-install-instrumentation.yaml b/tests/e2e/instrumentation-nodejs/00-install-instrumentation.yaml new file mode 100644 index 0000000000..a17642f744 --- /dev/null +++ b/tests/e2e/instrumentation-nodejs/00-install-instrumentation.yaml @@ -0,0 +1,12 @@ +apiVersion: opentelemetry.io/v1alpha1 +kind: Instrumentation +metadata: + name: nodejs +spec: + exporter: + endpoint: http://localhost:4317 + propagators: + - jaeger + - b3 + nodejs: + image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-nodejs:latest diff --git a/tests/e2e/instrumentation-nodejs/01-assert.yaml b/tests/e2e/instrumentation-nodejs/01-assert.yaml new file mode 100644 index 0000000000..db5ff83555 --- /dev/null +++ b/tests/e2e/instrumentation-nodejs/01-assert.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: Pod +metadata: + annotations: + sidecar.opentelemetry.io/inject: "true" + instrumentation.opentelemetry.io/inject-nodejs: "true" + labels: + app: my-pod-with-sidecar +spec: + containers: + - name: myapp + env: + - name: OTEL_SERVICE_NAME + value: myapp + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: http://localhost:4317 + - name: OTEL_RESOURCE_ATTRIBUTES + - name: OTEL_PROPAGATORS + value: jaeger,b3 + - name: NODE_OPTIONS + value: " --require /otel-auto-instrumentation/autoinstrumentation.js" + volumeMounts: + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + - mountPath: /otel-auto-instrumentation + name: opentelemetry-auto-instrumentation + - name: otc-container + initContainers: + - name: opentelemetry-auto-instrumentation +status: + phase: Running diff --git a/tests/e2e/instrumentation-nodejs/01-install-app.yaml b/tests/e2e/instrumentation-nodejs/01-install-app.yaml new file mode 100644 index 0000000000..7514b59866 --- /dev/null +++ b/tests/e2e/instrumentation-nodejs/01-install-app.yaml @@ -0,0 +1,20 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-deployment-with-sidecar +spec: + selector: + matchLabels: + app: my-pod-with-sidecar + replicas: 1 + template: + metadata: + labels: + app: my-pod-with-sidecar + annotations: + sidecar.opentelemetry.io/inject: "true" + instrumentation.opentelemetry.io/inject-nodejs: "true" + spec: + containers: + - name: myapp + image: ghcr.io/anuraaga/express-hello-world:latest