Skip to content

Commit ee9b063

Browse files
Use HTTP error bodies in HttpExporter warnings (#8428)
1 parent 5fc623a commit ee9b063

2 files changed

Lines changed: 126 additions & 2 deletions

File tree

exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/HttpExporter.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import io.opentelemetry.sdk.common.internal.ThrottlingLogger;
1919
import java.io.IOException;
2020
import java.net.URI;
21+
import java.nio.charset.StandardCharsets;
2122
import java.util.concurrent.atomic.AtomicBoolean;
2223
import java.util.function.Supplier;
2324
import java.util.logging.Level;
@@ -32,6 +33,8 @@
3233
*/
3334
@SuppressWarnings("checkstyle:JavadocMethod")
3435
public final class HttpExporter {
36+
// Limit logged response body text to avoid flooding warnings with large payloads.
37+
private static final int MAX_RESPONSE_BODY_LOG_LENGTH = 1024;
3538

3639
private static final Logger internalLogger = Logger.getLogger(HttpExporter.class.getName());
3740

@@ -132,10 +135,23 @@ private static String extractErrorStatus(String statusMessage, @Nullable byte[]
132135
if (responseBody == null) {
133136
return "Response body missing, HTTP status message: " + statusMessage;
134137
}
138+
if (responseBody.length == 0) {
139+
return "Response body has 0 length, HTTP status message: " + statusMessage;
140+
}
135141
try {
136142
return GrpcExporterUtil.getStatusMessage(responseBody);
137143
} catch (IOException e) {
138-
return "Unable to parse response body, HTTP status message: " + statusMessage;
144+
return extractResponseBodyMessage(responseBody, statusMessage);
145+
}
146+
}
147+
148+
private static String extractResponseBodyMessage(byte[] responseBody, String statusMessage) {
149+
int lengthToRead = Math.min(responseBody.length, MAX_RESPONSE_BODY_LOG_LENGTH);
150+
String responseBodyText =
151+
new String(responseBody, 0, lengthToRead, StandardCharsets.UTF_8).trim();
152+
if (responseBodyText.isEmpty()) {
153+
return "HTTP status message: " + statusMessage;
139154
}
155+
return "Response body: " + responseBodyText + ", HTTP status message: " + statusMessage;
140156
}
141157
}

exporters/otlp/all/src/test/java/io/opentelemetry/exporter/otlp/internal/HttpExporterTest.java

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
import static org.mockito.ArgumentMatchers.any;
1010
import static org.mockito.Mockito.doAnswer;
1111

12+
import io.github.netmikey.logunit.api.LogCapturer;
1213
import io.opentelemetry.api.common.Attributes;
14+
import io.opentelemetry.api.metrics.MeterProvider;
1315
import io.opentelemetry.exporter.internal.marshal.Marshaler;
1416
import io.opentelemetry.internal.testing.slf4j.SuppressLogger;
1517
import io.opentelemetry.sdk.common.InternalTelemetryVersion;
@@ -22,13 +24,20 @@
2224
import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
2325
import java.io.IOException;
2426
import java.net.URI;
27+
import java.nio.charset.StandardCharsets;
28+
import java.util.concurrent.TimeUnit;
2529
import java.util.function.Consumer;
30+
import javax.annotation.Nullable;
31+
import org.junit.jupiter.api.Test;
32+
import org.junit.jupiter.api.extension.RegisterExtension;
2633
import org.junit.jupiter.params.ParameterizedTest;
2734
import org.junit.jupiter.params.provider.EnumSource;
2835
import org.mockito.Mockito;
2936

3037
class HttpExporterTest {
3138

39+
@RegisterExtension LogCapturer logs = LogCapturer.create().captureForType(HttpExporter.class);
40+
3241
@ParameterizedTest
3342
@EnumSource
3443
@SuppressLogger(HttpExporter.class)
@@ -197,14 +206,112 @@ void testInternalTelemetry(StandardComponentId.ExporterType exporterType) {
197206
}
198207
}
199208

209+
@Test
210+
@SuppressLogger(HttpExporter.class)
211+
void export_httpJsonErrorBodyUsesBodyTextWithoutGrpcParseWarning() {
212+
HttpSender mockSender = Mockito.mock(HttpSender.class);
213+
Marshaler mockMarshaller = Mockito.mock(Marshaler.class);
214+
HttpExporter exporter =
215+
new HttpExporter(
216+
ComponentId.generateLazy(StandardComponentId.ExporterType.OTLP_HTTP_SPAN_EXPORTER),
217+
mockSender,
218+
MeterProvider::noop,
219+
InternalTelemetryVersion.LATEST,
220+
URI.create("http://testing:1234"),
221+
false);
222+
223+
doAnswer(
224+
invoc -> {
225+
Consumer<HttpResponse> onResponse = invoc.getArgument(1);
226+
onResponse.accept(
227+
new FakeHttpResponse(
228+
500,
229+
"Internal Server Error",
230+
"{\"error\":\"grpc not supported\"}".getBytes(StandardCharsets.UTF_8)));
231+
return null;
232+
})
233+
.when(mockSender)
234+
.send(any(), any(), any());
235+
236+
assertThat(exporter.export(mockMarshaller, 1).join(10, TimeUnit.SECONDS).isSuccess()).isFalse();
237+
238+
logs.assertContains("Response body: {\"error\":\"grpc not supported\"}");
239+
logs.assertDoesNotContain("Unable to parse response body");
240+
}
241+
242+
@Test
243+
@SuppressLogger(HttpExporter.class)
244+
void export_nullErrorBodyUsesMissingBodyMessage() {
245+
HttpSender mockSender = Mockito.mock(HttpSender.class);
246+
Marshaler mockMarshaller = Mockito.mock(Marshaler.class);
247+
HttpExporter exporter =
248+
new HttpExporter(
249+
ComponentId.generateLazy(StandardComponentId.ExporterType.OTLP_HTTP_SPAN_EXPORTER),
250+
mockSender,
251+
MeterProvider::noop,
252+
InternalTelemetryVersion.LATEST,
253+
URI.create("http://testing:1234"),
254+
false);
255+
256+
doAnswer(
257+
invoc -> {
258+
Consumer<HttpResponse> onResponse = invoc.getArgument(1);
259+
onResponse.accept(new FakeHttpResponse(500, "Internal Server Error", null));
260+
return null;
261+
})
262+
.when(mockSender)
263+
.send(any(), any(), any());
264+
265+
assertThat(exporter.export(mockMarshaller, 1).join(10, TimeUnit.SECONDS).isSuccess()).isFalse();
266+
267+
logs.assertContains("Response body missing, HTTP status message: Internal Server Error");
268+
}
269+
270+
@Test
271+
@SuppressLogger(HttpExporter.class)
272+
void export_whitespaceErrorBodyFallsBackToStatusMessage() {
273+
HttpSender mockSender = Mockito.mock(HttpSender.class);
274+
Marshaler mockMarshaller = Mockito.mock(Marshaler.class);
275+
HttpExporter exporter =
276+
new HttpExporter(
277+
ComponentId.generateLazy(StandardComponentId.ExporterType.OTLP_HTTP_SPAN_EXPORTER),
278+
mockSender,
279+
MeterProvider::noop,
280+
InternalTelemetryVersion.LATEST,
281+
URI.create("http://testing:1234"),
282+
false);
283+
284+
doAnswer(
285+
invoc -> {
286+
Consumer<HttpResponse> onResponse = invoc.getArgument(1);
287+
onResponse.accept(
288+
new FakeHttpResponse(
289+
500, "Internal Server Error", " ".getBytes(StandardCharsets.UTF_8)));
290+
return null;
291+
})
292+
.when(mockSender)
293+
.send(any(), any(), any());
294+
295+
assertThat(exporter.export(mockMarshaller, 1).join(10, TimeUnit.SECONDS).isSuccess()).isFalse();
296+
297+
logs.assertContains("HTTP status message: Internal Server Error");
298+
logs.assertDoesNotContain("Response body:");
299+
}
300+
200301
private static class FakeHttpResponse implements HttpResponse {
201302

202303
final int statusCode;
203304
final String statusMessage;
305+
@Nullable final byte[] responseBody;
204306

205307
FakeHttpResponse(int statusCode, String statusMessage) {
308+
this(statusCode, statusMessage, new byte[0]);
309+
}
310+
311+
FakeHttpResponse(int statusCode, String statusMessage, @Nullable byte[] responseBody) {
206312
this.statusCode = statusCode;
207313
this.statusMessage = statusMessage;
314+
this.responseBody = responseBody;
208315
}
209316

210317
@Override
@@ -218,8 +325,9 @@ public String getStatusMessage() {
218325
}
219326

220327
@Override
328+
@Nullable
221329
public byte[] getResponseBody() {
222-
return new byte[0];
330+
return responseBody;
223331
}
224332
}
225333
}

0 commit comments

Comments
 (0)