Add support for fetching xray sampling targets. (#3335)
* Add support for fetching xray sampling targets. * Cleanup
This commit is contained in:
parent
ce9c8854c7
commit
08ec61708a
|
|
@ -26,4 +26,5 @@ dependencies {
|
|||
testImplementation("com.linecorp.armeria:armeria-junit5")
|
||||
testImplementation("com.google.guava:guava")
|
||||
testImplementation("org.slf4j:slf4j-simple")
|
||||
testImplementation("org.skyscreamer:jsonassert")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.sdk.extension.aws.trace;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.google.auto.value.AutoValue;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@AutoValue
|
||||
@JsonSerialize(as = GetSamplingTargetsRequest.class)
|
||||
abstract class GetSamplingTargetsRequest {
|
||||
|
||||
static GetSamplingTargetsRequest create(List<SamplingStatisticsDocument> documents) {
|
||||
return new AutoValue_GetSamplingTargetsRequest(documents);
|
||||
}
|
||||
|
||||
// Limit of 25 items
|
||||
@JsonProperty("SamplingStatisticsDocuments")
|
||||
abstract List<SamplingStatisticsDocument> getDocuments();
|
||||
|
||||
@AutoValue
|
||||
@JsonSerialize(as = SamplingStatisticsDocument.class)
|
||||
abstract static class SamplingStatisticsDocument {
|
||||
|
||||
static SamplingStatisticsDocument.Builder newBuilder() {
|
||||
return new AutoValue_GetSamplingTargetsRequest_SamplingStatisticsDocument.Builder();
|
||||
}
|
||||
|
||||
@JsonProperty("BorrowCount")
|
||||
abstract int getBorrowCount();
|
||||
|
||||
@JsonProperty("ClientID")
|
||||
abstract String getClientId();
|
||||
|
||||
@JsonProperty("RequestCount")
|
||||
abstract int getRequestCount();
|
||||
|
||||
@JsonProperty("RuleName")
|
||||
abstract String getRuleName();
|
||||
|
||||
@JsonProperty("SampledCount")
|
||||
abstract int getSampledCount();
|
||||
|
||||
@JsonProperty("Timestamp")
|
||||
@JsonFormat(
|
||||
shape = JsonFormat.Shape.STRING,
|
||||
pattern = "yyyy-MM-dd'T'HH:mm:ss",
|
||||
timezone = "UTC")
|
||||
abstract Date getTimestamp();
|
||||
|
||||
@AutoValue.Builder
|
||||
abstract static class Builder {
|
||||
abstract Builder setBorrowCount(int borrowCount);
|
||||
|
||||
abstract Builder setClientId(String clientId);
|
||||
|
||||
abstract Builder setRequestCount(int requestCount);
|
||||
|
||||
abstract Builder setRuleName(String ruleName);
|
||||
|
||||
abstract Builder setSampledCount(int sampledCount);
|
||||
|
||||
abstract Builder setTimestamp(Date timestamp);
|
||||
|
||||
abstract SamplingStatisticsDocument build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.sdk.extension.aws.trace;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.auto.value.AutoValue;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@AutoValue
|
||||
abstract class GetSamplingTargetsResponse {
|
||||
|
||||
@JsonCreator
|
||||
static GetSamplingTargetsResponse create(
|
||||
@JsonProperty("LastRuleModification") Date lastRuleModification,
|
||||
@JsonProperty("SamplingTargetDocuments") List<SamplingTargetDocument> documents,
|
||||
@JsonProperty("UnprocessedStatistics") List<UnprocessedStatistics> unprocessedStatistics) {
|
||||
return new AutoValue_GetSamplingTargetsResponse(
|
||||
lastRuleModification, documents, unprocessedStatistics);
|
||||
}
|
||||
|
||||
abstract Date getLastRuleModification();
|
||||
|
||||
abstract List<SamplingTargetDocument> getDocuments();
|
||||
|
||||
abstract List<UnprocessedStatistics> getUnprocessedStatistics();
|
||||
|
||||
@AutoValue
|
||||
abstract static class SamplingTargetDocument {
|
||||
|
||||
@JsonCreator
|
||||
static SamplingTargetDocument create(
|
||||
@JsonProperty("FixedRate") double fixedRate,
|
||||
@JsonProperty("Interval") int intervalSecs,
|
||||
@JsonProperty("ReservoirQuota") int reservoirQuota,
|
||||
@JsonProperty("ReservoirQuotaTTL") @Nullable Date reservoirQuotaTtl,
|
||||
@JsonProperty("RuleName") String ruleName) {
|
||||
return new AutoValue_GetSamplingTargetsResponse_SamplingTargetDocument(
|
||||
fixedRate, intervalSecs, reservoirQuota, reservoirQuotaTtl, ruleName);
|
||||
}
|
||||
|
||||
abstract double getFixedRate();
|
||||
|
||||
abstract int getIntervalSecs();
|
||||
|
||||
abstract int getReservoirQuota();
|
||||
|
||||
@Nullable
|
||||
abstract Date getReservoirQuotaTtl();
|
||||
|
||||
abstract String getRuleName();
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
abstract static class UnprocessedStatistics {
|
||||
|
||||
@JsonCreator
|
||||
static UnprocessedStatistics create(
|
||||
@JsonProperty("ErrorCode") String errorCode,
|
||||
@JsonProperty("Message") String message,
|
||||
@JsonProperty("RuleName") String ruleName) {
|
||||
return new AutoValue_GetSamplingTargetsResponse_UnprocessedStatistics(
|
||||
errorCode, message, ruleName);
|
||||
}
|
||||
|
||||
abstract String getErrorCode();
|
||||
|
||||
abstract String getMessage();
|
||||
|
||||
abstract String getRuleName();
|
||||
}
|
||||
}
|
||||
|
|
@ -3,15 +3,42 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// Includes work from:
|
||||
|
||||
/*
|
||||
* Copyright 2010-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Portions copyright 2006-2009 James Murty. Please see LICENSE.txt
|
||||
* for applicable license terms and NOTICE.txt for applicable notices.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://aws.amazon.com/apache2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
package io.opentelemetry.sdk.extension.aws.trace;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import io.opentelemetry.sdk.extension.aws.internal.JdkHttpClient;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
final class XraySamplerClient {
|
||||
|
|
@ -19,6 +46,9 @@ final class XraySamplerClient {
|
|||
private static final ObjectMapper OBJECT_MAPPER =
|
||||
new ObjectMapper()
|
||||
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
|
||||
// AWS APIs return timestamps as floats.
|
||||
.registerModule(
|
||||
new SimpleModule().addDeserializer(Date.class, new FloatDateDeserializer()))
|
||||
// In case API is extended with new fields.
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, /* state= */ false);
|
||||
|
||||
|
|
@ -26,14 +56,26 @@ final class XraySamplerClient {
|
|||
Collections.singletonMap("Content-Type", "application/json");
|
||||
|
||||
private final String getSamplingRulesEndpoint;
|
||||
private final String getSamplingTargetsEndpoint;
|
||||
private final JdkHttpClient httpClient;
|
||||
|
||||
XraySamplerClient(String host) {
|
||||
this.getSamplingRulesEndpoint = host + "/GetSamplingRules";
|
||||
// Lack of Get may look wrong but is correct.
|
||||
this.getSamplingTargetsEndpoint = host + "/SamplingTargets";
|
||||
httpClient = new JdkHttpClient();
|
||||
}
|
||||
|
||||
GetSamplingRulesResponse getSamplingRules(GetSamplingRulesRequest request) {
|
||||
return executeJsonRequest(getSamplingRulesEndpoint, request, GetSamplingRulesResponse.class);
|
||||
}
|
||||
|
||||
GetSamplingTargetsResponse getSamplingTargets(GetSamplingTargetsRequest request) {
|
||||
return executeJsonRequest(
|
||||
getSamplingTargetsEndpoint, request, GetSamplingTargetsResponse.class);
|
||||
}
|
||||
|
||||
private <T> T executeJsonRequest(String endpoint, Object request, Class<T> responseType) {
|
||||
final byte[] requestBody;
|
||||
try {
|
||||
requestBody = OBJECT_MAPPER.writeValueAsBytes(request);
|
||||
|
|
@ -42,13 +84,41 @@ final class XraySamplerClient {
|
|||
}
|
||||
|
||||
String response =
|
||||
httpClient.fetchString(
|
||||
"POST", getSamplingRulesEndpoint, JSON_CONTENT_TYPE, null, requestBody);
|
||||
httpClient.fetchString("POST", endpoint, JSON_CONTENT_TYPE, null, requestBody);
|
||||
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(response, GetSamplingRulesResponse.class);
|
||||
return OBJECT_MAPPER.readValue(response, responseType);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new UncheckedIOException("Failed to deserialize response.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("JavaUtilDate")
|
||||
private static class FloatDateDeserializer extends StdDeserializer<Date> {
|
||||
|
||||
private static final long serialVersionUID = 4446058377205025341L;
|
||||
|
||||
private static final int AWS_DATE_MILLI_SECOND_PRECISION = 3;
|
||||
|
||||
private FloatDateDeserializer() {
|
||||
super(Date.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
return parseServiceSpecificDate(p.getText());
|
||||
}
|
||||
|
||||
// Copied from AWS SDK
|
||||
// https://github.com/aws/aws-sdk-java/blob/7b1e5b87b0bf03456df9e77716b14731adf9a7a7/aws-java-sdk-core/src/main/java/com/amazonaws/util/DateUtils.java#L239
|
||||
/** Parses the given date string returned by the AWS service into a Date object. */
|
||||
private static Date parseServiceSpecificDate(String dateString) {
|
||||
try {
|
||||
BigDecimal dateValue = new BigDecimal(dateString);
|
||||
return new Date(dateValue.scaleByPowerOfTen(AWS_DATE_MILLI_SECOND_PRECISION).longValue());
|
||||
} catch (NumberFormatException nfe) {
|
||||
throw new IllegalArgumentException("Unable to parse date : " + dateString, nfe);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,48 +7,30 @@ package io.opentelemetry.sdk.extension.aws.trace;
|
|||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.assertj.core.api.Assertions.entry;
|
||||
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.io.Resources;
|
||||
import com.linecorp.armeria.common.AggregatedHttpRequest;
|
||||
import com.linecorp.armeria.common.HttpResponse;
|
||||
import com.linecorp.armeria.common.HttpStatus;
|
||||
import com.linecorp.armeria.common.MediaType;
|
||||
import com.linecorp.armeria.server.ServerBuilder;
|
||||
import com.linecorp.armeria.server.logging.LoggingService;
|
||||
import com.linecorp.armeria.testing.junit5.server.ServerExtension;
|
||||
import com.linecorp.armeria.testing.junit5.server.mock.MockWebServerExtension;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
|
||||
class XraySamplerClientTest {
|
||||
|
||||
@RegisterExtension
|
||||
public static ServerExtension server =
|
||||
new ServerExtension() {
|
||||
|
||||
@Override
|
||||
protected void configure(ServerBuilder sb) throws Exception {
|
||||
byte[] getSamplingRulesResponse =
|
||||
ByteStreams.toByteArray(
|
||||
requireNonNull(
|
||||
XraySamplerClientTest.class.getResourceAsStream(
|
||||
"/get-sampling-rules-response.json")));
|
||||
|
||||
sb.decorator(LoggingService.newDecorator());
|
||||
sb.service(
|
||||
"/GetSamplingRules",
|
||||
(ctx, req) ->
|
||||
HttpResponse.from(
|
||||
req.aggregate()
|
||||
.thenApply(
|
||||
aggregatedReq -> {
|
||||
assertThat(aggregatedReq.contentUtf8())
|
||||
.isEqualTo("{\"NextToken\":\"token\"}");
|
||||
return HttpResponse.of(
|
||||
HttpStatus.OK, MediaType.JSON_UTF_8, getSamplingRulesResponse);
|
||||
})));
|
||||
}
|
||||
};
|
||||
@RegisterExtension public static MockWebServerExtension server = new MockWebServerExtension();
|
||||
|
||||
private XraySamplerClient client;
|
||||
|
||||
|
|
@ -58,10 +40,16 @@ class XraySamplerClientTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
void getSamplingRules() {
|
||||
void getSamplingRules() throws Exception {
|
||||
enqueueResource("/get-sampling-rules-response.json");
|
||||
GetSamplingRulesResponse response =
|
||||
client.getSamplingRules(GetSamplingRulesRequest.create("token"));
|
||||
|
||||
AggregatedHttpRequest request = server.takeRequest().request();
|
||||
assertThat(request.path()).isEqualTo("/GetSamplingRules");
|
||||
assertThat(request.contentType()).isEqualTo(MediaType.JSON);
|
||||
assertThat(request.contentUtf8()).isEqualTo("{\"NextToken\":\"token\"}");
|
||||
|
||||
assertThat(response.getNextToken()).isNull();
|
||||
assertThat(response.getSamplingRules())
|
||||
.satisfiesExactly(
|
||||
|
|
@ -103,4 +91,99 @@ class XraySamplerClientTest {
|
|||
assertThat(rule.getRule().getAttributes()).isEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void getSamplingRules_malformed() {
|
||||
server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.JSON, "notjson"));
|
||||
assertThatThrownBy(() -> client.getSamplingRules(GetSamplingRulesRequest.create("token")))
|
||||
.isInstanceOf(UncheckedIOException.class)
|
||||
.hasMessage("Failed to deserialize response.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getSamplingTargets() throws Exception {
|
||||
// Request and response adapted from
|
||||
// https://docs.aws.amazon.com/xray/latest/devguide/xray-api-sampling.html
|
||||
enqueueResource("/get-sampling-targets-response.json");
|
||||
Date timestamp = Date.from(Instant.parse("2021-06-21T06:46:07Z"));
|
||||
Date timestamp2 = Date.from(Instant.parse("2018-07-07T00:20:06Z"));
|
||||
GetSamplingTargetsRequest samplingTargetsRequest =
|
||||
GetSamplingTargetsRequest.create(
|
||||
Arrays.asList(
|
||||
GetSamplingTargetsRequest.SamplingStatisticsDocument.newBuilder()
|
||||
.setRuleName("Test")
|
||||
.setClientId("ABCDEF1234567890ABCDEF10")
|
||||
.setTimestamp(timestamp)
|
||||
.setRequestCount(110)
|
||||
.setSampledCount(30)
|
||||
.setBorrowCount(20)
|
||||
.build(),
|
||||
GetSamplingTargetsRequest.SamplingStatisticsDocument.newBuilder()
|
||||
.setRuleName("polling-scorekeep")
|
||||
.setClientId("ABCDEF1234567890ABCDEF11")
|
||||
.setTimestamp(timestamp2)
|
||||
.setRequestCount(10500)
|
||||
.setSampledCount(31)
|
||||
.setBorrowCount(0)
|
||||
.build()));
|
||||
GetSamplingTargetsResponse response = client.getSamplingTargets(samplingTargetsRequest);
|
||||
|
||||
AggregatedHttpRequest request = server.takeRequest().request();
|
||||
|
||||
assertThat(request.path()).isEqualTo("/SamplingTargets");
|
||||
assertThat(request.contentType()).isEqualTo(MediaType.JSON);
|
||||
JSONAssert.assertEquals(
|
||||
Resources.toString(
|
||||
XraySamplerClientTest.class.getResource("/get-sampling-targets-request.json"),
|
||||
StandardCharsets.UTF_8),
|
||||
request.contentUtf8(),
|
||||
true);
|
||||
|
||||
assertThat(response.getLastRuleModification())
|
||||
.isEqualTo(Date.from(Instant.parse("2018-07-06T23:41:45Z")));
|
||||
assertThat(response.getDocuments())
|
||||
.satisfiesExactly(
|
||||
document -> {
|
||||
assertThat(document.getRuleName()).isEqualTo("base-scorekeep");
|
||||
assertThat(document.getFixedRate()).isEqualTo(0.1);
|
||||
assertThat(document.getReservoirQuota()).isEqualTo(2);
|
||||
assertThat(document.getReservoirQuotaTtl())
|
||||
.isEqualTo(Date.from(Instant.parse("2018-07-07T00:25:07Z")));
|
||||
assertThat(document.getIntervalSecs()).isEqualTo(10);
|
||||
},
|
||||
document -> {
|
||||
assertThat(document.getRuleName()).isEqualTo("polling-scorekeep");
|
||||
assertThat(document.getFixedRate()).isEqualTo(0.003);
|
||||
assertThat(document.getReservoirQuota()).isZero();
|
||||
assertThat(document.getReservoirQuotaTtl()).isNull();
|
||||
assertThat(document.getIntervalSecs()).isZero();
|
||||
});
|
||||
assertThat(response.getUnprocessedStatistics())
|
||||
.satisfiesExactly(
|
||||
statistics -> {
|
||||
assertThat(statistics.getRuleName()).isEqualTo("cats-rule");
|
||||
assertThat(statistics.getErrorCode()).isEqualTo("400");
|
||||
assertThat(statistics.getMessage()).isEqualTo("Unknown rule");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void getSamplingTargets_malformed() {
|
||||
server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.JSON, "notjson"));
|
||||
assertThatThrownBy(
|
||||
() ->
|
||||
client.getSamplingTargets(
|
||||
GetSamplingTargetsRequest.create(Collections.emptyList())))
|
||||
.isInstanceOf(UncheckedIOException.class)
|
||||
.hasMessage("Failed to deserialize response.");
|
||||
}
|
||||
|
||||
private static void enqueueResource(String resourcePath) throws Exception {
|
||||
server.enqueue(
|
||||
HttpResponse.of(
|
||||
HttpStatus.OK,
|
||||
MediaType.JSON_UTF_8,
|
||||
ByteStreams.toByteArray(
|
||||
requireNonNull(XraySamplerClientTest.class.getResourceAsStream(resourcePath)))));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"SamplingStatisticsDocuments":[
|
||||
{
|
||||
"RuleName":"Test",
|
||||
"ClientID":"ABCDEF1234567890ABCDEF10",
|
||||
"Timestamp":"2021-06-21T06:46:07",
|
||||
"RequestCount":110,
|
||||
"SampledCount":30,
|
||||
"BorrowCount":20
|
||||
},
|
||||
{
|
||||
"RuleName":"polling-scorekeep",
|
||||
"ClientID":"ABCDEF1234567890ABCDEF11",
|
||||
"Timestamp":"2018-07-07T00:20:06",
|
||||
"RequestCount":10500,
|
||||
"SampledCount":31,
|
||||
"BorrowCount":0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"SamplingTargetDocuments": [
|
||||
{
|
||||
"RuleName": "base-scorekeep",
|
||||
"FixedRate": 0.1,
|
||||
"ReservoirQuota": 2,
|
||||
"ReservoirQuotaTTL": 1530923107.0,
|
||||
"Interval": 10
|
||||
},
|
||||
{
|
||||
"RuleName": "polling-scorekeep",
|
||||
"FixedRate": 0.003
|
||||
}
|
||||
],
|
||||
"LastRuleModification": 1530920505.0,
|
||||
"UnprocessedStatistics": [
|
||||
{
|
||||
"RuleName": "cats-rule",
|
||||
"ErrorCode": "400",
|
||||
"Message": "Unknown rule"
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue