Skip to content

Commit 1c63429

Browse files
Merge branch '1.8.x' into 1.9.x
2 parents 43aa16e + b1bb421 commit 1c63429

File tree

2 files changed

+206
-40
lines changed

2 files changed

+206
-40
lines changed

implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v1/DynatraceExporterV1.java

+60-26
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@
5353
*/
5454
public class DynatraceExporterV1 extends AbstractDynatraceExporter {
5555

56-
private static final int MAX_MESSAGE_SIZE = 15360; // max message size in bytes that
57-
// Dynatrace will accept
56+
// max message size in bytes that Dynatrace will accept
57+
private static final int MAX_MESSAGE_SIZE = 15360;
5858

5959
private final InternalLogger logger = InternalLoggerFactory.getInstance(DynatraceExporterV1.class);
6060

@@ -187,43 +187,73 @@ private boolean isCustomMetricCreated(final DynatraceTimeSeries timeSeries) {
187187

188188
// VisibleForTesting
189189
void putCustomMetric(final DynatraceMetricDefinition customMetric) {
190+
HttpSender.Request.Builder requestBuilder;
190191
try {
191-
httpClient
192+
requestBuilder = httpClient
192193
.put(customMetricEndpointTemplate + customMetric.getMetricId() + "?api-token=" + config.apiToken())
193-
.withJsonContent(customMetric.asJson()).send().onSuccess(response -> {
194-
logger.debug("created {} as custom metric in Dynatrace", customMetric.getMetricId());
195-
createdCustomMetrics.add(customMetric.getMetricId());
196-
})
197-
.onError(response -> logger.error(
198-
"failed to create custom metric {} in Dynatrace: Error Code={}, Response Body={}",
199-
customMetric.getMetricId(), response.code(), response.body()));
194+
.withJsonContent(customMetric.asJson());
200195
}
201-
catch (Throwable e) {
196+
catch (Exception ex) {
202197
if (logger.isErrorEnabled()) {
203-
logger.error("failed to create custom metric in Dynatrace: " + customMetric.getMetricId(), e);
198+
logger.error("failed to build request: {}", redactToken(ex.getMessage()));
204199
}
200+
return; // don't try to export data points, the request can't be built
201+
}
202+
203+
HttpSender.Response httpResponse = trySendHttpRequest(requestBuilder);
204+
205+
if (httpResponse != null) {
206+
httpResponse.onSuccess(response -> {
207+
logger.debug("created {} as custom metric in Dynatrace", customMetric.getMetricId());
208+
createdCustomMetrics.add(customMetric.getMetricId());
209+
}).onError(response -> logger.error(
210+
"failed to create custom metric {} in Dynatrace: Error Code={}, Response Body={}",
211+
customMetric.getMetricId(), response.code(), response.body()));
205212
}
206213
}
207214

208215
private void postCustomMetricValues(String type, String group, List<DynatraceTimeSeries> timeSeries,
209216
String customDeviceMetricEndpoint) {
210-
try {
211-
for (DynatraceBatchedPayload postMessage : createPostMessages(type, group, timeSeries)) {
212-
httpClient.post(customDeviceMetricEndpoint).withJsonContent(postMessage.payload).send()
213-
.onSuccess(response -> {
214-
if (logger.isDebugEnabled()) {
215-
logger.debug("successfully sent {} metrics to Dynatrace ({} bytes).",
216-
postMessage.metricCount, postMessage.payload.getBytes(UTF_8).length);
217-
}
218-
}).onError(response -> {
219-
logger.error("failed to send metrics to Dynatrace: Error Code={}, Response Body={}",
220-
response.code(), response.body());
221-
logger.debug("failed metrics payload: {}", postMessage.payload);
222-
});
217+
for (DynatraceBatchedPayload postMessage : createPostMessages(type, group, timeSeries)) {
218+
HttpSender.Request.Builder requestBuilder;
219+
try {
220+
requestBuilder = httpClient.post(customDeviceMetricEndpoint).withJsonContent(postMessage.payload);
221+
}
222+
catch (Exception ex) {
223+
if (logger.isErrorEnabled()) {
224+
logger.error("failed to build request: {}", redactToken(ex.getMessage()));
225+
}
226+
227+
return; // don't try to export data points, the request can't be built
228+
}
229+
230+
HttpSender.Response httpResponse = trySendHttpRequest(requestBuilder);
231+
232+
if (httpResponse != null) {
233+
httpResponse.onSuccess(response -> {
234+
if (logger.isDebugEnabled()) {
235+
logger.debug("successfully sent {} metrics to Dynatrace ({} bytes).", postMessage.metricCount,
236+
postMessage.payload.getBytes(UTF_8).length);
237+
}
238+
}).onError(response -> {
239+
logger.error("failed to send metrics to Dynatrace: Error Code={}, Response Body={}",
240+
response.code(), response.body());
241+
logger.debug("failed metrics payload: {}", postMessage.payload);
242+
});
223243
}
224244
}
245+
}
246+
247+
// VisibleForTesting
248+
HttpSender.Response trySendHttpRequest(HttpSender.Request.Builder requestBuilder) {
249+
try {
250+
return requestBuilder.send();
251+
}
225252
catch (Throwable e) {
226-
logger.error("failed to send metrics to Dynatrace", e);
253+
if (logger.isErrorEnabled()) {
254+
logger.error("failed to send metrics to Dynatrace: {}", redactToken(e.getMessage()));
255+
}
256+
return null;
227257
}
228258
}
229259

@@ -279,4 +309,8 @@ private Meter.Id idWithSuffix(Meter.Id id, String suffix) {
279309
return id.withName(id.getName() + "." + suffix);
280310
}
281311

312+
private String redactToken(String message) {
313+
return message.replace(config.apiToken(), "<redacted>");
314+
}
315+
282316
}

implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/v1/DynatraceExporterV1Test.java

+146-14
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import io.micrometer.core.instrument.*;
2020
import io.micrometer.core.instrument.config.validate.ValidationException;
2121
import io.micrometer.core.ipc.http.HttpSender;
22+
import io.micrometer.core.util.internal.logging.LogEvent;
2223
import io.micrometer.core.util.internal.logging.MockLogger;
2324
import io.micrometer.core.util.internal.logging.MockLoggerFactory;
2425
import io.micrometer.dynatrace.DynatraceApiVersion;
@@ -43,8 +44,7 @@
4344
import static org.assertj.core.api.Assertions.assertThat;
4445
import static org.assertj.core.api.Assertions.assertThatThrownBy;
4546
import static org.mockito.ArgumentMatchers.isA;
46-
import static org.mockito.Mockito.mock;
47-
import static org.mockito.Mockito.when;
47+
import static org.mockito.Mockito.*;
4848

4949
/**
5050
* Tests for {@link DynatraceExporterV1}.
@@ -266,16 +266,21 @@ void whenAllTsTooLargeEmptyMessageListReturned() {
266266

267267
@Test
268268
void splitsWhenExactlyExceedingMaxByComma() {
269+
// @formatter:off
269270
// comma needs to be considered when there is more than one time series
270-
List<DynatraceBatchedPayload> messages = exporter.createPostMessages("my.type", "my.group",
271+
List<DynatraceBatchedPayload> messages = exporter.createPostMessages(
272+
"my.type",
273+
"my.group",
271274
// Max bytes: 15330 (excluding header/footer, 15360 with header/footer)
272-
Arrays.asList(createTimeSeriesWithDimensions(750), // 14861 bytes
273-
createTimeSeriesWithDimensions(23, "asdfg"), // 469 bytes
274-
// (overflows due to
275-
// comma)
275+
Arrays.asList(
276+
createTimeSeriesWithDimensions(750), // 14861 bytes
277+
// 469 bytes (overflows due to comma)
278+
createTimeSeriesWithDimensions(23, "asdfg"),
276279
createTimeSeriesWithDimensions(750), // 14861 bytes
277280
createTimeSeriesWithDimensions(22, "asd") // 468 bytes + comma
278-
));
281+
)
282+
);
283+
// @formatter:on
279284
assertThat(messages).hasSize(3);
280285
assertThat(messages.get(0).metricCount).isEqualTo(1);
281286
assertThat(messages.get(1).metricCount).isEqualTo(1);
@@ -286,14 +291,19 @@ void splitsWhenExactlyExceedingMaxByComma() {
286291

287292
@Test
288293
void countsPreviousAndNextComma() {
289-
List<DynatraceBatchedPayload> messages = exporter.createPostMessages("my.type", null,
294+
// @formatter:off
295+
List<DynatraceBatchedPayload> messages = exporter.createPostMessages(
296+
"my.type",
297+
null,
290298
// Max bytes: 15330 (excluding header/footer, 15360 with header/footer)
291-
Arrays.asList(createTimeSeriesWithDimensions(750), // 14861 bytes
299+
Arrays.asList(
300+
createTimeSeriesWithDimensions(750), // 14861 bytes
292301
createTimeSeriesWithDimensions(10, "asdf"), // 234 bytes + comma
293-
createTimeSeriesWithDimensions(10, "asdf") // 234 bytes + comma =
294-
// 15331 bytes
295-
// (overflow)
296-
));
302+
// 234 bytes + comma = 15331 bytes (overflow)
303+
createTimeSeriesWithDimensions(10, "asdf")
304+
)
305+
);
306+
// @formatter:on
297307
assertThat(messages).hasSize(2);
298308
assertThat(messages.get(0).metricCount).isEqualTo(2);
299309
assertThat(messages.get(1).metricCount).isEqualTo(1);
@@ -386,6 +396,128 @@ void failOnPostShouldHaveProperLogging() throws Throwable {
386396
assertThat(LOGGER.getLogEvents().get(1).getCause()).isNull();
387397
}
388398

399+
@Test
400+
void testTokenShouldBeRedactedInPutFailure() {
401+
HttpSender httpClient = spy(HttpSender.class);
402+
403+
String invalidUrl = "http://localhost###";
404+
String apiToken = "this.is.a.fake.apiToken";
405+
406+
DynatraceExporterV1 exporter = getDynatraceExporterV1(httpClient, invalidUrl, apiToken);
407+
408+
meterRegistry.gauge("my.gauge", GAUGE_VALUE);
409+
Gauge gauge = meterRegistry.find("my.gauge").gauge();
410+
411+
exporter.export(Collections.singletonList(gauge));
412+
413+
assertThat(LOGGER.getLogEvents())
414+
// map to only keep the message strings
415+
.extracting(LogEvent::getMessage)
416+
.containsExactly(String.format(
417+
"failed to build request: Illegal character in fragment at index 17: %s/api/v1/timeseries/custom:my.gauge?api-token=<redacted>",
418+
invalidUrl));
419+
}
420+
421+
@Test
422+
void testTokenShouldBeRedactedInPostFailure() throws Throwable {
423+
HttpSender httpClient = spy(HttpSender.class);
424+
425+
String invalidUrl = "http://localhost###";
426+
String apiToken = "this.is.a.fake.apiToken";
427+
428+
HttpSender.Request.Builder builder = HttpSender.Request.build("http://localhost", httpClient);
429+
// mock the PUT call, so we can even run the post call
430+
doReturn(builder).when(httpClient).put(anyString());
431+
doReturn(new HttpSender.Response(200, "")).when(httpClient).send(any(HttpSender.Request.class));
432+
433+
DynatraceExporterV1 exporter = getDynatraceExporterV1(httpClient, invalidUrl, apiToken);
434+
435+
meterRegistry.gauge("my.gauge", GAUGE_VALUE);
436+
Gauge gauge = meterRegistry.find("my.gauge").gauge();
437+
438+
exporter.export(Collections.singletonList(gauge));
439+
440+
assertThat(LOGGER.getLogEvents())
441+
// map to only keep the message strings
442+
.extracting(LogEvent::getMessage).containsExactly(
443+
// the custom metric was created, meaning the PUT call succeeded
444+
"created custom:my.gauge as custom metric in Dynatrace",
445+
// the POST call throws an exception and the token is redacted
446+
String.format(
447+
"failed to build request: Illegal character in fragment at index 17: %s/api/v1/entity/infrastructure/custom/?api-token=<redacted>",
448+
invalidUrl));
449+
}
450+
451+
@Test
452+
void trySendHttpRequestSuccess() throws Throwable {
453+
HttpSender httpClient = mock(HttpSender.class);
454+
DynatraceExporterV1 exporter = FACTORY.injectLogger(() -> createExporter(httpClient));
455+
HttpSender.Request.Builder reqBuilder = mock(HttpSender.Request.Builder.class);
456+
457+
// simulate a success response
458+
when(reqBuilder.send()).thenReturn(new HttpSender.Response(200, ""));
459+
460+
// test that everything works and no error is logged
461+
exporter.trySendHttpRequest(reqBuilder);
462+
verify(reqBuilder).send();
463+
assertThat(LOGGER.getLogEvents()).isEmpty();
464+
}
465+
466+
@Test
467+
void trySendHttpRequestErrorCode() throws Throwable {
468+
HttpSender httpClient = mock(HttpSender.class);
469+
DynatraceExporterV1 exporter = FACTORY.injectLogger(() -> createExporter(httpClient));
470+
HttpSender.Request.Builder reqBuilder = mock(HttpSender.Request.Builder.class);
471+
472+
// simulate a failure response, errors are handled elsewhere
473+
when(reqBuilder.send()).thenReturn(new HttpSender.Response(400, ""));
474+
475+
// test that everything works and no error is logged
476+
exporter.trySendHttpRequest(reqBuilder);
477+
verify(reqBuilder).send();
478+
assertThat(LOGGER.getLogEvents()).isEmpty();
479+
}
480+
481+
@Test
482+
void trySendHttpRequestThrowsAndRedacts() throws Throwable {
483+
HttpSender httpClient = mock(HttpSender.class);
484+
String apiToken = "this.is.a.fake.apiToken";
485+
DynatraceExporterV1 exporter = getDynatraceExporterV1(httpClient, "http://localhost", apiToken);
486+
487+
HttpSender.Request.Builder reqBuilder = mock(HttpSender.Request.Builder.class);
488+
489+
// Simulate that the request builder throws an exception.
490+
// Should not happen if the endpoint is invalid, the URI is validated elsewhere.
491+
String exceptionMessageTemplate = "Exception with the token: %s";
492+
when(reqBuilder.send()).thenThrow(new Throwable(String.format(exceptionMessageTemplate, apiToken)));
493+
494+
exporter.trySendHttpRequest(reqBuilder);
495+
verify(reqBuilder).send();
496+
// assert that an error is logged
497+
assertThat(LOGGER.getLogEvents()).hasSize(1).extracting(x -> x.getMessage()).containsExactlyInAnyOrder(
498+
"failed to send metrics to Dynatrace: " + String.format(exceptionMessageTemplate, "<redacted>"));
499+
}
500+
501+
private DynatraceExporterV1 getDynatraceExporterV1(HttpSender httpClient, String url, String apiToken) {
502+
DynatraceExporterV1 exporter = FACTORY.injectLogger(() -> new DynatraceExporterV1(new DynatraceConfig() {
503+
@Override
504+
public String get(String key) {
505+
return null;
506+
}
507+
508+
@Override
509+
public String apiToken() {
510+
return apiToken;
511+
}
512+
513+
@Override
514+
public String uri() {
515+
return url;
516+
}
517+
}, clock, httpClient));
518+
return exporter;
519+
}
520+
389521
private DynatraceExporterV1 createExporter(HttpSender httpClient) {
390522
return new DynatraceExporterV1(config, clock, httpClient);
391523
}

0 commit comments

Comments
 (0)