diff --git a/api/config/crd/bases/odigos.io_instrumentationconfigs.yaml b/api/config/crd/bases/odigos.io_instrumentationconfigs.yaml index b2f338f1d..fdfd06cea 100644 --- a/api/config/crd/bases/odigos.io_instrumentationconfigs.yaml +++ b/api/config/crd/bases/odigos.io_instrumentationconfigs.yaml @@ -416,6 +416,10 @@ spec: - payloadCollection type: object type: array + serviceName: + description: the service.name property is used to populate the `service.name` + resource attribute in the telemetry generated by this workload + type: string type: object status: properties: diff --git a/api/generated/odigos/applyconfiguration/odigos/v1alpha1/instrumentationconfigspec.go b/api/generated/odigos/applyconfiguration/odigos/v1alpha1/instrumentationconfigspec.go index 0a91a1d61..4c6b6fb4c 100644 --- a/api/generated/odigos/applyconfiguration/odigos/v1alpha1/instrumentationconfigspec.go +++ b/api/generated/odigos/applyconfiguration/odigos/v1alpha1/instrumentationconfigspec.go @@ -20,6 +20,7 @@ package v1alpha1 // InstrumentationConfigSpecApplyConfiguration represents a declarative configuration of the InstrumentationConfigSpec type for use // with apply. type InstrumentationConfigSpecApplyConfiguration struct { + ServiceName *string `json:"serviceName,omitempty"` RuntimeDetailsInvalidated *bool `json:"runtimeDetailsInvalidated,omitempty"` Config []WorkloadInstrumentationConfigApplyConfiguration `json:"config,omitempty"` SdkConfigs []SdkConfigApplyConfiguration `json:"sdkConfigs,omitempty"` @@ -31,6 +32,14 @@ func InstrumentationConfigSpec() *InstrumentationConfigSpecApplyConfiguration { return &InstrumentationConfigSpecApplyConfiguration{} } +// WithServiceName sets the ServiceName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ServiceName field is set to the value of the last call. +func (b *InstrumentationConfigSpecApplyConfiguration) WithServiceName(value string) *InstrumentationConfigSpecApplyConfiguration { + b.ServiceName = &value + return b +} + // WithRuntimeDetailsInvalidated sets the RuntimeDetailsInvalidated field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the RuntimeDetailsInvalidated field is set to the value of the last call. diff --git a/api/odigos/v1alpha1/instrumentationconfig_types.go b/api/odigos/v1alpha1/instrumentationconfig_types.go index 3b3b35ffd..273ca211c 100644 --- a/api/odigos/v1alpha1/instrumentationconfig_types.go +++ b/api/odigos/v1alpha1/instrumentationconfig_types.go @@ -34,6 +34,10 @@ type InstrumentationConfigStatus struct { // Config for the OpenTelemeetry SDKs that should be applied to a workload. // The workload is identified by the owner reference type InstrumentationConfigSpec struct { + + // the service.name property is used to populate the `service.name` resource attribute in the telemetry generated by this workload + ServiceName string `json:"serviceName,omitempty"` + // true when the runtime details are invalidated and should be recalculated RuntimeDetailsInvalidated bool `json:"runtimeDetailsInvalidated,omitempty"` diff --git a/instrumentor/controllers/instrumentationconfig/common.go b/instrumentor/controllers/instrumentationconfig/common.go index 335291516..22bed59d6 100644 --- a/instrumentor/controllers/instrumentationconfig/common.go +++ b/instrumentor/controllers/instrumentationconfig/common.go @@ -1,14 +1,18 @@ package instrumentationconfig import ( + "context" + "fmt" + odigosv1alpha1 "github.com/odigos-io/odigos/api/odigos/v1alpha1" "github.com/odigos-io/odigos/api/odigos/v1alpha1/instrumentationrules" "github.com/odigos-io/odigos/common" "github.com/odigos-io/odigos/instrumentor/controllers/utils" "github.com/odigos-io/odigos/k8sutils/pkg/workload" + "sigs.k8s.io/controller-runtime/pkg/client" ) -func updateInstrumentationConfigForWorkload(ic *odigosv1alpha1.InstrumentationConfig, ia *odigosv1alpha1.InstrumentedApplication, rules *odigosv1alpha1.InstrumentationRuleList) error { +func updateInstrumentationConfigForWorkload(ic *odigosv1alpha1.InstrumentationConfig, ia *odigosv1alpha1.InstrumentedApplication, rules *odigosv1alpha1.InstrumentationRuleList, serviceName string) error { workloadName, workloadKind, err := workload.ExtractWorkloadInfoFromRuntimeObjectName(ia.Name) if err != nil { @@ -20,6 +24,8 @@ func updateInstrumentationConfigForWorkload(ic *odigosv1alpha1.InstrumentationCo Kind: workloadKind, } + ic.Spec.ServiceName = serviceName + sdkConfigs := make([]odigosv1alpha1.SdkConfig, 0, len(ia.Spec.RuntimeDetails)) // create an empty sdk config for each detected programming language @@ -239,3 +245,13 @@ func mergeMessagingPayloadCollectionRules(rule1 *instrumentationrules.MessagingP func boolPtr(b bool) *bool { return &b } + +func resolveServiceName(ctx context.Context, k8sClient client.Client, workloadName string, namespace string, kind workload.WorkloadKind) (string, error) { + objectKey := client.ObjectKey{Name: workloadName, Namespace: namespace} + obj, err := workload.GetWorkloadObject(ctx, objectKey, kind, k8sClient) + + if err != nil { + return "", fmt.Errorf("failed to get workload object to resolve reported service name annotation. will use fallback service name: %w", err) + } + return workload.ExtractServiceNameFromAnnotations(obj.GetAnnotations(), workloadName), nil +} diff --git a/instrumentor/controllers/instrumentationconfig/common_test.go b/instrumentor/controllers/instrumentationconfig/common_test.go index c924168b6..c5bede439 100644 --- a/instrumentor/controllers/instrumentationconfig/common_test.go +++ b/instrumentor/controllers/instrumentationconfig/common_test.go @@ -32,7 +32,7 @@ func TestUpdateInstrumentationConfigForWorkload_SingleLanguage(t *testing.T) { }, } rules := &odigosv1.InstrumentationRuleList{} - err := updateInstrumentationConfigForWorkload(&ic, &ia, rules) + err := updateInstrumentationConfigForWorkload(&ic, &ia, rules, "service-name") if err != nil { t.Errorf("Expected nil error, got %v", err) } @@ -72,7 +72,7 @@ func TestUpdateInstrumentationConfigForWorkload_MultipleLanguages(t *testing.T) }, } rules := &odigosv1.InstrumentationRuleList{} - err := updateInstrumentationConfigForWorkload(&ic, &ia, rules) + err := updateInstrumentationConfigForWorkload(&ic, &ia, rules, "service-name") if err != nil { t.Errorf("Expected nil error, got %v", err) } @@ -119,7 +119,7 @@ func TestUpdateInstrumentationConfigForWorkload_IgnoreUnknownLanguage(t *testing }, } rules := &odigosv1.InstrumentationRuleList{} - err := updateInstrumentationConfigForWorkload(&ic, &ia, rules) + err := updateInstrumentationConfigForWorkload(&ic, &ia, rules, "service-name") if err != nil { t.Errorf("Expected nil error, got %v", err) } @@ -150,7 +150,7 @@ func TestUpdateInstrumentationConfigForWorkload_NoLanguages(t *testing.T) { }, } rules := &odigosv1.InstrumentationRuleList{} - err := updateInstrumentationConfigForWorkload(&ic, &ia, rules) + err := updateInstrumentationConfigForWorkload(&ic, &ia, rules, "service-name") if err != nil { t.Errorf("Expected nil error, got %v", err) } @@ -187,7 +187,7 @@ func TestUpdateInstrumentationConfigForWorkload_SameLanguageMultipleContainers(t }, } rules := &odigosv1.InstrumentationRuleList{} - err := updateInstrumentationConfigForWorkload(&ic, &ia, rules) + err := updateInstrumentationConfigForWorkload(&ic, &ia, rules, "service-name") if err != nil { t.Errorf("Expected nil error, got %v", err) } @@ -237,7 +237,7 @@ func TestUpdateInstrumentationConfigForWorkload_SingleMatchingRule(t *testing.T) }, }, } - err := updateInstrumentationConfigForWorkload(&ic, &ia, rules) + err := updateInstrumentationConfigForWorkload(&ic, &ia, rules, "service-name") if err != nil { t.Errorf("Expected nil error, got %v", err) } @@ -303,7 +303,7 @@ func TestUpdateInstrumentationConfigForWorkload_InWorkloadList(t *testing.T) { }, } - err := updateInstrumentationConfigForWorkload(&ic, &ia, rules) + err := updateInstrumentationConfigForWorkload(&ic, &ia, rules, "service-name") if err != nil { t.Errorf("Expected nil error, got %v", err) } @@ -360,7 +360,7 @@ func TestUpdateInstrumentationConfigForWorkload_NotInWorkloadList(t *testing.T) }, } - err := updateInstrumentationConfigForWorkload(&ic, &ia, rules) + err := updateInstrumentationConfigForWorkload(&ic, &ia, rules, "service-name") if err != nil { t.Errorf("Expected nil error, got %v", err) } @@ -412,7 +412,7 @@ func TestUpdateInstrumentationConfigForWorkload_DisabledRule(t *testing.T) { }, } - err := updateInstrumentationConfigForWorkload(&ic, &ia, rules) + err := updateInstrumentationConfigForWorkload(&ic, &ia, rules, "service-name") if err != nil { t.Errorf("Expected nil error, got %v", err) } @@ -476,7 +476,7 @@ func TestUpdateInstrumentationConfigForWorkload_MultipleDefaultRules(t *testing. }, } - err := updateInstrumentationConfigForWorkload(&ic, &ia, rules) + err := updateInstrumentationConfigForWorkload(&ic, &ia, rules, "service-name") if err != nil { t.Errorf("Expected nil error, got %v", err) } @@ -567,7 +567,7 @@ func TestUpdateInstrumentationConfigForWorkload_RuleForLibrary(t *testing.T) { }, } - err := updateInstrumentationConfigForWorkload(&ic, &ia, rules) + err := updateInstrumentationConfigForWorkload(&ic, &ia, rules, "service-name") if err != nil { t.Errorf("Expected nil error, got %v", err) } @@ -630,7 +630,7 @@ func TestUpdateInstrumentationConfigForWorkload_LibraryRuleOtherLanguage(t *test }, } - err := updateInstrumentationConfigForWorkload(&ic, &ia, rules) + err := updateInstrumentationConfigForWorkload(&ic, &ia, rules, "service-name") if err != nil { t.Errorf("Expected nil error, got %v", err) } diff --git a/instrumentor/controllers/instrumentationconfig/instrumentationrule_controller.go b/instrumentor/controllers/instrumentationconfig/instrumentationrule_controller.go index a5971964d..c2b0a7a47 100644 --- a/instrumentor/controllers/instrumentationconfig/instrumentationrule_controller.go +++ b/instrumentor/controllers/instrumentationconfig/instrumentationrule_controller.go @@ -4,6 +4,7 @@ import ( "context" odigosv1alpha1 "github.com/odigos-io/odigos/api/odigos/v1alpha1" + "github.com/odigos-io/odigos/k8sutils/pkg/workload" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" @@ -33,6 +34,16 @@ func (r *InstrumentationRuleReconciler) Reconcile(ctx context.Context, req ctrl. } for _, ia := range instrumentedApplications.Items { + workloadName, workloadKind, err := workload.ExtractWorkloadInfoFromRuntimeObjectName(ia.Name) + if err != nil { + return ctrl.Result{}, err + } + + serviceName, err := resolveServiceName(ctx, r.Client, workloadName, ia.Namespace, workloadKind) + if err != nil { + logger.Error(err, "error resolving service name", "workload", ia.Name) + continue + } ic := &odigosv1alpha1.InstrumentationConfig{} err = r.Client.Get(ctx, client.ObjectKey{Name: ia.Name, Namespace: ia.Namespace}, ic) if err != nil { @@ -44,7 +55,7 @@ func (r *InstrumentationRuleReconciler) Reconcile(ctx context.Context, req ctrl. } } - err := updateInstrumentationConfigForWorkload(ic, &ia, instrumentationRules) + err = updateInstrumentationConfigForWorkload(ic, &ia, instrumentationRules, serviceName) if err != nil { logger.Error(err, "error updating instrumentation config", "workload", ia.Name) continue diff --git a/instrumentor/controllers/instrumentationconfig/instrumentedapplication_controller.go b/instrumentor/controllers/instrumentationconfig/instrumentedapplication_controller.go index 9718e5b5a..009007705 100644 --- a/instrumentor/controllers/instrumentationconfig/instrumentedapplication_controller.go +++ b/instrumentor/controllers/instrumentationconfig/instrumentedapplication_controller.go @@ -21,6 +21,7 @@ import ( odigosv1 "github.com/odigos-io/odigos/api/odigos/v1alpha1" "github.com/odigos-io/odigos/k8sutils/pkg/utils" + "github.com/odigos-io/odigos/k8sutils/pkg/workload" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" @@ -54,13 +55,24 @@ func (r *InstrumentedApplicationReconciler) Reconcile(ctx context.Context, req c return ctrl.Result{}, err } + workloadName, workloadKind, err := workload.ExtractWorkloadInfoFromRuntimeObjectName(ia.Name) + if err != nil { + return ctrl.Result{}, err + } + + serviceName, err := resolveServiceName(ctx, r.Client, workloadName, ia.Namespace, workloadKind) + if err != nil { + logger.Error(err, "Failed to resolve service name", "workload", workloadName, "kind", workloadKind) + return ctrl.Result{}, err + } + instrumentationRules := &odigosv1.InstrumentationRuleList{} err = r.Client.List(ctx, instrumentationRules) if client.IgnoreNotFound(err) != nil { return ctrl.Result{}, err } - err = updateInstrumentationConfigForWorkload(&ic, &ia, instrumentationRules) + err = updateInstrumentationConfigForWorkload(&ic, &ia, instrumentationRules, serviceName) if err != nil { return ctrl.Result{}, err } diff --git a/instrumentor/controllers/instrumentationconfig/manager.go b/instrumentor/controllers/instrumentationconfig/manager.go index 17b4ba3ec..1a20b5718 100644 --- a/instrumentor/controllers/instrumentationconfig/manager.go +++ b/instrumentor/controllers/instrumentationconfig/manager.go @@ -2,11 +2,13 @@ package instrumentationconfig import ( odigosv1alpha1 "github.com/odigos-io/odigos/api/odigos/v1alpha1" + appsv1 "k8s.io/api/apps/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" ) func SetupWithManager(mgr ctrl.Manager) error { + // Watch InstrumentationRule err := builder. ControllerManagedBy(mgr). Named("instrumentor-instrumentationconfig-instrumentationrule"). @@ -19,6 +21,7 @@ func SetupWithManager(mgr ctrl.Manager) error { return err } + // Watch InstrumentedApplication err = builder. ControllerManagedBy(mgr). Named("instrumentor-instrumentationconfig-instrumentedapplication"). @@ -31,5 +34,44 @@ func SetupWithManager(mgr ctrl.Manager) error { return err } + // Watch for Deployment changes using DeploymentReconciler + if err := builder. + ControllerManagedBy(mgr). + Named("instrumentor-instrumentationconfig-deployment"). + For(&appsv1.Deployment{}). + WithEventFilter(workloadReportedNameAnnotationChanged{}). + Complete(&DeploymentReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }); err != nil { + return err + } + + // Watch for StatefulSet changes using StatefulSetReconciler + if err := builder. + ControllerManagedBy(mgr). + Named("instrumentor-instrumentationconfig-statefulset"). + For(&appsv1.StatefulSet{}). + WithEventFilter(workloadReportedNameAnnotationChanged{}). + Complete(&StatefulSetReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }); err != nil { + return err + } + + // Watch for DaemonSet changes using DaemonSetReconciler + if err := builder. + ControllerManagedBy(mgr). + Named("instrumentor-instrumentationconfig-daemonset"). + For(&appsv1.DaemonSet{}). + WithEventFilter(workloadReportedNameAnnotationChanged{}). + Complete(&DaemonSetReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }); err != nil { + return err + } + return nil } diff --git a/instrumentor/controllers/instrumentationconfig/workload_predicate.go b/instrumentor/controllers/instrumentationconfig/workload_predicate.go new file mode 100644 index 000000000..369141f27 --- /dev/null +++ b/instrumentor/controllers/instrumentationconfig/workload_predicate.go @@ -0,0 +1,42 @@ +package instrumentationconfig + +import ( + "github.com/odigos-io/odigos/common/consts" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +// workloadReportedNameAnnotationChanged is a custom predicate that detects changes +// to the `odigos.io/reported-name` annotation on workload resources such as +// Deployment, StatefulSet, and DaemonSet. This ensures that the controller +// reacts only when the specific annotation is updated. +type workloadReportedNameAnnotationChanged struct { + predicate.Funcs +} + +// the instrumentation config is create by the instrumented application controller +func (w workloadReportedNameAnnotationChanged) Create(e event.CreateEvent) bool { + return false +} + +func (w workloadReportedNameAnnotationChanged) Update(e event.UpdateEvent) bool { + if e.ObjectOld == nil || e.ObjectNew == nil { + return false + } + + oldAnnotations := e.ObjectOld.GetAnnotations() + newAnnotations := e.ObjectNew.GetAnnotations() + + oldName := oldAnnotations[consts.OdigosReportedNameAnnotation] + newName := newAnnotations[consts.OdigosReportedNameAnnotation] + + return oldName != newName +} + +func (w workloadReportedNameAnnotationChanged) Delete(e event.DeleteEvent) bool { + return false +} + +func (w workloadReportedNameAnnotationChanged) Generic(e event.GenericEvent) bool { + return false +} diff --git a/instrumentor/controllers/instrumentationconfig/workloads_controllers.go b/instrumentor/controllers/instrumentationconfig/workloads_controllers.go new file mode 100644 index 000000000..45b80e17d --- /dev/null +++ b/instrumentor/controllers/instrumentationconfig/workloads_controllers.go @@ -0,0 +1,75 @@ +package instrumentationconfig + +import ( + "context" + + odigosv1alpha1 "github.com/odigos-io/odigos/api/odigos/v1alpha1" + "github.com/odigos-io/odigos/k8sutils/pkg/utils" + "github.com/odigos-io/odigos/k8sutils/pkg/workload" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// These controllers handle update of the InstrumentationConfig's ServiceName +// whenever there are changes in the associated workloads (Deployments, DaemonSets, StatefulSets). + +type DeploymentReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +func (r *DeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + return reconcileWorkload(ctx, r.Client, workload.WorkloadKindDeployment, req) +} + +type DaemonSetReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +func (r *DaemonSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + return reconcileWorkload(ctx, r.Client, workload.WorkloadKindDaemonSet, req) +} + +type StatefulSetReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +func (r *StatefulSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + return reconcileWorkload(ctx, r.Client, workload.WorkloadKindStatefulSet, req) +} + +func reconcileWorkload(ctx context.Context, k8sClient client.Client, objKind workload.WorkloadKind, req ctrl.Request) (ctrl.Result, error) { + serviceName, err := resolveServiceName(ctx, k8sClient, req.Name, req.Namespace, objKind) + if err != nil { + return ctrl.Result{}, err + } + instConfigName := workload.CalculateWorkloadRuntimeObjectName(req.Name, objKind) + return updateInstrumentationConfigServiceName(ctx, k8sClient, instConfigName, req.Namespace, serviceName) + +} + +func updateInstrumentationConfigServiceName(ctx context.Context, k8sClient client.Client, instConfigName, namespace string, serviceName string) (reconcile.Result, error) { + logger := log.FromContext(ctx) + + instConfig := &odigosv1alpha1.InstrumentationConfig{} + err := k8sClient.Get(ctx, types.NamespacedName{Name: instConfigName, Namespace: namespace}, instConfig) + if err != nil { + return reconcile.Result{}, client.IgnoreNotFound(err) + } + + if instConfig.Spec.ServiceName != serviceName { + instConfig.Spec.ServiceName = serviceName + + logger.Info("Updating InstrumentationConfig", "name", instConfigName, "namespace", namespace) + err = k8sClient.Update(ctx, instConfig) + return utils.K8SUpdateErrorHandler(err) + } + + return reconcile.Result{}, nil +} diff --git a/k8sutils/pkg/workload/workload.go b/k8sutils/pkg/workload/workload.go index fa860a50d..d8b6369a8 100644 --- a/k8sutils/pkg/workload/workload.go +++ b/k8sutils/pkg/workload/workload.go @@ -4,13 +4,14 @@ import ( "context" "errors" - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "sigs.k8s.io/controller-runtime/pkg/log" - "github.com/odigos-io/odigos/common/consts" + v1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" ) type Workload interface { @@ -157,3 +158,44 @@ func GetInstrumentationLabelTexts(workloadLabels map[string]string, workloadKind return } + +func GetWorkloadObject(ctx context.Context, objectKey client.ObjectKey, kind WorkloadKind, kubeClient client.Client) (metav1.Object, error) { + switch kind { + case WorkloadKindDeployment: + var deployment v1.Deployment + err := kubeClient.Get(ctx, objectKey, &deployment) + if err != nil { + return nil, err + } + return &deployment, nil + + case WorkloadKindStatefulSet: + var statefulSet v1.StatefulSet + err := kubeClient.Get(ctx, objectKey, &statefulSet) + if err != nil { + return nil, err + } + return &statefulSet, nil + + case WorkloadKindDaemonSet: + var daemonSet v1.DaemonSet + err := kubeClient.Get(ctx, objectKey, &daemonSet) + if err != nil { + return nil, err + } + return &daemonSet, nil + + default: + return nil, errors.New("failed to get workload object for kind: " + string(kind)) + } +} + +func ExtractServiceNameFromAnnotations(annotations map[string]string, defaultName string) string { + if annotations == nil { + return defaultName + } + if reportedName, exists := annotations[consts.OdigosReportedNameAnnotation]; exists && reportedName != "" { + return reportedName + } + return defaultName +}