Skip to content

Commit

Permalink
feat: otel sdk instrumentation rule (#1499)
Browse files Browse the repository at this point in the history
Add a instrumentation rule which allows to configure the SDKs to use.

This replaces the need to use `odigos-config` which must be applied as a
whole which is inconvenient and error prone.

By using the new instrumentation rule, one can specify the usecase in a
separate manifest, and gain more flexibility on how it is applied
(workloads list, languages, multiple levels, etc)
  • Loading branch information
blumamir authored Sep 14, 2024
1 parent 7b7b0a1 commit 228f166
Show file tree
Hide file tree
Showing 14 changed files with 259 additions and 28 deletions.
23 changes: 23 additions & 0 deletions api/config/crd/bases/odigos.io_instrumentationrules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,29 @@ spec:
regarding the rule for convenience. For example: why it was added.
Odigos does not use or assume any meaning from this field.'
type: string
otelSdks:
description: |-
Set the OtelSdk to use for the workloads in this rule.
instrumentation libraries will be ignored if set.
the rule will be used only for languages which are specified, and ignored otherwise.
properties:
otelSdkByLanguage:
additionalProperties:
properties:
sdkTier:
type: string
sdkType:
description: 'Odigos supports two types of OpenTelemetry
SDKs: native and ebpf.'
type: string
required:
- sdkTier
- sdkType
type: object
type: object
required:
- otelSdkByLanguage
type: object
payloadCollection:
description: Allows to configure payload collection aspects for different
types of payloads.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions api/odigos/v1alpha1/instrumentationrule_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ type InstrumentationRuleSpec struct {

// Allows to configure payload collection aspects for different types of payloads.
PayloadCollection *instrumentationrules.PayloadCollection `json:"payloadCollection,omitempty"`

// Set the OtelSdk to use for the workloads in this rule.
// instrumentation libraries will be ignored if set.
// the rule will be used only for languages which are specified, and ignored otherwise.
OtelSdks *instrumentationrules.OtelSdks `json:"otelSdks,omitempty"`
}

type InstrumentationRuleStatus struct {
Expand Down
9 changes: 9 additions & 0 deletions api/odigos/v1alpha1/instrumentationrules/otel-sdk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package instrumentationrules

import "github.com/odigos-io/odigos/common"

// +kubebuilder:object:generate=true
// +kubebuilder:deepcopy-gen=true
type OtelSdks struct {
OtelSdkByLanguage map[common.ProgrammingLanguage]common.OtelSdk `json:"otelSdkByLanguage"`
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions api/odigos/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions cli/cmd/resources/profiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,16 @@ var (
ShortDescription: "Populate the spans resource `host.name` attribute with value of `k8s.pod.name`",
ClientObject: &odigosv1alpha1.Processor{},
}
javaNativeInstrumentationsProfile = Profile{
ProfileName: common.ProfileName("java-native-instrumentations"),
ShortDescription: "Instrument Java applications using native instrumentation and eBPF enterprise processing",
ClientObject: &odigosv1alpha1.InstrumentationRule{},
}

kratosProfile = Profile{
ProfileName: common.ProfileName("kratos"),
ShortDescription: "Bundle profile that includes full-payload-collection, semconv, category-attributes, copy-scope, hostname-as-podname",
Dependencies: []common.ProfileName{"full-payload-collection", "semconv", "category-attributes", "copy-scope", "hostname-as-podname"},
ShortDescription: "Bundle profile that includes full-payload-collection, semconv, category-attributes, copy-scope, hostname-as-podname, java-native-instrumentations",
Dependencies: []common.ProfileName{"full-payload-collection", "semconv", "category-attributes", "copy-scope", "hostname-as-podname", "java-native-instrumentations"},
}
)

Expand All @@ -59,7 +64,7 @@ func GetAvailableCommunityProfiles() []Profile {
}

func GetAvailableOnPremProfiles() []Profile {
return append([]Profile{fullPayloadCollectionProfile, categoryAttributesProfile, hostnameAsPodNameProfile, kratosProfile},
return append([]Profile{fullPayloadCollectionProfile, categoryAttributesProfile, hostnameAsPodNameProfile, javaNativeInstrumentationsProfile, kratosProfile},
GetAvailableCommunityProfiles()...)
}

Expand Down
12 changes: 12 additions & 0 deletions cli/cmd/resources/profiles/java-native-instrumentations.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: odigos.io/v1alpha1
kind: InstrumentationRule
metadata:
name: java-native-instrumentations
spec:
ruleName: "java native instrumentations"
notes: "Auto generated rule from java-native-instrumentations profile. Do not edit."
otelSdks:
otelSdkByLanguage:
java:
sdkTier: "enterprise"
sdkType: "native"
34 changes: 12 additions & 22 deletions instrumentor/controllers/instrumentationconfig/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
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"
)

Expand Down Expand Up @@ -37,26 +38,30 @@ func updateInstrumentationConfigForWorkload(ic *odigosv1alpha1.InstrumentationCo
continue
}
// filter out rules where the workload does not match
participating := isWorkloadParticipatingInRule(workload, rule)
participating := utils.IsWorkloadParticipatingInRule(workload, rule)
if !participating {
continue
}

for i := range sdkConfigs {
if rule.Spec.InstrumentationLibraries == nil { // nil means a rule in SDK level, that applies unless overridden by library level rule
sdkConfigs[i].DefaultPayloadCollection.HttpRequest = mergeHttpPayloadCollectionRules(sdkConfigs[i].DefaultPayloadCollection.HttpRequest, rule.Spec.PayloadCollection.HttpRequest)
sdkConfigs[i].DefaultPayloadCollection.HttpResponse = mergeHttpPayloadCollectionRules(sdkConfigs[i].DefaultPayloadCollection.HttpResponse, rule.Spec.PayloadCollection.HttpResponse)
sdkConfigs[i].DefaultPayloadCollection.DbQuery = mergeDbPayloadCollectionRules(sdkConfigs[i].DefaultPayloadCollection.DbQuery, rule.Spec.PayloadCollection.DbQuery)
if rule.Spec.PayloadCollection != nil {
sdkConfigs[i].DefaultPayloadCollection.HttpRequest = mergeHttpPayloadCollectionRules(sdkConfigs[i].DefaultPayloadCollection.HttpRequest, rule.Spec.PayloadCollection.HttpRequest)
sdkConfigs[i].DefaultPayloadCollection.HttpResponse = mergeHttpPayloadCollectionRules(sdkConfigs[i].DefaultPayloadCollection.HttpResponse, rule.Spec.PayloadCollection.HttpResponse)
sdkConfigs[i].DefaultPayloadCollection.DbQuery = mergeDbPayloadCollectionRules(sdkConfigs[i].DefaultPayloadCollection.DbQuery, rule.Spec.PayloadCollection.DbQuery)
}
} else {
for _, library := range *rule.Spec.InstrumentationLibraries {
libraryConfig := findOrCreateSdkLibraryConfig(&sdkConfigs[i], library)
if libraryConfig == nil {
// library is not relevant to this SDK
continue
}
libraryConfig.PayloadCollection.HttpRequest = mergeHttpPayloadCollectionRules(libraryConfig.PayloadCollection.HttpRequest, rule.Spec.PayloadCollection.HttpRequest)
libraryConfig.PayloadCollection.HttpResponse = mergeHttpPayloadCollectionRules(libraryConfig.PayloadCollection.HttpResponse, rule.Spec.PayloadCollection.HttpResponse)
libraryConfig.PayloadCollection.DbQuery = mergeDbPayloadCollectionRules(libraryConfig.PayloadCollection.DbQuery, rule.Spec.PayloadCollection.DbQuery)
if rule.Spec.PayloadCollection != nil {
libraryConfig.PayloadCollection.HttpRequest = mergeHttpPayloadCollectionRules(libraryConfig.PayloadCollection.HttpRequest, rule.Spec.PayloadCollection.HttpRequest)
libraryConfig.PayloadCollection.HttpResponse = mergeHttpPayloadCollectionRules(libraryConfig.PayloadCollection.HttpResponse, rule.Spec.PayloadCollection.HttpResponse)
libraryConfig.PayloadCollection.DbQuery = mergeDbPayloadCollectionRules(libraryConfig.PayloadCollection.DbQuery, rule.Spec.PayloadCollection.DbQuery)
}
}
}
}
Expand Down Expand Up @@ -106,21 +111,6 @@ func createDefaultSdkConfig(sdkConfigs []odigosv1alpha1.SdkConfig, containerLang
})
}

// naive implementation, can be optimized.
// assumption is that the list of workloads is small
func isWorkloadParticipatingInRule(workload workload.PodWorkload, rule *odigosv1alpha1.InstrumentationRule) bool {
// nil means all workloads are participating
if rule.Spec.Workloads == nil {
return true
}
for _, allowedWorkload := range *rule.Spec.Workloads {
if allowedWorkload == workload {
return true
}
}
return false
}

func mergeHttpPayloadCollectionRules(rule1 *instrumentationrules.HttpPayloadCollection, rule2 *instrumentationrules.HttpPayloadCollection) *instrumentationrules.HttpPayloadCollection {

// nil means a rules has not yet been set, so return the other rule
Expand Down
40 changes: 39 additions & 1 deletion instrumentor/controllers/instrumentationdevice/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package instrumentationdevice
import (
"context"
"errors"

odigosv1 "github.com/odigos-io/odigos/api/odigos/v1alpha1"
"github.com/odigos-io/odigos/common/consts"
"github.com/odigos-io/odigos/instrumentor/controllers/utils"
"github.com/odigos-io/odigos/instrumentor/controllers/utils/versionsupport"
"github.com/odigos-io/odigos/instrumentor/instrumentation"
"github.com/odigos-io/odigos/k8sutils/pkg/conditions"
Expand Down Expand Up @@ -74,18 +76,54 @@ func addInstrumentationDeviceToWorkload(ctx context.Context, kubeClient client.C
return err
}

workload := workload.PodWorkload{
Name: obj.GetName(),
Namespace: obj.GetNamespace(),
Kind: workload.WorkloadKind(obj.GetObjectKind().GroupVersionKind().Kind),
}

odigosConfig, err := k8sutils.GetCurrentOdigosConfig(ctx, kubeClient)
if err != nil {
return err
}

// build an otel sdk map from instrumentation rules first, and merge it with the default otel sdk map
// this way, we can override the default otel sdk with the instrumentation rules
instrumentationRules := odigosv1.InstrumentationRuleList{}
err = kubeClient.List(ctx, &instrumentationRules)
if err != nil {
return err
}

otelSdkToUse := odigosConfig.DefaultSDKs

for i := range instrumentationRules.Items {
instrumentationRule := &instrumentationRules.Items[i]
if instrumentationRule.Spec.Disabled || instrumentationRule.Spec.OtelSdks == nil {
// we only care about rules that have otel sdks configuration
continue
}

participating := utils.IsWorkloadParticipatingInRule(workload, instrumentationRule)
if !participating {
// filter rules that do not apply to the workload
continue
}

for lang, otelSdk := range instrumentationRule.Spec.OtelSdks.OtelSdkByLanguage {
// languages can override the default otel sdk or another rule.
// there is not check or warning if a language is defined in multiple rules at the moment.
otelSdkToUse[lang] = otelSdk
}
}

result, err := controllerutil.CreateOrPatch(ctx, kubeClient, obj, func() error {
podSpec, err := getPodSpecFromObject(obj)
if err != nil {
return err
}

return instrumentation.ApplyInstrumentationDevicesToPodTemplate(podSpec, runtimeDetails, odigosConfig.DefaultSDKs, obj)
return instrumentation.ApplyInstrumentationDevicesToPodTemplate(podSpec, runtimeDetails, otelSdkToUse, obj)
})

if err != nil {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package instrumentationdevice

import (
"context"

odigosv1 "github.com/odigos-io/odigos/api/odigos/v1alpha1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
)

type InstrumentationRuleReconciler struct {
client.Client
Scheme *runtime.Scheme
}

func (r *InstrumentationRuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {

var instApps odigosv1.InstrumentedApplicationList
if err := r.List(ctx, &instApps); err != nil {
return ctrl.Result{}, err
}
isNodeCollectorReady := isDataCollectionReady(ctx, r.Client)

for _, runtimeDetails := range instApps.Items {
err := reconcileSingleWorkload(ctx, r.Client, &runtimeDetails, isNodeCollectorReady)
if err != nil {
return ctrl.Result{}, err
}
}

logger := log.FromContext(ctx)
logger.V(0).Info("InstrumentationRule changed, recalculating instrumentation device for potential changes of otel sdks")

return ctrl.Result{}, nil
}
12 changes: 12 additions & 0 deletions instrumentor/controllers/instrumentationdevice/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,5 +135,17 @@ func SetupWithManager(mgr ctrl.Manager) error {
return err
}

err = builder.
ControllerManagedBy(mgr).
Named("instrumentationdevice-instrumentationrules").
For(&odigosv1.InstrumentationRule{}).
WithEventFilter(&utils.OtelSdkInstrumentationRulePredicate{}).
Complete(&InstrumentationRuleReconciler{
Client: mgr.GetClient(),
})
if err != nil {
return err
}

return nil
}
21 changes: 21 additions & 0 deletions instrumentor/controllers/utils/instrumentationrules.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package utils

import (
odigosv1alpha1 "github.com/odigos-io/odigos/api/odigos/v1alpha1"
"github.com/odigos-io/odigos/k8sutils/pkg/workload"
)

// naive implementation, can be optimized.
// assumption is that the list of workloads is small
func IsWorkloadParticipatingInRule(workload workload.PodWorkload, rule *odigosv1alpha1.InstrumentationRule) bool {
// nil means all workloads are participating
if rule.Spec.Workloads == nil {
return true
}
for _, allowedWorkload := range *rule.Spec.Workloads {
if allowedWorkload == workload {
return true
}
}
return false
}
Loading

0 comments on commit 228f166

Please sign in to comment.