Repository navigation
Conversation
8f5ef3c to
c59ec07
Compare
…va.profiling Lets the Helm chart turn on the profiler embedded in the ADOT Java agent (v2.32.0 and later) for Java-instrumented pods through a java.profiling block in --auto-instrumentation-config, with a minikube integration case. - main.go copies java.profiling.enabled to AUTO_INSTRUMENTATION_JAVA_PROFILING_ENABLED and the other keys, as JSON, to AUTO_INSTRUMENTATION_JAVA_PROFILING_SETTINGS - When enabled is exactly "true", the Java SDK injector sets OTEL_AWS_PROFILER_ENABLED=true and OTEL_AWS_PROFILER_ENDPOINT, unless the container already sets them - The endpoint is the cloudwatch-agent Service on the port of the cloudwatch-agent agent's opentelemetry.collect.otlp.http_endpoint, read from the AmazonCloudWatchAgent resource as the default Instrumentation already does, and 4318 when the config has no usable http_endpoint - Each profiled pod gets one disk-backed emptyDir, opentelemetry-profiler-data-java, mounted at /otel-profiler-data-java, with OTEL_AWS_PROFILER_DATA_DIR=/otel-profiler-data-java/<container-name>; its sizeLimit is volume_size_limit (default 160Mi, at least 16Mi, whole bytes) per profiled container - Every other java.profiling key becomes OTEL_AWS_PROFILER_<KEY> unless the container sets it; endpoint is refused, keys and values are validated, and keys ADOT v2.32.0 does not read are injected and logged - A container is skipped, with a log line and never rejected: when it sets OTEL_AWS_PROFILER_ENABLED to anything but "true" (the opt-out), on Windows or Fargate, when it may use OTLP gRPC (the profiler follows the OTLP protocol and the receiver is HTTP), with a Secret or prefixed envFrom, or when the volume name or mount path is already taken - integration-tests/java-profiling adds deployments, expectations and a validator that checks the exact OTEL_AWS_PROFILER_* set per container, the volume, its size and mounts, that other and init containers get nothing, and that an opted-out container keeps only its own OTEL_AWS_PROFILER_ENABLED=false - operator-integration-test.yml enables java.profiling on the manager, waits for the webhook to inject, and runs the profiled and opt-out cases - TestJavaProfilingIntegrationCase renders the same fixtures through the pod mutator in envtest, with controls that fail when profiling is off; TestJavaProfilingEndpointFollowsAgentConfig checks the endpoint against agent resources in envtest Nothing changes unless java.profiling.enabled is "true". Assisted-by: Claude Opus 5.5 Signed-off-by: Carlos Henriquez <carloshq@amazon.com>
3475553 to
e3913d0
Compare
There was a problem hiding this comment.
Thanks for removing the opt-out annotation and the profiling-specific receiver. Both are now in line with the default-on OTLP design. The comments below are what's left before this can merge. Several need matching changes in aws-observability/helm-charts#422, and each comment says which repo it applies to.
Blocking
1. The endpoint has to come from the chart, not from the agent config. (#441, #422)
For now, the Helm chart will manage the OTLP endpoint. The draft aws-observability/helm-charts#425 builds the receiver and its pipelines in the chart's collector YAML (spec.otelConfig, receiver otlp/cw_k8s_otlp) from the new otlp.* values. The agent's JSON config no longer has an opentelemetry block, and #425 refuses to render when the JSON has opentelemetry.collect.otlp and otlp.enabled is true.
This PR reads the port from opentelemetry.collect.otlp.http_endpoint in the JSON. Once #425 merges, the operator never finds that block and always falls back to 4318:
| Setting | Endpoint #441 injects | Result |
|---|---|---|
| #425 defaults | :4318 (fallback) |
Reaches the right receiver only by coincidence |
otlp.http.port=14318 |
:4318 |
Wrong port; profiles are lost |
otlp.http.enabled=false |
:4318 |
Nothing listens there |
The chart is what knows the endpoint, so it should pass it to the operator, for example in --auto-instrumentation-config.
- It already has
otlp.enabled,otlp.http.enabledandotlp.http.port. Until #425 ships, it has the custom JSONhttp_endpointinstead. - The operator then uses the endpoint it is given and stops reading the AmazonCloudWatchAgent resource for profiling. This also keeps the operator from depending on the chart's internal receiver names.
- The chart owns the receiver, so it can pass the matching payload cap with the endpoint (see item 4).
2. Gate injection on an OTLP HTTP endpoint existing. (#441, #422)
If the chart passes no endpoint, inject nothing, and drop the 4318 fallback. This follows the PR's own reasoning for skipping gRPC containers: injecting without a receiver costs the profiler's CPU and delivers nothing.
- The chart passes an endpoint only when
otlp.enabledandotlp.http.enabledare both true. Until #425 ships, the condition is a custom JSONhttp_endpoint. - Rendering should fail when
java.profiling.enabledis"true"and that condition does not hold.
3. data_dir writes outside the profiler volume. (#441, #422)
The volume is always mounted at /otel-profiler-data-java, but data_dir changes OTEL_AWS_PROFILER_DATA_DIR to <data_dir>/<container>. With data_dir: /tmp/prof, for example, profiler data goes to the container root filesystem. That fails with readOnlyRootFilesystem, and the sizeLimit no longer applies. Please remove the data_dir key.
4. Expose only enabled for this iteration. (#441, #422)
Today any key under java.profiling becomes OTEL_AWS_PROFILER_<KEY>. The chart only checks that the values are strings, and the operator injects unknown keys and logs a line. As a result, a typo such as wall_interval_msec renders, deploys and silently does nothing, and the chart's public interface becomes ADOT's env var names, which can change without any change on our side.
Please make the interface a schema, and for this iteration let it contain only enabled. Is there a setting you need beyond that? Our reasoning for the others:
volume_size_limit: the operator should own it. ADOT caps rotated files at 128 MiB, and the measured peaks were 27 MB and 70 MB, so the fixed 160Mi default covers ADOT's defaults. It only needs to come back if we expose settings that grow the data, such aswindow_secondsormemory_enabled.max_payload_bytes: this works around a mismatch between ADOT's 64 MiB default and the agent receiver's 20 MiB limit. A customer gains nothing by changing it, so the operator should always inject the cap the chart passes with the endpoint.data_dir: removed; see item 3.- ADOT settings (
mode,window_secondsand so on): add them one at a time when there is a use case. For each, decide whether it also brings backvolume_size_limit.
In the chart, reject any key other than enabled at render time (values.schema.json or a template check), and update the README and values.yaml to match. In the operator, drop the generic OTEL_AWS_PROFILER_<KEY> mapping.
5. Don't skip containers that use envFrom with a Secret or a prefix. (#441)
injectJavaProfiling skips any container with envFrom.secretRef or a prefixed envFrom. Many real workloads load credentials this way, so they are silently not profiled; the only sign is an Info log line in the operator. The existing javaagent and Application Signals injection don't skip these containers: their env entries override envFrom values, and that behavior is accepted. Please follow the same rule for profiling, and document that a container's own OTEL_AWS_PROFILER_ENABLED must be set in env or a ConfigMap to take precedence.
6. CI is failing. (#441)
- "Run Unit Tests" fails in
Test_OptOutByDisablingMonitorAllServices/StatefulSet/auto_restart_true(pkg/instrumentation/auto). - The NamespaceAnnotationsTest job fails in
TestAllLanguagesNamespace. - This PR touches neither package, and "Push on main" is green at
5710ae66. Please re-run them, or show that they fail on main too.
Summary
The ADOT Java agent embeds async-profiler from v2.32.0 and exports profiles over OTLP. This PR lets the operator turn it on for every Java-instrumented pod. It is driven by a new
java.profilingblock in--auto-instrumentation-config, which the Helm chart sets in aws-observability/helm-charts#422.Profiles go to the
cloudwatch-agentagent's OTLP HTTP endpoint (the shared OTLP endpoint; today enabled withagent.config: default:otelor a customhttp_endpoint), on the port that agent's config sets. The operator opens no receiver for profiling.Nothing changes unless
java.profiling.enabledis exactly"true". The agent's port and Service declarations (internal/manifests/collector) are untouched, so metrics, logs and traces behave as before.Order and dependencies.
One commit:
feat(instrumentation): enable the ADOT Java embedded profiler from java.profiling.What
main.go:setLangEnvVarscopiesjava.profiling.enabledtoAUTO_INSTRUMENTATION_JAVA_PROFILING_ENABLED, and every otherjava.profilingkey, as JSON, toAUTO_INSTRUMENTATION_JAVA_PROFILING_SETTINGS. Other languages are ignored.pkg/instrumentation/javaprofiling.go(new) is called from the Java branch ofsdkInjector.inject, after the javaagent is injected. For each instrumented Java container it:OTEL_AWS_PROFILER_ENABLED=trueandOTEL_AWS_PROFILER_ENDPOINT=http://cloudwatch-agent.amazon-cloudwatch:<port>/v1development/profiles.<port>is the port ofopentelemetry.collect.otlp.http_endpointin thecloudwatch-agentAmazonCloudWatchAgent's config. It is read with the existing client, like the default Instrumentation's endpoints, so no new RBAC is needed.http_endpoint.internalTrafficPolicy: Local, so profiles stay on the node.opentelemetry-profiler-data-java, mounted at/otel-profiler-data-java, and setsOTEL_AWS_PROFILER_DATA_DIR=/otel-profiler-data-java/<container-name>. Profiling then works withreadOnlyRootFilesystem, and the files stay off the javaagent volume;sizeLimit=volume_size_limitonce per profiled container, because each JVM keeps its own rotating JFR budget (128 MiB plus the current window). The default is160Mi, in whole bytes and at least16Mi; any other value is logged and the default is used;OTEL_AWS_PROFILER_<KEY upper-cased>:enabled;endpoint(refused, as is any key containing it);data_dir(an absolute base path);volume_size_limit.env,valueFromor a ConfigMapenvFrom.OTEL_AWS_PROFILER_ENABLEDto anything but"true"is not profiled. No pod or namespace annotation is read.kubernetes.io/osnode selector orspec.os.name).schedulerName: fargate-scheduleror theeks.amazonaws.com/fargate-profilelabel), which has no node-local agent.OTEL_EXPORTER_OTLP_PROTOCOL=grpc(fromenvorenvFrom, any case), a protocol fromvalueFrom(unknown), or-Dotel.exporter.otlp.protocol=grpcinJAVA_TOOL_OPTIONS. See "Why gRPC containers are skipped".envFrom, which the operator cannot resolve./otel-profiler-data-java.helper.go:defaultProfilerVolumeSize(160Mi).Why gRPC containers are skipped
The profiler follows the application's OTLP protocol, and the operator points it at the agent's OTLP HTTP endpoint. In ADOT Java v2.32.0 (
instrumentation/serviceevents/.../config/ServiceEventsConfig.java):profilerProtocol = parseProfilerProtocol(getConfigValue("OTEL_EXPORTER_OTLP_PROTOCOL")). Profiles have no per-signal protocol variable.getConfigValue: the system propertyotel.exporter.otlp.protocolis checked before the environment variable.parseProfilerProtocol: unset meanshttp/protobuf, andgrpc(any case) selectsOtlpGrpcProfilesExporter.resolveProfilerEndpoint:OTEL_AWS_PROFILER_ENDPOINTis used verbatim for both transports.A gRPC container pointed at the HTTP endpoint would pay the profiler's cost and deliver nothing, so it is skipped. A container that sets its own
OTEL_AWS_PROFILER_ENDPOINTis still profiled with that endpoint. Profiling gRPC apps, with a gRPC receiver and per-protocol routing, is planned as a separate follow-up.Why 160Mi per container
Measured data-dir peaks with ADOT's default settings were 27 MB on a Spring Boot service with 200 request threads, and 70 MB on a synthetic JVM with 100 threads. ADOT caps the rotated files at 128 MiB but never deletes the window being written. The limit is a ceiling, not a reservation. Exceeding it gets the pod evicted, which is why values under 16Mi are refused.
Integration test
A new case in the
InstrumentationTestjob ofoperator-integration-test.yml, after the existing language steps:--auto-instrumentation-configwithjava.profiling(enabled: "true",mode: "cpu",volume_size_limit: "64Mi") to the manager container, as the Helm chart does.OTEL_AWS_PROFILER_ENABLED. The pod webhook ignores failures, so readiness alone does not prove injection.sample-deployment-java-profiling.yamlhas two Java containers (app,worker) and a non-Javasidecar.go run ./integration-tests/java-profiling/cmdchecks every running pod againstjava_profiling_expectations.json:ENABLED,ENDPOINT,DATA_DIR=/otel-profiler-data-java/<container>andMODE=cpu, each set once;sizeLimit128Mi (2 x 64Mi);OTEL_AWS_PROFILER_ENABLED: "false"must keep only that variable, set once, with no other profiler variable, mount or volume.validate_instrumentation_vars.gois untouched.make test).TestJavaProfilingIntegrationCaserenders the same fixtures through the pod mutator, with controls that fail when profiling is off.TestJavaProfilingEndpointFollowsAgentConfigcreates AmazonCloudWatchAgent resources and checks the endpoint port for each of these cases:Testing
All of this was run locally on this PR's commit. The logs are local and not linkable; the PR's CI runs will add the links.
make teststeps andmake lintpass (Go 1.26.10):KUBEBUILDER_ASSETS;go testover all packages: 31 ok;api-docs(crdoc) could not be fetched; there are no CRD changes.make container. The workflow'srun:steps were run unchanged in golang:1.26.OTEL_AWS_PROFILER_ENABLED=false).make deploy; tool binaries were seeded to avoid GitHub rate limits.EKS-JavaProfilingmodule: Kubernetes 1.33, us-west-2, a test account, and one m5.2xlarge node.mainbuild from before v1.300075.0, which is not published yet). The agent's OTLP endpoint was enabled withagent.config: default:otel.default:otelOTLP HTTP endpoint on 4318.PutTraceSegmentscalls were rejected with 400. Traces did reach the agent on 4316. The traces check needs a run in the CI account, whose link will be added.mode, the chart'smax_payload_bytes, and 2 x 160Mi.ekse2e: N/A. That suite has no profiles validator yet, and its EKS tests install released operator and agent images, which do not have this feature.Response to the CloudWatch Agents team review
opentelemetry.collect.otlp.http_endpointof thecloudwatch-agentagent), falling back tocloudwatch-agent.amazon-cloudwatch:4318.java.profiling.OTEL_AWS_PROFILER_ENABLED=falseon the container. A namespace or workload opt-out is not needed, so noexcludesetting is added.monitorAllServicesscope. Done in test: fix flaky auto-monitor and TestRateLimit unit tests #422's README.max_payload_bytesdefaults to 20 MiB. ADOT checks the uncompressed payload against a 64 MiB default, but the agent's OTLP HTTP receiver caps the decompressed body at 20 MiB.cloudwatch-agentagent. Declined for scope: it is chart behavior beyond making ADOT send to the endpoint.True, and the operator argument.Notes for reviewers
JAVA_TOOL_OPTIONS, not from the command line,args,JDK_JAVA_OPTIONSor_JAVA_OPTIONS. A container that selects gRPC only there is profiled, and its exports to the HTTP receiver fail.OTEL_EXPORTER_OTLP_PROFILES_HEADERSorOTEL_EXPORTER_OTLP_HEADERSto profile exports. A container that sends OTLP to a third-party backend with such headers, and has no ownOTEL_AWS_PROFILER_ENDPOINT, sends those headers to the in-cluster agent.License
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
PR Checklist
makepasses locally (build, unit tests, lint): themake teststeps andmake lint(see Testing)operator-integration-test.ymlpasses;integration-tests/java-profiling) and in test: fix flaky auto-monitor and TestRateLimit unit tests #422's EKS module. amazon-cloudwatch-agent-test has no profiles validator, and its EKS tests use released images without this feature.javaprofiling_test.go,javaprofiling_endpoint_test.go,javaprofiling_integration_test.go,main_test.go,validate_test.gojava.profilingblock only; nothing changes when it is unset