Skip to content

Commit

Permalink
feat: inject OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES via webho…
Browse files Browse the repository at this point in the history
…ok (#2008)

For dotnet and OSS java which currently relies on the resource to inject
the device id in odiglet, and then resolves it from kublet to the
desired service name
  • Loading branch information
blumamir authored Dec 16, 2024
1 parent d39ef14 commit 34ac66a
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 22 deletions.
4 changes: 3 additions & 1 deletion instrumentor/controllers/instrumentationdevice/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,9 @@ func SetupWithManager(mgr ctrl.Manager) error {
err = builder.
WebhookManagedBy(mgr).
For(&corev1.Pod{}).
WithDefaulter(&PodsWebhook{}).
WithDefaulter(&PodsWebhook{
Client: mgr.GetClient(),
}).
Complete()
if err != nil {
return err
Expand Down
148 changes: 132 additions & 16 deletions instrumentor/controllers/instrumentationdevice/pods_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,31 @@ import (
"fmt"
"strings"

common "github.com/odigos-io/odigos/common"
"github.com/odigos-io/odigos/k8sutils/pkg/consts"
"github.com/odigos-io/odigos/common"
k8sconsts "github.com/odigos-io/odigos/k8sutils/pkg/consts"
containerutils "github.com/odigos-io/odigos/k8sutils/pkg/container"
"github.com/odigos-io/odigos/k8sutils/pkg/workload"
"go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
)

type PodsWebhook struct{}
const otelServiceNameEnvVarName = "OTEL_SERVICE_NAME"
const otelResourceAttributesEnvVarName = "OTEL_RESOURCE_ATTRIBUTES"

type resourceAttribute struct {
Key attribute.Key
Value string
}

type PodsWebhook struct {
client.Client
}

var _ webhook.CustomDefaulter = &PodsWebhook{}

Expand All @@ -27,26 +43,48 @@ func (p *PodsWebhook) Default(ctx context.Context, obj runtime.Object) error {
pod.Annotations = map[string]string{}
}

serviceName, podWorkload := p.getServiceNameForEnv(ctx, pod)

// Inject ODIGOS environment variables into all containers
injectOdigosEnvVars(pod)
injectOdigosEnvVars(pod, podWorkload, serviceName)

return nil
}

func injectOdigosEnvVars(pod *corev1.Pod) {
// checks for the service name on the annotation, or fallback to the workload name
func (p *PodsWebhook) getServiceNameForEnv(ctx context.Context, pod *corev1.Pod) (*string, *workload.PodWorkload) {

logger := log.FromContext(ctx)

podWorkload, err := workload.PodWorkloadObject(ctx, pod)
if err != nil {
logger.Error(err, "failed to extract pod workload details from pod. skipping OTEL_SERVICE_NAME injection")
return nil, nil
}

workloadObj, err := workload.GetWorkloadObject(ctx, client.ObjectKey{Namespace: podWorkload.Namespace, Name: podWorkload.Name}, podWorkload.Kind, p.Client)
if err != nil {
logger.Error(err, "failed to get workload object from cache. cannot check for workload annotation. using workload name as OTEL_SERVICE_NAME")
return &podWorkload.Name, podWorkload
}
resolvedServiceName := workload.ExtractServiceNameFromAnnotations(workloadObj.GetAnnotations(), podWorkload.Name)
return &resolvedServiceName, podWorkload
}

func injectOdigosEnvVars(pod *corev1.Pod, podWorkload *workload.PodWorkload, serviceName *string) {

// Common environment variables that do not change across containers
commonEnvVars := []corev1.EnvVar{
{
Name: consts.OdigosEnvVarNamespace,
Name: k8sconsts.OdigosEnvVarNamespace,
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "metadata.namespace",
},
},
},
{
Name: consts.OdigosEnvVarPodName,
Name: k8sconsts.OdigosEnvVarPodName,
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "metadata.name",
Expand All @@ -55,11 +93,19 @@ func injectOdigosEnvVars(pod *corev1.Pod) {
},
}

var serviceNameEnv *corev1.EnvVar
if serviceName != nil {
serviceNameEnv = &corev1.EnvVar{
Name: otelServiceNameEnvVarName,
Value: *serviceName,
}
}

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) {
pl, otelsdk, found := containerutils.GetLanguageAndOtelSdk(container)
if !found {
continue
}

Expand All @@ -68,10 +114,25 @@ func injectOdigosEnvVars(pod *corev1.Pod) {
continue
}

container.Env = append(container.Env, append(commonEnvVars, corev1.EnvVar{
Name: consts.OdigosEnvVarContainerName,
containerNameEnv := corev1.EnvVar{
Name: k8sconsts.OdigosEnvVarContainerName,
Value: container.Name,
})...)
}

resourceAttributes := getResourceAttributes(podWorkload, container.Name)
resourceAttributesEnvValue := getResourceAttributesEnvVarValue(resourceAttributes)

container.Env = append(container.Env, append(commonEnvVars, containerNameEnv)...)

if serviceNameEnv != nil && shouldInjectServiceName(pl, otelsdk) {
if !otelNameExists(container.Env) {
container.Env = append(container.Env, *serviceNameEnv)
}
container.Env = append(container.Env, corev1.EnvVar{
Name: otelResourceAttributesEnvVarName,
Value: resourceAttributesEnvValue,
})
}
}
}

Expand All @@ -89,12 +150,67 @@ func envVarsExist(containerEnv []corev1.EnvVar, commonEnvVars []corev1.EnvVar) b
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) {
func getWorkloadKindAttributeKey(podWorkload *workload.PodWorkload) attribute.Key {
switch podWorkload.Kind {
case workload.WorkloadKindDeployment:
return semconv.K8SDeploymentNameKey
case workload.WorkloadKindStatefulSet:
return semconv.K8SStatefulSetNameKey
case workload.WorkloadKindDaemonSet:
return semconv.K8SDaemonSetNameKey
}
return attribute.Key("")
}

func getResourceAttributes(podWorkload *workload.PodWorkload, containerName string) []resourceAttribute {
if podWorkload == nil {
return []resourceAttribute{}
}

workloadKindKey := getWorkloadKindAttributeKey(podWorkload)
return []resourceAttribute{
{
Key: semconv.K8SContainerNameKey,
Value: containerName,
},
{
Key: semconv.K8SNamespaceNameKey,
Value: podWorkload.Namespace,
},
{
Key: workloadKindKey,
Value: podWorkload.Name,
},
}
}

func getResourceAttributesEnvVarValue(ra []resourceAttribute) string {
var attrs []string
for _, a := range ra {
attrs = append(attrs, fmt.Sprintf("%s=%s", a.Key, a.Value))
}
return strings.Join(attrs, ",")
}

func otelNameExists(containerEnv []corev1.EnvVar) bool {
for _, envVar := range containerEnv {
if envVar.Name == otelServiceNameEnvVarName {
return true
}
}
return false
}

// this is used to set the OTEL_SERVICE_NAME for programming languages and otel sdks that requires it.
// eBPF instrumentations sets the service name in code, thus it's not needed here.
// OpAMP sends the service name in the protocol, thus it's not needed here.
// We are only left with OSS Java and Dotnet that requires the OTEL_SERVICE_NAME to be set.
func shouldInjectServiceName(pl common.ProgrammingLanguage, otelsdk common.OtelSdk) bool {
if pl == common.DotNetProgrammingLanguage {
return true
}
if pl == common.JavaProgrammingLanguage && otelsdk.SdkTier == common.CommunityOtelSdkTier {
return true
}
return false
}
2 changes: 1 addition & 1 deletion instrumentor/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/onsi/ginkgo v1.16.5
github.com/onsi/gomega v1.36.1
github.com/stretchr/testify v1.10.0
go.opentelemetry.io/otel v1.29.0
k8s.io/api v0.32.0
k8s.io/apimachinery v0.32.0
k8s.io/client-go v0.32.0
Expand All @@ -31,7 +32,6 @@ require (
github.com/nxadm/tail v1.4.8 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
Expand Down
6 changes: 3 additions & 3 deletions k8sutils/pkg/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ var (
func LanguageSdkFromPodContainer(pod *v1.Pod, containerName string) (common.ProgrammingLanguage, common.OtelSdk, error) {
for _, container := range pod.Spec.Containers {
if container.Name == containerName {
language, sdk, found := GetLanguageAndOtelSdk(container)
language, sdk, found := GetLanguageAndOtelSdk(&container)
if !found {
return common.UnknownProgrammingLanguage, common.OtelSdk{}, ErrDeviceNotDetected
}
Expand All @@ -28,7 +28,7 @@ func LanguageSdkFromPodContainer(pod *v1.Pod, containerName string) (common.Prog
return common.UnknownProgrammingLanguage, common.OtelSdk{}, ErrContainerNotInPodSpec
}

func GetLanguageAndOtelSdk(container v1.Container) (common.ProgrammingLanguage, common.OtelSdk, bool) {
func GetLanguageAndOtelSdk(container *v1.Container) (common.ProgrammingLanguage, common.OtelSdk, bool) {
deviceName := podContainerDeviceName(container)
if deviceName == nil {
return common.UnknownProgrammingLanguage, common.OtelSdk{}, false
Expand All @@ -38,7 +38,7 @@ func GetLanguageAndOtelSdk(container v1.Container) (common.ProgrammingLanguage,
return language, sdk, true
}

func podContainerDeviceName(container v1.Container) *string {
func podContainerDeviceName(container *v1.Container) *string {
if container.Resources.Limits == nil {
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion odiglet/pkg/kube/instrumentation_ebpf/shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func instrumentPodWithEbpf(ctx context.Context, pod *corev1.Pod, directors ebpf.
continue
}

language, sdk, found := odgiosK8s.GetLanguageAndOtelSdk(container)
language, sdk, found := odgiosK8s.GetLanguageAndOtelSdk(&container)
if !found {
continue
}
Expand Down

0 comments on commit 34ac66a

Please sign in to comment.