Add support for fetching xray sampling targets. (#3335)

* Add support for fetching xray sampling targets.

* Cleanup
This commit is contained in:
Anuraag Agrawal 2021-06-23 08:16:42 +09:00 committed by GitHub
parent ce9c8854c7
commit 08ec61708a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 382 additions and 34 deletions

View File

@ -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")
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}
}
}

View File

@ -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)))));
}
}

View File

@ -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
}
]
}

View File

@ -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"
}
]
}