Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: otel sdk instrumentation rule #1499

Merged
merged 4 commits into from
Sep 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading