Skip to content

Commit b8dfc39

Browse files
jefchienmitali-salvi
authored andcommitted
Support JMX annotations.
1 parent 377104f commit b8dfc39

File tree

9 files changed

+179
-25
lines changed

9 files changed

+179
-25
lines changed

internal/manifests/collector/adapters/config_from.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ type Traces struct {
5858
type MetricsCollected struct {
5959
StatsD *statsD `json:"statsd,omitempty"`
6060
CollectD *collectD `json:"collectd,omitempty"`
61+
JMX *jmx `json:"jmx,omitempty"`
6162
}
6263

6364
type LogMetricsCollected struct {
@@ -86,6 +87,9 @@ type AppSignals struct {
8687

8788
type emf struct {
8889
}
90+
91+
type jmx struct{}
92+
8993
type kubernetes struct {
9094
EnhancedContainerInsights bool `json:"enhanced_container_insights,omitempty"`
9195
AcceleratedComputeMetrics bool `json:"accelerated_compute_metrics,omitempty"`

internal/manifests/collector/ports.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,24 @@ const (
2727
XrayTraces = "aws-traces"
2828
OtlpGrpc = "otlp-grpc"
2929
OtlpHttp = "otlp-http"
30-
AppSignalsGrpc = "appsig-grpc"
31-
AppSignalsHttp = "appsig-http"
32-
AppSignalsProxy = "appsig-xray"
30+
AppSignalsGrpc = "appsignals-grpc"
31+
AppSignalsHttp = "appsignals-http"
32+
AppSignalsProxy = "appsignals-xray"
3333
AppSignalsGrpcSA = ":4315"
3434
AppSignalsHttpSA = ":4316"
3535
AppSignalsProxySA = ":2000"
3636
EMF = "emf"
3737
EMFTcp = "emf-tcp"
3838
EMFUdp = "emf-udp"
3939
CWA = "cwa-"
40+
JmxHttp = "jmx-http"
4041
)
4142

4243
var receiverDefaultPortsMap = map[string]int32{
4344
StatsD: 8125,
4445
CollectD: 25826,
4546
XrayTraces: 2000,
47+
JmxHttp: 4314,
4648
OtlpGrpc: 4317,
4749
OtlpHttp: 4318,
4850
EMF: 25888,
@@ -83,6 +85,7 @@ func getContainerPorts(logger logr.Logger, cfg string, specPorts []corev1.Servic
8385
if err != nil {
8486
logger.Error(err, "error parsing cw agent config")
8587
} else {
88+
logger.Info("%v", config)
8689
servicePorts = getServicePortsFromCWAgentConfig(logger, config)
8790
}
8891

@@ -143,6 +146,9 @@ func getMetricsReceiversServicePorts(logger logr.Logger, config *adapters.CwaCon
143146
if config.Metrics.MetricsCollected.CollectD != nil {
144147
getReceiverServicePort(logger, config.Metrics.MetricsCollected.CollectD.ServiceAddress, CollectD, corev1.ProtocolUDP, servicePortsMap)
145148
}
149+
if config.Metrics.MetricsCollected.JMX != nil {
150+
getReceiverServicePort(logger, "", JmxHttp, corev1.ProtocolTCP, servicePortsMap)
151+
}
146152
}
147153

148154
func getReceiverServicePort(logger logr.Logger, serviceAddress string, receiverName string, protocol corev1.Protocol, servicePortsMap map[int32][]corev1.ServicePort) {

internal/manifests/collector/ports_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,21 @@ func TestInvalidConfigGetContainerPorts(t *testing.T) {
204204

205205
}
206206

207+
func TestJMXGetContainerPorts(t *testing.T) {
208+
cfg := getJSONStringFromFile("testdata/jmx.json")
209+
containerPorts := getContainerPorts(logger, cfg, []corev1.ServicePort{})
210+
assert.Equal(t, 4, len(containerPorts))
211+
assert.Equal(t, int32(4315), containerPorts[AppSignalsGrpc].ContainerPort)
212+
assert.Equal(t, AppSignalsGrpc, containerPorts[AppSignalsGrpc].Name)
213+
assert.Equal(t, int32(4316), containerPorts[AppSignalsHttp].ContainerPort)
214+
assert.Equal(t, AppSignalsHttp, containerPorts[AppSignalsHttp].Name)
215+
assert.Equal(t, int32(2000), containerPorts[AppSignalsProxy].ContainerPort)
216+
assert.Equal(t, AppSignalsProxy, containerPorts[AppSignalsProxy].Name)
217+
assert.Equal(t, int32(4314), containerPorts[JmxHttp].ContainerPort)
218+
assert.Equal(t, JmxHttp, containerPorts[JmxHttp].Name)
219+
assert.Equal(t, corev1.ProtocolTCP, containerPorts[JmxHttp].Protocol)
220+
}
221+
207222
func getJSONStringFromFile(path string) string {
208223
buf, err := os.ReadFile(path)
209224
if err != nil {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"metrics": {
3+
"metrics_collected": {
4+
"jmx": {
5+
}
6+
}
7+
}
8+
}

pkg/instrumentation/defaultinstrumentation.go

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ import (
88
"fmt"
99
"os"
1010

11-
"k8s.io/apimachinery/pkg/api/resource"
12-
1311
corev1 "k8s.io/api/core/v1"
12+
"k8s.io/apimachinery/pkg/api/resource"
1413
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1514

1615
"github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1"
1716
"github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/collector/adapters"
17+
"github.com/aws/amazon-cloudwatch-agent-operator/pkg/instrumentation/jmx"
1818
)
1919

2020
const (
@@ -50,7 +50,7 @@ func getInstrumentationConfigForResource(langStr string, resourceStr string) cor
5050
return instrumentationConfigForResource
5151
}
5252

53-
func getDefaultInstrumentation(agentConfig *adapters.CwaConfig, isWindowsPod bool) (*v1alpha1.Instrumentation, error) {
53+
func getDefaultInstrumentation(agentConfig *adapters.CwaConfig, additionalEnvs map[Type]map[string]string, isWindowsPod bool) (*v1alpha1.Instrumentation, error) {
5454
javaInstrumentationImage, ok := os.LookupEnv("AUTO_INSTRUMENTATION_JAVA")
5555
if !ok {
5656
return nil, errors.New("unable to determine java instrumentation image")
@@ -103,18 +103,7 @@ func getDefaultInstrumentation(agentConfig *adapters.CwaConfig, isWindowsPod boo
103103
},
104104
Java: v1alpha1.Java{
105105
Image: javaInstrumentationImage,
106-
Env: []corev1.EnvVar{
107-
{Name: "OTEL_AWS_APP_SIGNALS_ENABLED", Value: "true"}, //TODO: remove in favor of new name once safe
108-
{Name: "OTEL_AWS_APPLICATION_SIGNALS_ENABLED", Value: "true"},
109-
{Name: "OTEL_TRACES_SAMPLER_ARG", Value: fmt.Sprintf("endpoint=%s://%s:2000", http, cloudwatchAgentServiceEndpoint)},
110-
{Name: "OTEL_TRACES_SAMPLER", Value: "xray"},
111-
{Name: "OTEL_EXPORTER_OTLP_PROTOCOL", Value: "http/protobuf"},
112-
{Name: "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", Value: fmt.Sprintf("%s://%s:4316/v1/traces", exporterPrefix, cloudwatchAgentServiceEndpoint)},
113-
{Name: "OTEL_AWS_APP_SIGNALS_EXPORTER_ENDPOINT", Value: fmt.Sprintf("%s://%s:4316/v1/metrics", exporterPrefix, cloudwatchAgentServiceEndpoint)}, //TODO: remove in favor of new name once safe
114-
{Name: "OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT", Value: fmt.Sprintf("%s://%s:4316/v1/metrics", exporterPrefix, cloudwatchAgentServiceEndpoint)},
115-
{Name: "OTEL_METRICS_EXPORTER", Value: "none"},
116-
{Name: "OTEL_LOGS_EXPORTER", Value: "none"},
117-
},
106+
Env: getJavaEnvs(cloudwatchAgentServiceEndpoint, exporterPrefix, additionalEnvs[TypeJava]),
118107
Resources: corev1.ResourceRequirements{
119108
Limits: getInstrumentationConfigForResource(java, limit),
120109
Requests: getInstrumentationConfigForResource(java, request),
@@ -182,3 +171,38 @@ func getDefaultInstrumentation(agentConfig *adapters.CwaConfig, isWindowsPod boo
182171
},
183172
}, nil
184173
}
174+
175+
func getJavaEnvs(cloudwatchAgentServiceEndpoint, exporterPrefix string, additionalEnvs map[string]string) []corev1.EnvVar {
176+
envs := []corev1.EnvVar{
177+
{Name: "OTEL_AWS_APP_SIGNALS_ENABLED", Value: "true"}, //TODO: remove in favor of new name once safe
178+
{Name: "OTEL_AWS_APPLICATION_SIGNALS_ENABLED", Value: "true"},
179+
{Name: "OTEL_TRACES_SAMPLER_ARG", Value: fmt.Sprintf("endpoint=%s://%s:2000", http, cloudwatchAgentServiceEndpoint)},
180+
{Name: "OTEL_TRACES_SAMPLER", Value: "xray"},
181+
{Name: "OTEL_EXPORTER_OTLP_PROTOCOL", Value: "http/protobuf"},
182+
{Name: "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", Value: fmt.Sprintf("%s://%s:4316/v1/traces", exporterPrefix, cloudwatchAgentServiceEndpoint)},
183+
{Name: "OTEL_AWS_APP_SIGNALS_EXPORTER_ENDPOINT", Value: fmt.Sprintf("%s://%s:4316/v1/metrics", exporterPrefix, cloudwatchAgentServiceEndpoint)}, //TODO: remove in favor of new name once safe
184+
{Name: "OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT", Value: fmt.Sprintf("%s://%s:4316/v1/metrics", exporterPrefix, cloudwatchAgentServiceEndpoint)},
185+
}
186+
var jmxEnvs []corev1.EnvVar
187+
if targetSystems, ok := additionalEnvs[jmx.EnvTargetSystem]; ok {
188+
jmxEnvs = []corev1.EnvVar{
189+
{Name: "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", Value: fmt.Sprintf("%s://%s:4314/v1/metrics", http, cloudwatchAgentServiceEndpoint)},
190+
{Name: "OTEL_INSTRUMENTATION_RUNTIME_TELEMETRY_ENABLED", Value: "false"},
191+
{Name: "OTEL_INSTRUMENTATION_COMMON_DEFAULT_ENABLED", Value: "false"},
192+
{Name: "OTEL_JMX_ENABLED", Value: "true"},
193+
{Name: "OTEL_JMX_TARGET_SYSTEM", Value: targetSystems},
194+
{Name: "OTEL_EXPERIMENTAL_METRICS_VIEW_CONFIG", Value: "classpath:/jmx/view.yaml"},
195+
}
196+
}
197+
if len(jmxEnvs) == 0 {
198+
envs = append(
199+
envs,
200+
corev1.EnvVar{Name: "OTEL_METRICS_EXPORTER", Value: "none"},
201+
corev1.EnvVar{Name: "OTEL_LOGS_EXPORTER", Value: "none"},
202+
)
203+
} else {
204+
envs = append(envs, corev1.EnvVar{Name: "OTEL_LOGS_EXPORTER", Value: "none"})
205+
envs = append(envs, jmxEnvs...)
206+
}
207+
return envs
208+
}

pkg/instrumentation/defaultinstrumentation_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ func Test_getDefaultInstrumentationLinux(t *testing.T) {
327327
}
328328
for _, tt := range tests {
329329
t.Run(tt.name, func(t *testing.T) {
330-
got, err := getDefaultInstrumentation(tt.args.agentConfig, false)
330+
got, err := getDefaultInstrumentation(tt.args.agentConfig, nil, false)
331331
if (err != nil) != tt.wantErr {
332332
t.Errorf("getDefaultInstrumentation() error = %v, wantErr %v", err, tt.wantErr)
333333
return
@@ -650,7 +650,7 @@ func Test_getDefaultInstrumentationWindows(t *testing.T) {
650650
}
651651
for _, tt := range tests {
652652
t.Run(tt.name, func(t *testing.T) {
653-
got, err := getDefaultInstrumentation(tt.args.agentConfig, true)
653+
got, err := getDefaultInstrumentation(tt.args.agentConfig, nil, true)
654654
if (err != nil) != tt.wantErr {
655655
t.Errorf("getDefaultInstrumentation() error = %v, wantErr %v", err, tt.wantErr)
656656
return

pkg/instrumentation/jmx/jmx.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package jmx
5+
6+
const (
7+
annotationPrefix = "cloudwatch.aws.amazon.com/inject-jmx-"
8+
)
9+
10+
const (
11+
EnvTargetSystem = "OTEL_JMX_TARGET_SYSTEM"
12+
13+
TargetJVM = "jvm"
14+
TargetTomcat = "tomcat"
15+
TargetKafka = "kafka"
16+
TargetKafkaConsumer = "kafka-consumer"
17+
TargetKafkaProducer = "kafka-producer"
18+
)
19+
20+
var SupportedTargets = []string{TargetJVM, TargetTomcat, TargetKafka, TargetKafkaConsumer, TargetKafkaProducer}
21+
22+
func AnnotationKey(target string) string {
23+
return annotationPrefix + target
24+
}

pkg/instrumentation/podmutator.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/collector/adapters"
2020
"github.com/aws/amazon-cloudwatch-agent-operator/internal/webhook/podmutation"
2121
"github.com/aws/amazon-cloudwatch-agent-operator/pkg/featuregate"
22+
"github.com/aws/amazon-cloudwatch-agent-operator/pkg/instrumentation/jmx"
2223
)
2324

2425
const (
@@ -368,8 +369,19 @@ func (pm *instPodMutator) getInstrumentationInstance(ctx context.Context, ns cor
368369
return nil, nil
369370
}
370371

372+
var additionalEnvs map[Type]map[string]string
373+
if instAnnotation == annotationInjectJava {
374+
additionalEnvs = map[Type]map[string]string{}
375+
targetSystems := getJmxTargetSystems(ns, pod)
376+
if len(targetSystems) != 0 {
377+
additionalEnvs[TypeJava] = map[string]string{
378+
jmx.EnvTargetSystem: strings.Join(targetSystems, ","),
379+
}
380+
}
381+
}
382+
371383
if strings.EqualFold(instValue, "true") {
372-
return pm.selectInstrumentationInstanceFromNamespace(ctx, ns, isWindowsPod(pod))
384+
return pm.selectInstrumentationInstanceFromNamespace(ctx, ns, additionalEnvs, isWindowsPod(pod))
373385
}
374386

375387
var instNamespacedName types.NamespacedName
@@ -388,7 +400,7 @@ func (pm *instPodMutator) getInstrumentationInstance(ctx context.Context, ns cor
388400
return otelInst, nil
389401
}
390402

391-
func (pm *instPodMutator) selectInstrumentationInstanceFromNamespace(ctx context.Context, ns corev1.Namespace, isWindowsPod bool) (*v1alpha1.Instrumentation, error) {
403+
func (pm *instPodMutator) selectInstrumentationInstanceFromNamespace(ctx context.Context, ns corev1.Namespace, additionalEnvs map[Type]map[string]string, isWindowsPod bool) (*v1alpha1.Instrumentation, error) {
392404
var otelInsts v1alpha1.InstrumentationList
393405
if err := pm.Client.List(ctx, &otelInsts, client.InNamespace(ns.Name)); err != nil {
394406
return nil, err
@@ -403,7 +415,7 @@ func (pm *instPodMutator) selectInstrumentationInstanceFromNamespace(ctx context
403415
pm.Logger.Error(err, "unable to retrieve cloudwatch agent config for instrumentation")
404416
}
405417

406-
return getDefaultInstrumentation(config, isWindowsPod)
418+
return getDefaultInstrumentation(config, additionalEnvs, isWindowsPod)
407419
case s > 1:
408420
return nil, errMultipleInstancesPossible
409421
default:
@@ -425,3 +437,14 @@ func GetAmazonCloudWatchAgentResource(ctx context.Context, c client.Client, name
425437
func isWindowsPod(pod corev1.Pod) bool {
426438
return pod.Spec.NodeSelector["kubernetes.io/os"] == "windows"
427439
}
440+
441+
func getJmxTargetSystems(ns corev1.Namespace, pod corev1.Pod) []string {
442+
var targetSystems []string
443+
for _, target := range jmx.SupportedTargets {
444+
value := annotationValue(ns.ObjectMeta, pod.ObjectMeta, jmx.AnnotationKey(target))
445+
if strings.EqualFold(value, "true") {
446+
targetSystems = append(targetSystems, target)
447+
}
448+
}
449+
return targetSystems
450+
}

pkg/instrumentation/podmutator_test.go

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"os"
1010
"testing"
1111

12+
"github.com/aws/amazon-cloudwatch-agent-operator/pkg/instrumentation/jmx"
13+
1214
"github.com/go-logr/logr"
1315
"github.com/stretchr/testify/assert"
1416
"github.com/stretchr/testify/require"
@@ -31,7 +33,7 @@ const (
3133
)
3234

3335
func TestGetInstrumentationInstanceFromNameSpaceDefault(t *testing.T) {
34-
defaultInst, _ := getDefaultInstrumentation(&adapters.CwaConfig{}, false)
36+
defaultInst, _ := getDefaultInstrumentation(&adapters.CwaConfig{}, nil, false)
3537
namespace := corev1.Namespace{
3638
ObjectMeta: metav1.ObjectMeta{
3739
Name: "default-namespace",
@@ -45,12 +47,60 @@ func TestGetInstrumentationInstanceFromNameSpaceDefault(t *testing.T) {
4547
Client: fake.NewClientBuilder().Build(),
4648
Logger: logr.Logger{},
4749
}
48-
instrumentation, err := podMutator.selectInstrumentationInstanceFromNamespace(context.Background(), namespace, false)
50+
instrumentation, err := podMutator.selectInstrumentationInstanceFromNamespace(context.Background(), namespace, nil, false)
4951

5052
assert.Nil(t, err)
5153
assert.Equal(t, defaultInst, instrumentation)
5254
}
5355

56+
func TestGetInstrumentationInstanceJMX(t *testing.T) {
57+
if err := v1alpha1.AddToScheme(testScheme); err != nil {
58+
fmt.Printf("failed to register scheme: %v", err)
59+
os.Exit(1)
60+
}
61+
mutator := instPodMutator{
62+
Client: fake.NewClientBuilder().Build(),
63+
Logger: logr.Discard(),
64+
}
65+
66+
tests := []struct {
67+
name string
68+
pod corev1.Pod
69+
ns corev1.Namespace
70+
wantLen int
71+
wantEnv []corev1.EnvVar
72+
}{
73+
{
74+
name: "enable jvm/tomcat",
75+
pod: corev1.Pod{
76+
ObjectMeta: metav1.ObjectMeta{
77+
Annotations: map[string]string{
78+
annotationInjectJava: "true",
79+
jmx.AnnotationKey(jmx.TargetJVM): "true",
80+
jmx.AnnotationKey(jmx.TargetTomcat): "true",
81+
},
82+
},
83+
},
84+
ns: corev1.Namespace{},
85+
wantLen: 15,
86+
wantEnv: []corev1.EnvVar{
87+
{Name: "OTEL_JMX_ENABLED", Value: "true"},
88+
{Name: "OTEL_JMX_TARGET_SYSTEM", Value: "jvm,tomcat"},
89+
},
90+
},
91+
}
92+
for _, tt := range tests {
93+
t.Run(tt.name, func(t *testing.T) {
94+
inst, err := mutator.getInstrumentationInstance(context.Background(), tt.ns, tt.pod, annotationInjectJava)
95+
assert.NoError(t, err)
96+
assert.Len(t, inst.Spec.Java.Env, tt.wantLen)
97+
for _, env := range tt.wantEnv {
98+
assert.Containsf(t, inst.Spec.Java.Env, env, "java env does not contain %s:%s", env.Name, env.Value)
99+
}
100+
})
101+
}
102+
}
103+
54104
func TestMutatePod(t *testing.T) {
55105
mutator := NewMutator(logr.Discard(), k8sClient, record.NewFakeRecorder(100))
56106
require.NotNil(t, mutator)

0 commit comments

Comments
 (0)