Skip to content
Draft
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
20 changes: 20 additions & 0 deletions internal/manifests/collector/adapters/config_from.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ type collectD struct {

type AppSignals struct {
TLS *TLS `json:"tls,omitempty"`
// HostedIn is the Application Signals environment / cluster name the CloudWatch agent
// resolves into (set by the Helm chart from .Values.clusterName). Read-only here: the
// operator uses it to inject k8s.cluster.name into instrumented pods so the SDK resolves
// the same Environment the agent does. Does NOT affect the agent config itself.
HostedIn string `json:"hosted_in,omitempty"`
}

type emf struct {
Expand Down Expand Up @@ -159,3 +164,18 @@ func (c *CwaConfig) GetApplicationSignalsTracesConfig() *AppSignals {
}
return nil
}

// GetClusterName returns the cluster name the CloudWatch agent resolves Application Signals
// into, taken from application_signals.hosted_in (logs or traces) in the agent config — the
// SAME value the agent uses. Returns "" if not present. Read-only; does not modify the agent
// config. The operator injects this as k8s.cluster.name on instrumented pods so the SDK
// (ServiceEvents / Dynamic Instrumentation) resolves the same Environment as AppSignals.
func (c *CwaConfig) GetClusterName() string {
if as := c.GetApplicationSignalsMetricsConfig(); as != nil && as.HostedIn != "" {
return as.HostedIn
}
if as := c.GetApplicationSignalsTracesConfig(); as != nil && as.HostedIn != "" {
return as.HostedIn
}
return ""
}
45 changes: 44 additions & 1 deletion pkg/instrumentation/defaultinstrumentation.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"os"
"strings"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
Expand All @@ -32,6 +33,11 @@ const (
nodeJS = "NODEJS"
limit = "LIMIT"
request = "REQUEST"

// k8sModeEKS is the operator's k8s-mode value indicating an EKS cluster (mirrors the
// CloudWatch agent's KubernetesMode == ModeEKS); other modes (ROSA, native K8S) use the
// generic "k8s:" Environment prefix.
k8sModeEKS = "EKS"
)

func getInstrumentationConfigForResource(langStr string, resourceStr string) corev1.ResourceList {
Expand All @@ -50,7 +56,7 @@ func getInstrumentationConfigForResource(langStr string, resourceStr string) cor
return instrumentationConfigForResource
}

func getDefaultInstrumentation(agentConfig *adapters.CwaConfig, additionalEnvs map[Type]map[string]string, isWindowsPod bool) (*v1alpha1.Instrumentation, error) {
func getDefaultInstrumentation(agentConfig *adapters.CwaConfig, k8sMode string, additionalEnvs map[Type]map[string]string, isWindowsPod bool) (*v1alpha1.Instrumentation, error) {
javaInstrumentationImage, ok := os.LookupEnv("AUTO_INSTRUMENTATION_JAVA")
if !ok {
return nil, errors.New("unable to determine java instrumentation image")
Expand Down Expand Up @@ -85,6 +91,15 @@ func getDefaultInstrumentation(agentConfig *adapters.CwaConfig, additionalEnvs m
}
}

// Inject the cluster name (and, on EKS, cloud.platform=aws_eks) into instrumented pods so
// the SDK resolves the same Environment the CloudWatch agent does. The cluster name comes
// from the agent config's application_signals.hosted_in (the SAME value the agent uses) —
// the in-pod OTel EKS detector is RBAC-gated and the operator does not otherwise inject the
// cluster name. The eks-vs-k8s prefix is driven by k8sMode, mirroring the agent's
// KubernetesMode == ModeEKS ? "eks:" : "k8s:" logic (so AppSignals and
// ServiceEvents/DynamicInstrumentation stay consistent). See EKS_OPERATOR_CHANGE.md.
resourceAttributes := defaultResourceAttributes(agentConfig, k8sMode)

return &v1alpha1.Instrumentation{
Status: v1alpha1.InstrumentationStatus{},
TypeMeta: metav1.TypeMeta{
Expand All @@ -101,6 +116,9 @@ func getDefaultInstrumentation(agentConfig *adapters.CwaConfig, additionalEnvs m
v1alpha1.Baggage,
v1alpha1.XRay,
},
Resource: v1alpha1.Resource{
Attributes: resourceAttributes,
},
Java: v1alpha1.Java{
Image: javaInstrumentationImage,
Env: getJavaEnvs(isApplicationSignalsEnabled, cloudwatchAgentServiceEndpoint, exporterPrefix, additionalEnvs[TypeJava]),
Expand Down Expand Up @@ -137,6 +155,31 @@ func getDefaultInstrumentation(agentConfig *adapters.CwaConfig, additionalEnvs m
}, nil
}

// defaultResourceAttributes builds the resource attributes the operator injects into every
// instrumented pod's default Instrumentation. It sets k8s.cluster.name (from the agent
// config's hosted_in) so the SDK can resolve the platform-scoped Environment, and on EKS
// also cloud.platform=aws_eks so the SDK yields the "eks:" prefix (vs the generic "k8s:" for
// ROSA / native K8s) — mirroring the agent's KubernetesMode-driven prefix. Returns nil when
// no cluster name is available (e.g. EC2/on-prem-no-cluster), leaving resolution unchanged.
func defaultResourceAttributes(agentConfig *adapters.CwaConfig, k8sMode string) map[string]string {
if agentConfig == nil {
return nil
}
clusterName := agentConfig.GetClusterName()
if clusterName == "" {
return nil
}
attrs := map[string]string{
"k8s.cluster.name": clusterName,
}
// Only claim the EKS platform when the deployment is actually EKS; ROSA / native K8s must
// keep the generic "k8s:" prefix, matching the agent (KubernetesMode == ModeEKS ? eks : k8s).
if strings.EqualFold(k8sMode, k8sModeEKS) {
attrs["cloud.platform"] = "aws_eks"
}
return attrs
}

// getSharedOtlpEndpointEnvs returns the OTLP logs/metrics endpoints routed to the CloudWatch Agent.
// Shared by Service Events and Dynamic Instrumentation
func getSharedOtlpEndpointEnvs(cloudwatchAgentServiceEndpoint, exporterPrefix string) []corev1.EnvVar {
Expand Down
76 changes: 73 additions & 3 deletions pkg/instrumentation/defaultinstrumentation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ func Test_getDefaultInstrumentationLinux(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getDefaultInstrumentation(tt.args.agentConfig, nil, false)
got, err := getDefaultInstrumentation(tt.args.agentConfig, "", nil, false)
if (err != nil) != tt.wantErr {
t.Errorf("getDefaultInstrumentation() error = %v, wantErr %v", err, tt.wantErr)
return
Expand All @@ -365,6 +365,76 @@ func Test_getDefaultInstrumentationLinux(t *testing.T) {

}

func Test_defaultResourceAttributes(t *testing.T) {
withHostedIn := func(cluster string) *adapters.CwaConfig {
return &adapters.CwaConfig{
Logs: &adapters.Logs{
LogMetricsCollected: &adapters.LogMetricsCollected{
ApplicationSignals: &adapters.AppSignals{HostedIn: cluster},
},
},
}
}

tests := []struct {
name string
agentConfig *adapters.CwaConfig
k8sMode string
want map[string]string
}{
{
name: "nil agent config -> nil",
agentConfig: nil,
k8sMode: "EKS",
want: nil,
},
{
name: "no cluster name (no hosted_in) -> nil",
agentConfig: &adapters.CwaConfig{Logs: &adapters.Logs{LogMetricsCollected: &adapters.LogMetricsCollected{ApplicationSignals: &adapters.AppSignals{}}}},
k8sMode: "EKS",
want: nil,
},
{
name: "EKS -> cluster name + cloud.platform=aws_eks",
agentConfig: withHostedIn("my-cluster"),
k8sMode: "EKS",
want: map[string]string{"k8s.cluster.name": "my-cluster", "cloud.platform": "aws_eks"},
},
{
name: "EKS case-insensitive -> cloud.platform set",
agentConfig: withHostedIn("my-cluster"),
k8sMode: "eks",
want: map[string]string{"k8s.cluster.name": "my-cluster", "cloud.platform": "aws_eks"},
},
{
name: "native K8s -> cluster name only (k8s: prefix)",
agentConfig: withHostedIn("my-cluster"),
k8sMode: "K8S",
want: map[string]string{"k8s.cluster.name": "my-cluster"},
},
{
name: "ROSA -> cluster name only (k8s: prefix)",
agentConfig: withHostedIn("my-cluster"),
k8sMode: "ROSA",
want: map[string]string{"k8s.cluster.name": "my-cluster"},
},
{
name: "empty k8sMode -> cluster name only",
agentConfig: withHostedIn("my-cluster"),
k8sMode: "",
want: map[string]string{"k8s.cluster.name": "my-cluster"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := defaultResourceAttributes(tt.agentConfig, tt.k8sMode)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("defaultResourceAttributes() got = %v, want %v", got, tt.want)
}
})
}
}

func Test_getDefaultInstrumentationWindows(t *testing.T) {
_ = os.Setenv("AUTO_INSTRUMENTATION_JAVA", defaultJavaInstrumentationImage)
_ = os.Setenv("AUTO_INSTRUMENTATION_PYTHON", defaultPythonInstrumentationImage)
Expand Down Expand Up @@ -700,7 +770,7 @@ func Test_getDefaultInstrumentationWindows(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getDefaultInstrumentation(tt.args.agentConfig, nil, true)
got, err := getDefaultInstrumentation(tt.args.agentConfig, "", nil, true)
if (err != nil) != tt.wantErr {
t.Errorf("getDefaultInstrumentation() error = %v, wantErr %v", err, tt.wantErr)
return
Expand Down Expand Up @@ -939,7 +1009,7 @@ func Test_getDefaultInstrumentationLinuxWithApplicationSignalsDisabled(t *testin

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getDefaultInstrumentation(tt.args.agentConfig, tt.args.additionalEnvs, false)
got, err := getDefaultInstrumentation(tt.args.agentConfig, "", tt.args.additionalEnvs, false)
if (err != nil) != tt.wantErr {
t.Errorf("getDefaultInstrumentation() error = %v, wantErr %v", err, tt.wantErr)
return
Expand Down
7 changes: 6 additions & 1 deletion pkg/instrumentation/podmutator.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"errors"
"fmt"
"os"
"strings"

"github.com/go-logr/logr"
Expand Down Expand Up @@ -414,7 +415,11 @@ func (pm *instPodMutator) selectInstrumentationInstanceFromNamespace(ctx context
pm.Logger.Error(err, "unable to retrieve cloudwatch agent config for instrumentation")
}

return getDefaultInstrumentation(config, additionalEnvs, isWindowsPod)
// K8S_MODE (EKS | ROSA | K8S) is set on the operator by the Helm chart from
// .Values.k8sMode; it drives the eks: vs k8s: Environment prefix, mirroring the
// CloudWatch agent's KubernetesMode. Empty (e.g. non-K8s) → no platform claimed.
k8sMode := os.Getenv("K8S_MODE")
return getDefaultInstrumentation(config, k8sMode, additionalEnvs, isWindowsPod)
case s > 1:
return nil, errMultipleInstancesPossible
default:
Expand Down
2 changes: 1 addition & 1 deletion pkg/instrumentation/podmutator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const (
)

func TestGetInstrumentationInstanceFromNameSpaceDefault(t *testing.T) {
defaultInst, _ := getDefaultInstrumentation(&adapters.CwaConfig{}, nil, false)
defaultInst, _ := getDefaultInstrumentation(&adapters.CwaConfig{}, "", nil, false)
namespace := corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "default-namespace",
Expand Down
Loading