Skip to content

Commit 8a56b9b

Browse files
authored
feat: init container instrumentation (#4635)
* feat: initContainer injection Signed-off-by: Alan Clucas <alan@clucas.org> * chore: delete telemetry checks Signed-off-by: Alan Clucas <alan@clucas.org> * chore: review feedback Signed-off-by: Alan Clucas <alan@clucas.org> --------- Signed-off-by: Alan Clucas <alan@clucas.org>
1 parent 5637a30 commit 8a56b9b

38 files changed

+2255
-161
lines changed

.chloggen/init-container.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
2+
change_type: enhancement
3+
4+
# The name of the component, or a single word describing the area of concern, (e.g. collector, target allocator, auto-instrumentation, opamp, github action)
5+
component: auto-instrumentation
6+
7+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
8+
note: "Add support for initContainers to instrumentation injector"
9+
10+
# One or more tracking issues related to the change
11+
issues: [3308]
12+
13+
# (Optional) One or more lines of additional information to render under the primary note.
14+
# These lines will be padded with 2 spaces and then inserted directly into the document.
15+
# Use pipe (|) for multiline entries.
16+
subtext: |
17+
Add support for instrumenting init containers.
18+
Init container support is available for Java, Python, Node.js, .NET and SDK-only, and works using the same annotation as for regular containers.

README.md

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -361,12 +361,12 @@ The possible values for the annotation can be
361361

362362
#### Multi-container pods with single instrumentation
363363

364-
If nothing else is specified, instrumentation is performed on the first container available in the pod spec.
364+
If nothing else is specified, instrumentation is performed on the first container available in the pod spec (from `.spec.containers`, not init containers).
365365
In some cases (for example in the case of the injection of an Istio sidecar) it becomes necessary to specify on which container(s) this injection must be performed.
366366

367367
For this, it is possible to fine-tune the pod(s) on which the injection will be carried out.
368368

369-
For this, we will use the `instrumentation.opentelemetry.io/container-names` annotation for which we will indicate one or more container names (`.spec.containers.name`) on which the injection must be made:
369+
For this, we will use the `instrumentation.opentelemetry.io/container-names` annotation for which we will indicate one or more container names (from `.spec.containers.name` or `.spec.initContainers.name`) on which the injection must be made:
370370

371371
```yaml
372372
apiVersion: apps/v1
@@ -399,11 +399,59 @@ In the above case, `myapp` and `myapp2` containers will be instrumented, `myapp3
399399

400400
> 🚨 **NOTE**: Go auto-instrumentation **does not** support multicontainer pods. When injecting Go auto-instrumentation the first pod should be the only pod you want instrumented.
401401

402+
#### Instrumenting Init Containers
403+
404+
Init containers can be instrumented by including their names in the `container-names` annotation. When an init container is targeted for instrumentation, the operator automatically inserts the instrumentation init container **before** the target init container in the pod's init container sequence. This ensures the instrumentation agent files are available when the target init container runs.
405+
406+
Supported instrumentations for init containers:
407+
- Java
408+
- Python
409+
- Node.js
410+
- .NET
411+
- SDK-only injection
412+
413+
**Not supported** for init containers:
414+
- Go (does not support multicontainer pods)
415+
- Apache HTTPD
416+
- Nginx
417+
418+
> **Note**: Kubernetes guarantees that container names are unique across both the `initContainers` and `containers` lists within a pod spec. This allows the operator to unambiguously identify whether a container name refers to an init container or a regular container.
419+
420+
Example with both init container and regular container instrumentation:
421+
422+
```yaml
423+
apiVersion: apps/v1
424+
kind: Deployment
425+
metadata:
426+
name: my-deployment-with-init-container
427+
spec:
428+
selector:
429+
matchLabels:
430+
app: my-app
431+
replicas: 1
432+
template:
433+
metadata:
434+
labels:
435+
app: my-app
436+
annotations:
437+
instrumentation.opentelemetry.io/inject-python: "true"
438+
instrumentation.opentelemetry.io/container-names: "my-init-job,myapp"
439+
spec:
440+
initContainers:
441+
- name: my-init-job
442+
image: my-python-init-image
443+
containers:
444+
- name: myapp
445+
image: my-python-app-image
446+
```
447+
448+
In this example, both `my-init-job` (an init container) and `myapp` (a regular container) will be instrumented with Python auto-instrumentation.
449+
402450
#### Multi-container pods with multiple instrumentations
403451

404452
Works only when `enable-multi-instrumentation` flag is `true`.
405453

406-
Annotations defining which language instrumentation will be injected are required. When feature is enabled, specific for Instrumentation language containers annotations are used:
454+
Annotations defining which language instrumentation will be injected are required. When feature is enabled, specific for Instrumentation language containers annotations are used (these also support init container names for Java, Python, Node.js, .NET, and SDK):
407455

408456
Java:
409457

@@ -453,11 +501,11 @@ SDK:
453501
instrumentation.opentelemetry.io/sdk-container-names: "app1,app2"
454502
```
455503

456-
If language instrumentation specific container names are not specified, instrumentation is performed on the first container available in the pod spec (only if single instrumentation injection is configured).
504+
If language instrumentation specific container names are not specified, instrumentation is performed on the first regular container available in the pod spec (only if single instrumentation injection is configured).
457505

458506
In some cases containers in the pod are using different technologies. It becomes necessary to specify language instrumentation for container(s) on which this injection must be performed.
459507

460-
For this, we will use language instrumentation specific container names annotation for which we will indicate one or more container names (`.spec.containers.name`) on which the injection must be made:
508+
For this, we will use language instrumentation specific container names annotation for which we will indicate one or more container names (`.spec.containers.name` or `.spec.initContainers.name`) on which the injection must be made:
461509

462510
```yaml
463511
apiVersion: apps/v1

internal/instrumentation/dotnet.go

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,27 +39,27 @@ const (
3939
dotNetRuntimeLinuxMusl = "linux-musl-x64"
4040
)
4141

42-
func injectDotNetSDK(dotNetSpec v1alpha1.DotNet, pod corev1.Pod, container *corev1.Container, runtime string, instSpec v1alpha1.InstrumentationSpec) (corev1.Pod, error) {
42+
func injectDotNetSDKToContainer(dotNetSpec v1alpha1.DotNet, container *corev1.Container, runtime string) error {
4343
volume := instrVolume(dotNetSpec.VolumeClaimTemplate, dotnetVolumeName, dotNetSpec.VolumeSizeLimit)
4444

4545
err := validateContainerEnv(container.Env, envDotNetStartupHook, envDotNetAdditionalDeps, envDotNetSharedStore)
4646
if err != nil {
47-
return pod, err
47+
return err
4848
}
4949

5050
// check if OTEL_DOTNET_AUTO_HOME env var is already set in the container
5151
// if it is already set, then we assume that .NET Auto-instrumentation is already configured for this container
5252
if getIndexOfEnv(container.Env, envDotNetOTelAutoHome) > -1 {
53-
return pod, errors.New("OTEL_DOTNET_AUTO_HOME environment variable is already set in the container")
53+
return errors.New("OTEL_DOTNET_AUTO_HOME environment variable is already set in the container")
5454
}
5555

5656
// check if OTEL_DOTNET_AUTO_HOME env var is already set in the .NET instrumentation spec
5757
// if it is already set, then we assume that .NET Auto-instrumentation is already configured for this container
5858
if getIndexOfEnv(dotNetSpec.Env, envDotNetOTelAutoHome) > -1 {
59-
return pod, errors.New("OTEL_DOTNET_AUTO_HOME environment variable is already set in the .NET instrumentation spec")
59+
return errors.New("OTEL_DOTNET_AUTO_HOME environment variable is already set in the .NET instrumentation spec")
6060
}
6161
if runtime != "" && runtime != dotNetRuntimeLinuxGlibc && runtime != dotNetRuntimeLinuxMusl {
62-
return pod, fmt.Errorf("provided instrumentation.opentelemetry.io/dotnet-runtime annotation value '%s' is not supported", runtime)
62+
return fmt.Errorf("provided instrumentation.opentelemetry.io/dotnet-runtime annotation value '%s' is not supported", runtime)
6363
}
6464

6565
// inject .NET instrumentation spec env vars.
@@ -69,11 +69,17 @@ func injectDotNetSDK(dotNetSpec v1alpha1.DotNet, pod corev1.Pod, container *core
6969
Name: volume.Name,
7070
MountPath: dotnetInstrMountPath,
7171
})
72+
return nil
73+
}
74+
75+
func injectDotNetSDKToPod(dotNetSpec v1alpha1.DotNet, pod corev1.Pod, firstContainerName string, instSpec v1alpha1.InstrumentationSpec) corev1.Pod {
76+
volume := instrVolume(dotNetSpec.VolumeClaimTemplate, dotnetVolumeName, dotNetSpec.VolumeSizeLimit)
7277

7378
// We just inject Volumes and init containers for the first processed container.
7479
if isInitContainerMissing(pod, dotnetInitContainerName) {
7580
pod.Spec.Volumes = append(pod.Spec.Volumes, volume)
76-
pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{
81+
82+
initContainer := corev1.Container{
7783
Name: dotnetInitContainerName,
7884
Image: dotNetSpec.Image,
7985
Command: []string{"cp", "-r", "/autoinstrumentation/.", dotnetInstrMountPath},
@@ -83,9 +89,25 @@ func injectDotNetSDK(dotNetSpec v1alpha1.DotNet, pod corev1.Pod, container *core
8389
MountPath: dotnetInstrMountPath,
8490
}},
8591
ImagePullPolicy: instSpec.ImagePullPolicy,
86-
})
92+
}
93+
94+
pod.Spec.InitContainers = insertInitContainer(&pod, initContainer, firstContainerName)
95+
}
96+
return pod
97+
}
98+
99+
// injectDotNetSDK injects .NET instrumentation into the specified containers.
100+
// Containers must point into the provided pod and be ordered with init containers first.
101+
func injectDotNetSDK(dotNetSpec v1alpha1.DotNet, pod *corev1.Pod, containers []*corev1.Container, runtime string, instSpec v1alpha1.InstrumentationSpec) error {
102+
for _, container := range containers {
103+
if err := injectDotNetSDKToContainer(dotNetSpec, container, runtime); err != nil {
104+
return err
105+
}
106+
}
107+
if len(containers) > 0 {
108+
*pod = injectDotNetSDKToPod(dotNetSpec, *pod, containers[0].Name, instSpec)
87109
}
88-
return pod, nil
110+
return nil
89111
}
90112

91113
func injectDefaultDotNetEnvVars(container *corev1.Container, runtime string) {

internal/instrumentation/dotnet_test.go

Lines changed: 106 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -533,20 +533,120 @@ func TestInjectDotNetSDK(t *testing.T) {
533533
},
534534
err: errors.New("provided instrumentation.opentelemetry.io/dotnet-runtime annotation value 'not-supported' is not supported"),
535535
},
536+
{
537+
name: "inject into init container",
538+
DotNet: v1alpha1.DotNet{Image: "foo/bar:1", Env: []corev1.EnvVar{}},
539+
pod: corev1.Pod{
540+
Spec: corev1.PodSpec{
541+
InitContainers: []corev1.Container{
542+
{
543+
Name: "my-init",
544+
},
545+
},
546+
},
547+
},
548+
runtime: "",
549+
expected: corev1.Pod{
550+
Spec: corev1.PodSpec{
551+
Volumes: []corev1.Volume{
552+
{
553+
Name: "opentelemetry-auto-instrumentation-dotnet",
554+
VolumeSource: corev1.VolumeSource{
555+
EmptyDir: &corev1.EmptyDirVolumeSource{
556+
SizeLimit: &defaultVolumeLimitSize,
557+
},
558+
},
559+
},
560+
},
561+
InitContainers: []corev1.Container{
562+
{
563+
Name: "opentelemetry-auto-instrumentation-dotnet",
564+
Image: "foo/bar:1",
565+
Command: []string{"cp", "-r", "/autoinstrumentation/.", "/otel-auto-instrumentation-dotnet"},
566+
VolumeMounts: []corev1.VolumeMount{{
567+
Name: "opentelemetry-auto-instrumentation-dotnet",
568+
MountPath: "/otel-auto-instrumentation-dotnet",
569+
}},
570+
},
571+
{
572+
Name: "my-init",
573+
VolumeMounts: []corev1.VolumeMount{
574+
{
575+
Name: "opentelemetry-auto-instrumentation-dotnet",
576+
MountPath: "/otel-auto-instrumentation-dotnet",
577+
},
578+
},
579+
Env: []corev1.EnvVar{
580+
{
581+
Name: envDotNetCoreClrEnableProfiling,
582+
Value: dotNetCoreClrEnableProfilingEnabled,
583+
},
584+
{
585+
Name: envDotNetCoreClrProfiler,
586+
Value: dotNetCoreClrProfilerID,
587+
},
588+
{
589+
Name: envDotNetCoreClrProfilerPath,
590+
Value: dotNetCoreClrProfilerGlibcPath,
591+
},
592+
{
593+
Name: envDotNetStartupHook,
594+
Value: dotNetStartupHookPath,
595+
},
596+
{
597+
Name: envDotNetAdditionalDeps,
598+
Value: dotNetAdditionalDepsPath,
599+
},
600+
{
601+
Name: envDotNetOTelAutoHome,
602+
Value: dotNetOTelAutoHomePath,
603+
},
604+
{
605+
Name: envDotNetSharedStore,
606+
Value: dotNetSharedStorePath,
607+
},
608+
},
609+
},
610+
},
611+
},
612+
},
613+
err: nil,
614+
},
536615
}
537616

538617
injector := sdkInjector{}
539618
for _, test := range tests {
540619
t.Run(test.name, func(t *testing.T) {
541-
pod, err := injectDotNetSDK(test.DotNet, test.pod, &test.pod.Spec.Containers[0], test.runtime, v1alpha1.InstrumentationSpec{})
542-
assert.Equal(t, test.err, err)
543-
if err == nil {
544-
pod = injector.injectDefaultDotNetEnvVarsWrapper(pod, &pod.Spec.Containers[0], test.runtime)
620+
pod := test.pod
621+
622+
// Collect all containers (regular first, then init)
623+
var containers []*corev1.Container
624+
for i := range pod.Spec.Containers {
625+
containers = append(containers, &pod.Spec.Containers[i])
626+
}
627+
for i := range pod.Spec.InitContainers {
628+
containers = append(containers, &pod.Spec.InitContainers[i])
629+
}
630+
631+
err := injectDotNetSDK(test.DotNet, &pod, containers, test.runtime, v1alpha1.InstrumentationSpec{})
632+
if err != nil {
545633
assert.Equal(t, test.expected, pod)
546634
assert.Equal(t, test.err, err)
547-
} else {
548-
assert.Equal(t, test.expected, pod)
635+
return
636+
}
637+
638+
for i := range pod.Spec.Containers {
639+
pod = injector.injectDefaultDotNetEnvVarsWrapper(pod, &pod.Spec.Containers[i], test.runtime)
549640
}
641+
for i := range pod.Spec.InitContainers {
642+
// Skip the instrumentation init container we added
643+
if pod.Spec.InitContainers[i].Name == dotnetInitContainerName {
644+
continue
645+
}
646+
pod = injector.injectDefaultDotNetEnvVarsWrapper(pod, &pod.Spec.InitContainers[i], test.runtime)
647+
}
648+
assert.Equal(t, test.expected, pod)
649+
assert.Equal(t, test.err, err)
550650
})
551651
}
552652
}

0 commit comments

Comments
 (0)