Skip to content

Commit 8708919

Browse files
authored
Merge pull request #36 from MattWhelan/mwhelan/reflectionFix
Address type erasure/reflection issue
2 parents 4d90df0 + aacd04e commit 8708919

File tree

8 files changed

+228
-73
lines changed

8 files changed

+228
-73
lines changed

README.md

Lines changed: 17 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,15 @@ Versioning will have the following format: {majorVersion}.{minorVersion}.{pointV
1111

1212
### How it Works
1313

14-
The SDK provides `TracingRequestHandler` and `TracingRequestStreamHandler` interfaces that extend AWS' Lambda request handlers. When a Lambda function that is using an implementation of either tracing request handler is invoked, the handler will obtain the globally registered OpenTracing [Tracer](https://opentracing.io/docs/overview/tracers/) and create/start an OpenTracing [Span](https://opentracing.io/docs/overview/spans/) to capture timing information and `key:value` pairs ([Tags/Logs](https://opentracing.io/docs/overview/tags-logs-baggage/)) detailing the trace data.
14+
The SDK provides `LambdaTracing` and `StreamLambdaTracing` classes that instrument requests. When a Lambda function
15+
that is using an implementation of either class is invoked, the handler will obtain the globally registered OpenTracing
16+
[Tracer](https://opentracing.io/docs/overview/tracers/) and create/start an OpenTracing
17+
[Span](https://opentracing.io/docs/overview/spans/) to capture timing information and `key:value` pairs
18+
([Tags/Logs](https://opentracing.io/docs/overview/tags-logs-baggage/)) detailing the trace data.
19+
20+
As part of the implementation the user's handler must call the `instrument` method, passing a callback that contains
21+
the business logic for their handler.
1522

16-
As part of the implementation the user must override the tracing handler's `doHandleRequest` method which is called by the handler interface's `handleRequest` method.
1723

1824
### Collected Span Tags/Logs
1925

@@ -42,7 +48,7 @@ Below are a list of the collected exception attributes:
4248
You can add the dependency by adding the following to your `build.gradle` file:
4349
```
4450
dependencies {
45-
compile "com.newrelic.opentracing:java-aws-lambda:2.0.0"
51+
implementation "com.newrelic.opentracing:java-aws-lambda:2.1.0"
4652
}
4753
```
4854

@@ -56,39 +62,20 @@ dependencies {
5662
#### Example Usage
5763

5864
```java
59-
package com.handler.example;
60-
61-
import com.amazonaws.services.lambda.runtime.Context;
62-
import com.newrelic.opentracing.aws.TracingRequestHandler;
63-
import io.opentracing.util.GlobalTracer;
64-
65-
import java.util.Map;
66-
67-
/**
68-
* Tracing request handler that creates a span on every invocation of a Lambda.
69-
*
70-
* @param Map<String, Object> The Lambda Function input
71-
* @param String The Lambda Function output
72-
*/
73-
public class MyLambdaHandler implements TracingRequestHandler<Map<String, Object>, String> {
65+
public class YourLambdaHandler implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
7466
static {
7567
// Obtain an instance of the OpenTracing Tracer of your choice
76-
Tracer tracer = new CustomTracer(...);
68+
Tracer tracer = LambdaTracer.INSTANCE;
7769
// Register your tracer as the Global Tracer
7870
GlobalTracer.registerIfAbsent(tracer);
7971
}
80-
81-
/**
82-
* Method that handles the Lambda function request.
83-
*
84-
* @param input The Lambda Function input
85-
* @param context The Lambda execution environment context object
86-
* @return String The Lambda Function output
87-
*/
72+
8873
@Override
89-
public String doHandleRequest(Map<String, Object> input, Context context) {
90-
// Your function logic here
91-
return "Lambda Function output";
74+
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent apiGatewayProxyRequestEvent, Context context) {
75+
return LambdaTracing.instrument(apiGatewayProxyRequestEvent, context, (event, ctx) -> {
76+
// Your business logic here
77+
return doSomethingWithTheEvent(event);
78+
});
9279
}
9380
}
9481
```
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.newrelic.opentracing.aws;
2+
3+
import com.amazonaws.services.lambda.runtime.Context;
4+
import io.opentracing.Scope;
5+
import io.opentracing.Span;
6+
import io.opentracing.SpanContext;
7+
import io.opentracing.Tracer;
8+
import io.opentracing.util.GlobalTracer;
9+
import java.util.concurrent.atomic.AtomicBoolean;
10+
import java.util.function.BiFunction;
11+
12+
/**
13+
* Trace calls to lambda functions, for arbitrary Input and Output types.
14+
*
15+
* <p>For flexibility, applications may extend this class to enhance the root span or handle novel
16+
* invocation event types.
17+
*
18+
* @param <Input> The invocation payload type for your lambda function.
19+
* @param <Output> The result type for your lambda function.
20+
*/
21+
public class LambdaTracing<Input, Output> {
22+
protected static final AtomicBoolean isColdStart = new AtomicBoolean(true);
23+
24+
/**
25+
* One-line instrumentation convenience method.
26+
*
27+
* @param input The invocation event
28+
* @param context The invocation context
29+
* @param realHandler The callback that implements the business logic for this event handler
30+
* @param <Input> The type of the invocation event
31+
* @param <Output> The type of the response
32+
* @return The invocation response (the return value of the realHandler callback)
33+
*/
34+
public static <Input, Output> Output instrument(
35+
Input input, Context context, BiFunction<Input, Context, Output> realHandler) {
36+
return new LambdaTracing<Input, Output>().instrumentRequest(input, context, realHandler);
37+
}
38+
39+
/**
40+
* Instrument a Lambda invocation
41+
*
42+
* @param input The invocation event
43+
* @param context The invocation context
44+
* @param realHandler The function that implements the business logic. Will be invoked with the
45+
* input and context parameters, from within the instrumentation scope.
46+
* @return the return value from realHandler
47+
*/
48+
public Output instrumentRequest(
49+
Input input, Context context, BiFunction<Input, Context, Output> realHandler) {
50+
final Tracer tracer = GlobalTracer.get();
51+
final SpanContext spanContext = extractContext(tracer, input);
52+
53+
Span span = buildRootSpan(input, context, tracer, spanContext);
54+
try (Scope scope = tracer.activateSpan(span)) {
55+
Output output = realHandler.apply(input, context);
56+
parseResponse(span, output);
57+
return output;
58+
} catch (Throwable throwable) {
59+
span.log(SpanUtil.createErrorAttributes(throwable));
60+
throw throwable;
61+
} finally {
62+
span.finish();
63+
}
64+
}
65+
66+
protected SpanContext extractContext(Tracer tracer, Object input) {
67+
return HeadersParser.parseAndExtract(tracer, input);
68+
}
69+
70+
protected Span buildRootSpan(
71+
Input input, Context context, Tracer tracer, SpanContext spanContext) {
72+
return SpanUtil.buildSpan(input, context, tracer, spanContext, isColdStart);
73+
}
74+
75+
protected void parseResponse(Span span, Output output) {
76+
ResponseParser.parseResponse(output, span);
77+
}
78+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package com.newrelic.opentracing.aws;
2+
3+
import com.amazonaws.services.lambda.runtime.Context;
4+
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
5+
import io.opentracing.Scope;
6+
import io.opentracing.Span;
7+
import io.opentracing.SpanContext;
8+
import io.opentracing.Tracer;
9+
import io.opentracing.util.GlobalTracer;
10+
import java.io.IOException;
11+
import java.io.InputStream;
12+
import java.io.OutputStream;
13+
14+
/**
15+
* Trace calls to lambda functions, implementing manual JSON serialization.
16+
*
17+
* <p>For flexibility, applications may extend this class to enhance the root span.
18+
*/
19+
public class StreamLambdaTracing {
20+
/**
21+
* One-line instrumentation convenience method.
22+
*
23+
* @param input The invocation event's input stream
24+
* @param output The invocation response output stream
25+
* @param context The invocation context
26+
* @param realHandler The callback that implements the business logic for this event handler
27+
*/
28+
public static void instrument(
29+
InputStream input, OutputStream output, Context context, RequestStreamHandler realHandler)
30+
throws IOException {
31+
new StreamLambdaTracing().instrumentRequest(input, output, context, realHandler);
32+
}
33+
34+
/**
35+
* Instrument a Lambda invocation
36+
*
37+
* @param input The invocation event's input stream
38+
* @param output The invocation response output stream
39+
* @param context The invocation context
40+
* @param realHandler The function that implements the business logic. Will be invoked with the
41+
* input and context parameters, from within the instrumentation scope.
42+
*/
43+
public void instrumentRequest(
44+
InputStream input, OutputStream output, Context context, RequestStreamHandler realHandler)
45+
throws IOException {
46+
final Tracer tracer = GlobalTracer.get();
47+
final SpanContext spanContext = extractContext(tracer, input);
48+
49+
Span span = buildRootSpan(input, context, tracer, spanContext);
50+
try (Scope scope = tracer.activateSpan(span)) {
51+
realHandler.handleRequest(input, output, context);
52+
} catch (Throwable throwable) {
53+
span.log(SpanUtil.createErrorAttributes(throwable));
54+
throw throwable;
55+
} finally {
56+
span.finish();
57+
}
58+
}
59+
60+
protected Span buildRootSpan(
61+
InputStream input, Context context, Tracer tracer, SpanContext spanContext) {
62+
return SpanUtil.buildSpan(input, context, tracer, spanContext, LambdaTracing.isColdStart);
63+
}
64+
65+
protected SpanContext extractContext(Tracer tracer, InputStream input) {
66+
return null;
67+
}
68+
}

src/main/java/com/newrelic/opentracing/aws/TracingRequestHandler.java

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,26 @@
66
package com.newrelic.opentracing.aws;
77

88
import com.amazonaws.services.lambda.runtime.Context;
9-
import io.opentracing.Scope;
10-
import io.opentracing.Span;
119
import io.opentracing.SpanContext;
1210
import io.opentracing.Tracer;
1311
import io.opentracing.propagation.Format;
14-
import io.opentracing.util.GlobalTracer;
15-
import java.util.concurrent.atomic.AtomicBoolean;
1612

1713
/**
1814
* Tracing request handler that creates a span on every invocation of a Lambda.
1915
*
2016
* <p>Implement this interface and update your AWS Lambda Handler name to reference your class name,
2117
* e.g., com.mycompany.HandlerClass
2218
*
19+
* <p>Due to an interaction between Java's type erasure and method inheritance, Input effectively
20+
* must be a Map. For that reason, this interface is deprecated in favor of {@link LambdaTracing}.
21+
*
2322
* @param <Input> The input parameter type
2423
* @param <Output> The output parameter type
2524
*/
25+
@Deprecated
2626
public interface TracingRequestHandler<Input, Output>
2727
extends com.amazonaws.services.lambda.runtime.RequestHandler<Input, Output> {
2828

29-
AtomicBoolean isColdStart = new AtomicBoolean(true);
30-
3129
/**
3230
* Method that handles the Lambda function request.
3331
*
@@ -40,22 +38,7 @@ public interface TracingRequestHandler<Input, Output>
4038
Output doHandleRequest(Input input, Context context);
4139

4240
default Output handleRequest(Input input, Context context) {
43-
final Tracer tracer = GlobalTracer.get();
44-
final SpanContext spanContext = extractContext(tracer, input);
45-
46-
Span span = SpanUtil.buildSpan(input, context, tracer, spanContext, isColdStart);
47-
try (Scope scope = tracer.activateSpan(span)) {
48-
try {
49-
Output output = doHandleRequest(input, context);
50-
ResponseParser.parseResponse(output, span);
51-
return output;
52-
} catch (Throwable throwable) {
53-
span.log(SpanUtil.createErrorAttributes(throwable));
54-
throw throwable;
55-
}
56-
} finally {
57-
span.finish();
58-
}
41+
return LambdaTracing.instrument(input, context, this::doHandleRequest);
5942
}
6043

6144
/**

src/main/java/com/newrelic/opentracing/aws/TracingRequestStreamHandler.java

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,26 @@
66
package com.newrelic.opentracing.aws;
77

88
import com.amazonaws.services.lambda.runtime.Context;
9-
import io.opentracing.Scope;
10-
import io.opentracing.Span;
119
import io.opentracing.SpanContext;
1210
import io.opentracing.Tracer;
1311
import io.opentracing.propagation.Format;
14-
import io.opentracing.util.GlobalTracer;
12+
import java.io.IOException;
1513
import java.io.InputStream;
1614
import java.io.OutputStream;
17-
import java.util.concurrent.atomic.AtomicBoolean;
1815

1916
/**
2017
* Tracing request stream handler that creates a span on every invocation of a Lambda.
2118
*
2219
* <p>Implement this interface and update your AWS Lambda Handler name to reference your class name,
2320
* e.g., com.mycompany.HandlerClass
21+
*
22+
* <p>While RequestStreamHandler's handleRequest method may throw an IOException, doHandleRequest
23+
* may not. For that reason, this interface is deprecated in favor of {@link StreamLambdaTracing}.
2424
*/
25+
@Deprecated
2526
public interface TracingRequestStreamHandler
2627
extends com.amazonaws.services.lambda.runtime.RequestStreamHandler {
2728

28-
AtomicBoolean isColdStart = new AtomicBoolean(true);
29-
3029
/**
3130
* Method that handles the Lambda function request.
3231
*
@@ -39,19 +38,10 @@ public interface TracingRequestStreamHandler
3938
void doHandleRequest(InputStream input, OutputStream output, Context context);
4039

4140
default void handleRequest(InputStream input, OutputStream output, Context context) {
42-
final Tracer tracer = GlobalTracer.get();
43-
final SpanContext spanContext = extractContext(tracer, input);
44-
45-
Span span = SpanUtil.buildSpan(input, context, tracer, spanContext, isColdStart);
46-
try (Scope scope = tracer.activateSpan(span)) {
47-
try {
48-
doHandleRequest(input, output, context);
49-
} catch (Throwable throwable) {
50-
span.log(SpanUtil.createErrorAttributes(throwable));
51-
throw throwable;
52-
}
53-
} finally {
54-
span.finish();
41+
try {
42+
StreamLambdaTracing.instrument(input, output, context, this::doHandleRequest);
43+
} catch (IOException e) {
44+
throw new RuntimeException("Exception while processing Lambda invocation", e);
5545
}
5646
}
5747

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.newrelic.opentracing.aws;
2+
3+
import static org.junit.Assert.assertEquals;
4+
5+
import com.amazonaws.services.lambda.runtime.Context;
6+
import com.amazonaws.services.lambda.runtime.RequestHandler;
7+
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
8+
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
9+
import java.lang.reflect.Method;
10+
import java.util.Arrays;
11+
import org.junit.Before;
12+
import org.junit.Test;
13+
14+
public class ReflectionTest {
15+
private static final int SYNTHETIC_MODIFIER = 0x1000;
16+
17+
private Object handler;
18+
19+
@Before
20+
public void setup() {
21+
handler = new TestHandler();
22+
}
23+
24+
@Test
25+
public void testInputReflection() {
26+
// Ignoring synthetics, we expect handleRequest to take the declared type as its first arg.
27+
// This is necessary for correct payload deserialization.
28+
final Method handleRequest =
29+
Arrays.stream(handler.getClass().getMethods())
30+
.filter(
31+
m ->
32+
((m.getModifiers() & SYNTHETIC_MODIFIER) == 0)
33+
&& m.getName().equals("handleRequest"))
34+
.findFirst()
35+
.orElseThrow(AssertionError::new);
36+
37+
assertEquals(APIGatewayProxyRequestEvent.class, handleRequest.getParameterTypes()[0]);
38+
}
39+
40+
public static class TestHandler
41+
implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
42+
@Override
43+
public APIGatewayProxyResponseEvent handleRequest(
44+
APIGatewayProxyRequestEvent apiGatewayProxyRequestEvent, Context context) {
45+
return LambdaTracing.instrument(
46+
apiGatewayProxyRequestEvent, context, (event, c) -> new APIGatewayProxyResponseEvent());
47+
}
48+
}
49+
}

src/test/java/com/newrelic/opentracing/aws/TracingRequestHandlerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public static void beforeClass() {
4848
public void before() {
4949
mockTracer.reset();
5050
// reset isColdStart before each test
51-
TracingRequestHandler.isColdStart.set(true);
51+
LambdaTracing.isColdStart.set(true);
5252
}
5353

5454
@Test

0 commit comments

Comments
 (0)