Conversation
exporters/common/src/main/java/io/opentelemetry/exporter/internal/grpc/GrpcExporterBuilder.java
Outdated
Show resolved
Hide resolved
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #8224 +/- ##
============================================
- Coverage 90.31% 90.29% -0.02%
- Complexity 7652 7682 +30
============================================
Files 843 848 +5
Lines 23087 23162 +75
Branches 2312 2334 +22
============================================
+ Hits 20851 20915 +64
- Misses 1517 1524 +7
- Partials 719 723 +4 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
exporters/common/src/main/java/io/opentelemetry/exporter/internal/grpc/GrpcExporterBuilder.java
Show resolved
Hide resolved
...ender/jdk/src/test/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSenderTest.java
Show resolved
Hide resolved
| @Nullable RetryPolicy retryPolicy, | ||
| @Nullable ExecutorService executorService) { | ||
| @Nullable ExecutorService executorService, | ||
| long maxResponseBodySize) { |
There was a problem hiding this comment.
JaegerRemoteSamplerBuilder.setMaxSamplingStrategyResponseBodySize validates bytes > 0. But the sender constructors (JdkHttpSender, OkHttpHttpSender, OkHttpGrpcSender) accept any long without
validation. A caller can bypass the guard by constructing a sender directly with 0 or -1. Consider adding the validation at the sender level too, or document the expected invariant.
There was a problem hiding this comment.
All the sender constructors are internal, and have a bunch of other unvalidated parameters which could be equally corrupted if a user goes around the guards. There's a conversation here discussing where to add additional null checks beyond the guarantees from nullaway. I think we should adopt a policy of adding additional null checks at the API boundaries, but trusting nullaway once we're within the walled garden of internal code. The same would apply to parameter validation, I think.
.../okhttp/src/main/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSender.java
Outdated
Show resolved
Hide resolved
...rs/sender/jdk/src/main/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSender.java
Show resolved
Hide resolved
| * Sets the maximum number of bytes to read from a sampling strategy response body. If unset, | ||
| * defaults to 4 MiB. Must be positive. | ||
| */ | ||
| public JaegerRemoteSamplerBuilder setMaxSamplingStrategyResponseBodySize(long bytes) { |
There was a problem hiding this comment.
GrpcExporterBuilder and HttpExporterBuilder hardcode 4MB with no setter. Only JaegerRemoteSamplerBuilder exposes a public setter. If a user wants to tighten the OTLP limit (say, to 64KB since responses are tiny), they can't. Worth a deliberate decision: intentionally omit setters for OTLP (since responses are well-known small), or add them for consistency?
There was a problem hiding this comment.
I omitted them in OTLP for now because we don't do anything with them and a 4mb limit is an improvement on the current no limit.
In contrast, for JaegerRemoteSampler, the default body size reduction is a change in behavior which could be meaningful so adding the setter seems essential.
…y-java into response-body-bounds
…alue GrpcResponse, HttpResponse
|
I had an oversight where I was enforcing the size limit against the response pre-decompression. This means that a response payload at the limit of 4mb, could decompress to a much larger size and have a big impact on memory. Fixing this means more complexity, including new enforcement that we only support gzip compressed resposnes. This issue has turned in quite the rabbit hole. On the bright side, there's pretty good abstract tests in AbstractHttpTelemetryExporterTest / AbstractGrpcTelemetryExporterTest that verify that all the sender implementations are doing the right thing. |
There was a problem hiding this comment.
Pull request overview
Adds response-body size bounding (default 4 MiB) to HTTP/gRPC sender configurations and updates sender implementations/tests to enforce the limit and handle gzip-encoded responses, aligning behavior with the spec guidance (HTTP: fail w/ exception; gRPC: fail w/ RESOURCE_EXHAUSTED).
Changes:
- Introduce
getMaxResponseBodySize()onHttpSenderConfig/GrpcSenderConfig(default 4 MiB) and document sender expectations. - Enforce bounded response reads (and gzip handling) in OkHttp and JDK senders; add Jaeger Remote Sampler builder option for sampling strategy response size.
- Extend OTLP exporter test infrastructure to generate oversized responses and validate failure behavior (including gzip cases).
Reviewed changes
Copilot reviewed 36 out of 36 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| sdk/common/src/main/java/io/opentelemetry/sdk/common/export/HttpSenderConfig.java | Adds configurable max response body size (default 4 MiB). |
| sdk/common/src/main/java/io/opentelemetry/sdk/common/export/HttpSender.java | Documents max-sized response body expectation. |
| sdk/common/src/main/java/io/opentelemetry/sdk/common/export/GrpcSenderConfig.java | Adds configurable max response body size (default 4 MiB). |
| sdk/common/src/main/java/io/opentelemetry/sdk/common/export/GrpcSender.java | Documents max-sized response message expectation. |
| sdk-extensions/jaeger-remote-sampler/.../JaegerRemoteSamplerBuilder.java | Adds sampler-specific response body size setting and wires into sender config. |
| exporters/sender/okhttp/.../OkHttpHttpSuppressionTest.java | Updates constructor usage with new max-size param. |
| exporters/sender/okhttp/.../OkHttpGrpcSuppressionTest.java | Updates constructor usage with new max-size param. |
| exporters/sender/okhttp/.../OkHttpGrpcSenderTest.java | Updates constructor usage with new max-size param. |
| exporters/sender/okhttp/.../OkHttpHttpSenderProvider.java | Passes max response body size into sender. |
| exporters/sender/okhttp/.../OkHttpHttpSender.java | Enforces bounded response reads + gzip handling. |
| exporters/sender/okhttp/.../OkHttpGrpcSenderProvider.java | Passes max response body size into sender. |
| exporters/sender/okhttp/.../OkHttpGrpcSender.java | Enforces bounded response reads + gzip handling for gRPC framing. |
| exporters/sender/okhttp/.../ImmutableHttpResponse.java | Introduces AutoValue HTTP response wrapper for OkHttp sender. |
| exporters/sender/okhttp/.../ImmutableGrpcResponse.java | Introduces AutoValue gRPC response wrapper for OkHttp sender. |
| exporters/sender/okhttp/build.gradle.kts | Adds AutoValue annotation processor. |
| exporters/sender/jdk/.../JdkHttpSenderTest.java | Updates constructor usage with new max-size param. |
| exporters/sender/jdk/.../JdkHttpSenderProvider.java | Passes max response body size into sender. |
| exporters/sender/jdk/.../JdkHttpSender.java | Enforces bounded response reads + gzip handling using JDK HttpClient. |
| exporters/sender/jdk/.../ImmutableHttpResponse.java | Introduces AutoValue HTTP response wrapper for JDK sender. |
| exporters/sender/jdk/build.gradle.kts | Adds AutoValue annotation processor. |
| exporters/sender/grpc-managed-channel/.../UpstreamGrpcSender.java | Minor import cleanup; sender is relevant for new response-bound feature. |
| exporters/sender/grpc-managed-channel/.../ImmutableGrpcResponse.java | Moves/adjusts internal ImmutableGrpcResponse type. |
| exporters/sender/grpc-managed-channel/build.gradle.kts | Adds AutoValue annotation processor. |
| exporters/otlp/testing-internal/.../TelemetryExporter.java | Adds ability for tests to generate response messages of a minimum size. |
| exporters/otlp/testing-internal/.../ManagedChannelTelemetryExporterBuilder.java | Sets max inbound message size and forwards new test helper method. |
| exporters/otlp/testing-internal/.../AbstractHttpTelemetryExporterTest.java | Adds HTTP response-bound and content-encoding tests. |
| exporters/otlp/testing-internal/.../AbstractGrpcTelemetryExporterTest.java | Adds gRPC response-bound and encoding tests. |
| exporters/otlp/profiles/.../OtlpGrpcProfileExporterTest.java | Updates test helper naming (addGrpcResponse). |
| exporters/otlp/all/.../OltpExporterBenchmark.java | Updates OkHttp sender construction for new max-size parameter. |
| exporters/common/src/test/.../GrpcExporterTest.java | Updates test to avoid depending on internal ImmutableGrpcResponse. |
| exporters/common/.../ImmutableHttpSenderConfig.java | Wires max response body size into HttpSenderConfig AutoValue config. |
| exporters/common/.../HttpExporterBuilder.java | Hardcodes 4 MiB bound for OTLP HTTP exporters for now. |
| exporters/common/.../ImmutableGrpcSenderConfig.java | Wires max response body size into GrpcSenderConfig AutoValue config. |
| exporters/common/.../GrpcExporterBuilder.java | Hardcodes 4 MiB bound for OTLP gRPC exporters for now. |
| docs/apidiffs/.../opentelemetry-sdk-extension-jaeger-remote-sampler.txt | Records new Jaeger builder API. |
| docs/apidiffs/.../opentelemetry-sdk-common.txt | Records new sender config APIs. |
Comments suppressed due to low confidence (1)
exporters/sender/grpc-managed-channel/src/main/java/io/opentelemetry/exporter/sender/grpc/managedchannel/internal/ImmutableGrpcResponse.java:23
@AutoValuerequires accessor methods for all properties. This class only declaresgetResponseMessage(), butGrpcResponsealso requiresgetStatusCode()andgetStatusDescription(). As written, AutoValue will not generate a type compatible with thecreate(statusCode, statusDescription, responseMessage)factory (compile error). Add abstract accessors for status code and description (and override them) so AutoValue can generate the correct implementation.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| static ImmutableHttpResponse create(int statusCode, String statusMessage, byte[] responseBody) { | ||
| return new AutoValue_ImmutableHttpResponse(statusCode, statusMessage, responseBody); | ||
| } | ||
|
|
There was a problem hiding this comment.
@AutoValue requires accessor methods for all properties. This class only declares getResponseBody(), but HttpResponse also requires getStatusCode() and getStatusMessage(). As written, the generated AutoValue_ImmutableHttpResponse will not match the create(statusCode, statusMessage, responseBody) factory and this will fail to compile. Add abstract accessors for statusCode and statusMessage (and annotate/override them) so AutoValue can generate the correct implementation.
| @Override | |
| public abstract int getStatusCode(); | |
| @Override | |
| public abstract String getStatusMessage(); |
| GrpcStatusCode statusCode, @Nullable String statusDescription, byte[] responseMessage) { | ||
| return new AutoValue_ImmutableGrpcResponse(statusCode, statusDescription, responseMessage); | ||
| } | ||
|
|
There was a problem hiding this comment.
@AutoValue requires accessor methods for all properties. This class only declares getResponseMessage(), but GrpcResponse also requires getStatusCode() and getStatusDescription(). The generated AutoValue_ImmutableGrpcResponse therefore won’t match the create(statusCode, statusDescription, responseMessage) factory and will fail compilation. Add abstract accessors for status code and description (and override them) so AutoValue can generate the intended value type.
| @Override | |
| public abstract GrpcStatusCode getStatusCode(); | |
| @Nullable | |
| @Override | |
| public abstract String getStatusDescription(); |
| static ImmutableHttpResponse create(int statusCode, String statusMessage, byte[] responseBody) { | ||
| return new AutoValue_ImmutableHttpResponse(statusCode, statusMessage, responseBody); | ||
| } | ||
|
|
There was a problem hiding this comment.
@AutoValue requires accessor methods for all properties. This class only declares getResponseBody(), but HttpResponse also requires getStatusCode() and getStatusMessage(). With the current definition, the generated AutoValue_ImmutableHttpResponse won’t have the expected constructor signature used by create(...), causing a compile error. Add abstract accessors for status code and status message (and override them) so AutoValue can generate the correct implementation.
| @Override | |
| public abstract int getStatusCode(); | |
| @Override | |
| public abstract String getStatusMessage(); |
| long readUpTo = | ||
| maxResponseBodySize == Long.MAX_VALUE ? Long.MAX_VALUE : maxResponseBodySize + 1; | ||
| Buffer wireBuffer = new Buffer(); | ||
| try { | ||
| while (wireBuffer.size() <= maxResponseBodySize) { | ||
| long n = body.source().read(wireBuffer, readUpTo - wireBuffer.size()); | ||
| if (n == -1L) { |
There was a problem hiding this comment.
The response-size limit is currently applied to the wire buffer size (which includes the 5-byte gRPC frame header). This effectively reduces the allowed unframed message size by 5 bytes and can incorrectly fail responses whose protobuf message is exactly maxResponseBodySize. Consider allowing an extra 5 bytes (plus the overflow byte) for the frame header when bounding wireBuffer, and enforce maxResponseBodySize on the unframed/decompressed bodyBytes instead.
| import io.grpc.StatusException; | ||
| import io.grpc.StatusRuntimeException; | ||
| import io.grpc.stub.ClientCalls; | ||
| import io.grpc.stub.MetadataUtils; | ||
| import io.opentelemetry.exporter.internal.grpc.ImmutableGrpcResponse; | ||
| import io.opentelemetry.exporter.internal.grpc.MarshalerInputStream; | ||
| import io.opentelemetry.sdk.common.CompletableResultCode; | ||
| import io.opentelemetry.sdk.common.export.GrpcResponse; |
There was a problem hiding this comment.
This sender still reads the entire gRPC response into memory with no size bound (RESPONSE_MARSHALER.parse loops until EOF). With GrpcSenderConfig#getMaxResponseBodySize() added in this PR, UpstreamGrpcSender should enforce that limit (and map overflow to RESOURCE_EXHAUSTED per the spec) to avoid unbounded heap allocation.
| // We have a 4mb hardcoded response body limit. Responses <= 4mb succeed. Responses >= 4mb | ||
| // succeed. We can't test payloads exactly at 4mb because protobuf message lengths are finicky - | ||
| // its hard to create a message with an exact size. | ||
|
|
||
| // Body below the limit, succeeds | ||
| addHttpResponse(200, exporter.exportResponse(100)); | ||
| CompletableResultCode result = | ||
| exporter | ||
| .export(Collections.singletonList(generateFakeTelemetry())) | ||
| .join(10, TimeUnit.SECONDS); | ||
| assertThat(result.isSuccess()).isTrue(); | ||
|
|
||
| // Body over the limit, fails with RESOURCE_EXHAUSTED | ||
| addHttpResponse(200, exporter.exportResponse(4 * 1024 * 1024 + 1)); |
There was a problem hiding this comment.
This comment is internally inconsistent: it says responses <= 4mb succeed and responses >= 4mb succeed. Also, for HTTP the failure mode here is an exception, not RESOURCE_EXHAUSTED. Please correct the comment to reflect the intended boundary behavior and terminology so the test remains clear.
| // We have a 4mb hardcoded response body limit. Responses <= 4mb succeed. Responses >= 4mb | ||
| // succeed. We can't test payloads exactly at 4mb because protobuf message lengths are finicky - | ||
| // its hard to create a message with an exact size. | ||
|
|
||
| // Body below the limit, succeeds | ||
| addGrpcResponse(GrpcStatusCode.OK, null, exporter.exportResponse(100)); | ||
| CompletableResultCode result = | ||
| exporter | ||
| .export(Collections.singletonList(generateFakeTelemetry())) | ||
| .join(10, TimeUnit.SECONDS); | ||
| assertThat(result.isSuccess()).isTrue(); | ||
|
|
||
| // Body over the limit, fails with RESOURCE_EXHAUSTED | ||
| addGrpcResponse(GrpcStatusCode.OK, null, exporter.exportResponse(4 * 1024 * 1024 + 1)); |
There was a problem hiding this comment.
This comment is internally inconsistent: it says responses <= 4mb succeed and responses >= 4mb succeed. Please correct it to reflect the intended boundary behavior (i.e., over-the-limit should fail) so the test documents the expected behavior accurately.
Add new max response body size parameter to sender config, defaulting to 4mb.
Per the spec, if a response exceeding the limit is received:
For OTLP exporters, the body size is not configurable, since we don't do anything with the response today. If adding this 4mb limit causes new exceptions for users, we can add a config option to allow it to be increased.
For JaegerRemoteSampler, a new programmatic config option is added because the responses are critical to the function of the sampler.
cc @brunobat you'll want to update the vertex sender to incorporate this new option.