Skip to content

Commit 9d9b29c

Browse files
authored
Merge pull request #169 from willarmiros/sort-subsegs
Add support for Lambda trace validation
2 parents 46307a1 + 0b7df67 commit 9d9b29c

File tree

9 files changed

+324
-40
lines changed

9 files changed

+324
-40
lines changed

validator/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ repositories {
3131

3232
dependencies {
3333
testCompile group: 'junit', name: 'junit', version: '4.12'
34+
testCompile group: 'org.assertj', name: 'assertj-core', version: '3.16.1'
3435

3536
// log
3637
compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.7'

validator/src/main/java/com/amazon/aoc/fileconfigs/ExpectedTrace.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public enum ExpectedTrace implements FileConfig {
2727
SPARK_SDK_HTTP_EXPECTED_TRACE("/expected-data-template/spark/sparkAppExpectedHTTPTrace.mustache"),
2828
SPARK_SDK_AWSSDK_EXPECTED_TRACE(
2929
"/expected-data-template/spark/sparkAppExpectedAWSSDKTrace.mustache"),
30+
LAMBDA_EXPECTED_TRACE("/expected-data-template/lambdaExpectedTrace.mustache"),
3031
SPRINGBOOT_SDK_HTTP_EXPECTED_TRACE(
3132
"/expected-data-template/springboot/springbootAppExpectedHTTPTrace.mustache"),
3233
SPRINGBOOT_SDK_AWSSDK_EXPECTED_TRACE(

validator/src/main/java/com/amazon/aoc/helpers/RetryHelper.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,18 @@ public class RetryHelper {
3030
* @param retryCount the total retry count
3131
* @param sleepInMilliSeconds sleep time among retries
3232
* @param retryable the lambda
33+
* @return false if retryCount exhausted
3334
* @throws Exception when the retry count is reached
3435
*/
35-
public static void retry(
36+
public static boolean retry(
3637
int retryCount, int sleepInMilliSeconds, boolean throwExceptionInTheEnd, Retryable retryable)
3738
throws Exception {
3839
Exception exceptionInTheEnd = null;
3940
while (retryCount-- > 0) {
4041
try {
4142
log.info("retry attempt left : {} ", retryCount);
4243
retryable.execute();
43-
return;
44+
return true;
4445
} catch (Exception ex) {
4546
exceptionInTheEnd = ex;
4647
log.info("retrying after {} seconds", TimeUnit.MILLISECONDS.toSeconds(sleepInMilliSeconds));
@@ -52,6 +53,7 @@ public static void retry(
5253
log.error("retries exhausted, possible");
5354
throw exceptionInTheEnd;
5455
}
56+
return false;
5557
}
5658

5759
/**
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.amazon.aoc.helpers;
2+
3+
import com.amazon.aoc.models.xray.Entity;
4+
5+
import java.util.List;
6+
7+
public final class SortUtils {
8+
private static final int MAX_RESURSIVE_DEPTH = 10;
9+
10+
/**
11+
* Given a list of entities, which are X-Ray segments or subsegments, recursively sort each of
12+
* their children subsegments by start time, then sort the given list itself by start time.
13+
*
14+
* @param entities - list of X-Ray entities to sort recursively. Modified in place.
15+
*/
16+
public static void recursiveEntitySort(List<Entity> entities) {
17+
recursiveEntitySort(entities, 0);
18+
}
19+
20+
private static void recursiveEntitySort(List<Entity> entities, int depth) {
21+
if (entities == null || entities.size() == 0 || depth >= MAX_RESURSIVE_DEPTH) {
22+
return;
23+
}
24+
int currDepth = depth + 1;
25+
26+
for (Entity entity : entities) {
27+
if (entity.getSubsegments() != null && !entity.getSubsegments().isEmpty()) {
28+
recursiveEntitySort(entity.getSubsegments(), currDepth);
29+
}
30+
}
31+
32+
entities.sort(
33+
(entity1, entity2) -> {
34+
if (entity1.getStartTime() == entity2.getStartTime()) {
35+
return 0;
36+
}
37+
38+
return entity1.getStartTime() < entity2.getStartTime() ? -1 : 1;
39+
}
40+
);
41+
}
42+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.amazon.aoc.models.xray;
2+
3+
import com.fasterxml.jackson.annotation.JsonInclude;
4+
import lombok.Getter;
5+
import lombok.Setter;
6+
7+
import java.util.List;
8+
import java.util.Map;
9+
10+
/**
11+
* Barebones class representing a X-Ray Entity, used for JSON deserialization with Jackson.
12+
* It is not exactly an entity because it includes fields that are only allowed in Segments
13+
* (e.g. origin, user) but for the purposes of the validator that is acceptable because those
14+
* fields will be ignored when they're not present in subsegments.
15+
*/
16+
@Getter
17+
@Setter
18+
public class Entity {
19+
private String name;
20+
private String id;
21+
private String parentId;
22+
private double startTime;
23+
private String resourceArn;
24+
private String user;
25+
private String origin;
26+
private String traceId;
27+
28+
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
29+
private double endTime;
30+
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
31+
private boolean fault;
32+
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
33+
private boolean error;
34+
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
35+
private boolean throttle;
36+
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
37+
private boolean inProgress;
38+
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
39+
private boolean inferred;
40+
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
41+
private boolean stubbed;
42+
43+
private String namespace;
44+
45+
private List<Entity> subsegments;
46+
47+
private Map<String, Object> cause;
48+
private Map<String, Object> http;
49+
private Map<String, Object> aws;
50+
private Map<String, Object> sql;
51+
52+
private Map<String, Map<String, Object>> metadata;
53+
private Map<String, Object> annotations;
54+
}

validator/src/main/java/com/amazon/aoc/validators/TraceValidator.java

Lines changed: 62 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,27 @@
1616
package com.amazon.aoc.validators;
1717

1818
import com.amazon.aoc.callers.ICaller;
19+
import com.amazon.aoc.enums.GenericConstants;
1920
import com.amazon.aoc.exception.BaseException;
2021
import com.amazon.aoc.exception.ExceptionCode;
2122
import com.amazon.aoc.fileconfigs.FileConfig;
2223
import com.amazon.aoc.helpers.MustacheHelper;
2324
import com.amazon.aoc.helpers.RetryHelper;
25+
import com.amazon.aoc.helpers.SortUtils;
2426
import com.amazon.aoc.models.Context;
2527
import com.amazon.aoc.models.SampleAppResponse;
2628
import com.amazon.aoc.models.ValidationConfig;
29+
import com.amazon.aoc.models.xray.Entity;
2730
import com.amazon.aoc.services.XRayService;
2831
import com.amazonaws.services.xray.model.Segment;
2932
import com.amazonaws.services.xray.model.Trace;
33+
import com.fasterxml.jackson.core.JsonProcessingException;
3034
import com.fasterxml.jackson.databind.ObjectMapper;
35+
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
3136
import com.github.wnameless.json.flattener.JsonFlattener;
3237
import lombok.extern.log4j.Log4j2;
3338

39+
import java.util.ArrayList;
3440
import java.util.Collections;
3541
import java.util.List;
3642
import java.util.Map;
@@ -44,6 +50,9 @@ public class TraceValidator implements IValidator {
4450
private Context context;
4551
private FileConfig expectedTrace;
4652

53+
private static final ObjectMapper MAPPER = new ObjectMapper()
54+
.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
55+
4756
@Override
4857
public void init(
4958
Context context, ValidationConfig validationConfig, ICaller caller, FileConfig expectedTrace)
@@ -64,23 +73,35 @@ public void validate() throws Exception {
6473
List<String> traceIdList = Collections.singletonList(traceId);
6574

6675
// get retrieved trace from x-ray service
67-
Map<String, Object> retrievedTrace = this.getRetrievedTrace(traceIdList);
68-
log.info("value of retrieved trace map: {}", retrievedTrace);
69-
// data model validation of other fields of segment document
70-
for (Map.Entry<String, Object> entry : storedTrace.entrySet()) {
71-
String targetKey = entry.getKey();
72-
if (retrievedTrace.get(targetKey) == null) {
73-
log.error("mis target data: {}", targetKey);
74-
throw new BaseException(ExceptionCode.DATA_MODEL_NOT_MATCHED);
75-
}
76-
if (!entry.getValue().toString().equalsIgnoreCase(retrievedTrace.get(targetKey).toString())) {
77-
log.error("data model validation failed");
78-
log.info("mis matched data model field list");
79-
log.info("value of stored trace map: {}", entry.getValue());
80-
log.info("value of retrieved map: {}", retrievedTrace.get(entry.getKey()));
81-
log.info("==========================================");
82-
throw new BaseException(ExceptionCode.DATA_MODEL_NOT_MATCHED);
83-
}
76+
boolean isMatched = RetryHelper.retry(10,
77+
Integer.parseInt(GenericConstants.SLEEP_IN_MILLISECONDS.getVal()),
78+
false,
79+
() -> {
80+
Map<String, Object> retrievedTrace = this.getRetrievedTrace(traceIdList);
81+
log.info("value of retrieved trace map: {}", retrievedTrace);
82+
// data model validation of other fields of segment document
83+
for (Map.Entry<String, Object> entry : storedTrace.entrySet()) {
84+
String targetKey = entry.getKey();
85+
if (retrievedTrace.get(targetKey) == null) {
86+
log.error("mis target data: {}", targetKey);
87+
throw new BaseException(ExceptionCode.DATA_MODEL_NOT_MATCHED);
88+
}
89+
if (!entry
90+
.getValue()
91+
.toString()
92+
.equalsIgnoreCase(retrievedTrace.get(targetKey).toString())) {
93+
log.error("data model validation failed");
94+
log.info("mis matched data model field list");
95+
log.info("value of stored trace map: {}", entry.getValue());
96+
log.info("value of retrieved map: {}", retrievedTrace.get(entry.getKey()));
97+
log.info("==========================================");
98+
throw new BaseException(ExceptionCode.DATA_MODEL_NOT_MATCHED);
99+
}
100+
}
101+
});
102+
103+
if (!isMatched) {
104+
throw new BaseException(ExceptionCode.DATA_MODEL_NOT_MATCHED);
84105
}
85106

86107
log.info("validation is passed for path {}", caller.getCallingPath());
@@ -113,30 +134,33 @@ private Map<String, Object> getRetrievedTrace(List<String> traceIdList) throws E
113134
}
114135

115136
private Map<String, Object> flattenDocument(List<Segment> segmentList) {
116-
// have to sort the segments by start_time because
117-
// 1. we can not get span id from xraysdk today,
118-
// 2. the segments come out with different order everytime
119-
segmentList.sort(
120-
(segment1, segment2) -> {
121-
try {
122-
Map<String, Object> map1 =
123-
new ObjectMapper().readValue(segment1.getDocument(), Map.class);
124-
Map<String, Object> map2 =
125-
new ObjectMapper().readValue(segment2.getDocument(), Map.class);
126-
return Double.valueOf(map1.get("start_time").toString())
127-
.compareTo(Double.valueOf(map2.get("start_time").toString()));
128-
} catch (Exception ex) {
129-
log.error(ex);
130-
return 0;
131-
}
132-
});
137+
List<Entity> entityList = new ArrayList<>();
133138

134-
// build the segment's document as a jsonarray and flatten it for easy comparison
135-
StringBuilder segmentsJson = new StringBuilder("[");
139+
// Parse retrieved segment documents into a barebones Entity POJO
136140
for (Segment segment : segmentList) {
137-
segmentsJson.append(segment.getDocument());
138-
segmentsJson.append(",");
141+
Entity entity;
142+
try {
143+
entity = MAPPER.readValue(segment.getDocument(), Entity.class);
144+
entityList.add(entity);
145+
} catch (JsonProcessingException e) {
146+
log.warn("Error parsing segment JSON", e);
147+
}
148+
}
149+
150+
// Recursively sort all segments and subsegments so the ordering is always consistent
151+
SortUtils.recursiveEntitySort(entityList);
152+
StringBuilder segmentsJson = new StringBuilder("[");
153+
154+
// build the segment's document as a json array and flatten it for easy comparison
155+
for (Entity entity : entityList) {
156+
try {
157+
segmentsJson.append(MAPPER.writeValueAsString(entity));
158+
segmentsJson.append(",");
159+
} catch (JsonProcessingException e) {
160+
log.warn("Error serializing segment JSON", e);
161+
}
139162
}
163+
140164
segmentsJson.replace(segmentsJson.length() - 1, segmentsJson.length(), "]");
141165
return JsonFlattener.flattenAsMap(segmentsJson.toString());
142166
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
[{
2+
"origin":"AWS::ApiGateway::Stage",
3+
"subsegments":[
4+
{
5+
"name":"Lambda",
6+
"namespace":"aws"
7+
}
8+
]
9+
},
10+
{
11+
"http":{
12+
"response":{
13+
"status":200
14+
}
15+
},
16+
"origin":"AWS::Lambda"
17+
},
18+
{
19+
"origin":"AWS::Lambda::Function",
20+
"subsegments":[
21+
{
22+
"name":"Invocation",
23+
"subsegments":[
24+
{
25+
"name":"lambda_function.lambda_handler",
26+
"aws":{
27+
"xray":{
28+
"auto_instrumentation":false,
29+
"sdk_version":"0.16b1",
30+
"sdk":"opentelemetry for python"
31+
}
32+
}
33+
}
34+
]
35+
},
36+
{
37+
"name":"Overhead"
38+
}
39+
]
40+
41+
},
42+
{
43+
"name":"HTTP GET",
44+
"inferred":true,
45+
"http":{
46+
"request":{
47+
"url":"http://httpbin.org/",
48+
"method":"GET"
49+
}
50+
}
51+
},
52+
{
53+
"name":"S3",
54+
"origin":"AWS::S3",
55+
"inferred":true,
56+
"aws":{
57+
"operation":"ListBuckets"
58+
}
59+
}]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-
2+
validationType: "trace"
3+
httpPath: ""
4+
httpMethod: "get"
5+
callingType: "http"
6+
expectedTraceTemplate: "LAMBDA_EXPECTED_TRACE"

0 commit comments

Comments
 (0)