Skip to content

Commit 0cde221

Browse files
author
Matt Whelan
committed
Address type erasure/reflection issue
This is a pretty broad rewrite, because the former approach cannot work. TracingRequestHandler's type parameters erase to `Object` at runtime, which defeats AWS's reflection-based automatic deserialization mechanism. The new approach uses composition over inheritance: the user provides their own event handler, using any mechanism AWS supports, and wraps their business logic with our instrumentation. This allows reflection to work as intended. In addition, the TracingRequestStreamHandler class's doHandler method didn't allow for IOException to be thrown, despite it being more or less inevitable. `StreamLambdaTracing` addresses this issue, while mirroring the design of `LambdaTracing`.
1 parent c535ab1 commit 0cde221

File tree

8 files changed

+225
-98
lines changed

8 files changed

+225
-98
lines changed

README.md

+22-23
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

@@ -59,36 +65,29 @@ dependencies {
5965
package com.handler.example;
6066

6167
import com.amazonaws.services.lambda.runtime.Context;
62-
import com.newrelic.opentracing.aws.TracingRequestHandler;
68+
import com.amazonaws.services.lambda.runtime.RequestHandler;
69+
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
70+
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
71+
import com.newrelic.opentracing.LambdaTracer;
72+
import com.newrelic.opentracing.aws.LambdaTracing;
6373
import io.opentracing.util.GlobalTracer;
6474

6575
import java.util.Map;
6676

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> {
77+
public static class YourLambdaHandler implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
7478
static {
7579
// Obtain an instance of the OpenTracing Tracer of your choice
76-
Tracer tracer = new CustomTracer(...);
80+
Tracer tracer = LambdaTracer.INSTANCE;
7781
// Register your tracer as the Global Tracer
7882
GlobalTracer.registerIfAbsent(tracer);
7983
}
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-
*/
84+
8885
@Override
89-
public String doHandleRequest(Map<String, Object> input, Context context) {
90-
// Your function logic here
91-
return "Lambda Function output";
86+
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent apiGatewayProxyRequestEvent, Context context) {
87+
return LambdaTracing.instrument(apiGatewayProxyRequestEvent, context, (event, ctx) -> {
88+
// Your business logic here
89+
return doSomethingWithTheEvent(event);
90+
});
9291
}
9392
}
9493
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
try {
56+
Output output = realHandler.apply(input, context);
57+
parseResponse(span, output);
58+
return output;
59+
} catch (Throwable throwable) {
60+
span.log(SpanUtil.createErrorAttributes(throwable));
61+
throw throwable;
62+
}
63+
} finally {
64+
span.finish();
65+
}
66+
}
67+
68+
protected SpanContext extractContext(Tracer tracer, Object input) {
69+
return HeadersParser.parseAndExtract(tracer, input);
70+
}
71+
72+
protected Span buildRootSpan(
73+
Input input, Context context, Tracer tracer, SpanContext spanContext) {
74+
return SpanUtil.buildSpan(input, context, tracer, spanContext, isColdStart);
75+
}
76+
77+
protected void parseResponse(Span span, Output output) {
78+
ResponseParser.parseResponse(output, span);
79+
}
80+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
try {
52+
realHandler.handleRequest(input, output, context);
53+
} catch (Throwable throwable) {
54+
span.log(SpanUtil.createErrorAttributes(throwable));
55+
throw throwable;
56+
}
57+
} finally {
58+
span.finish();
59+
}
60+
}
61+
62+
protected Span buildRootSpan(
63+
InputStream input, Context context, Tracer tracer, SpanContext spanContext) {
64+
return SpanUtil.buildSpan(input, context, tracer, spanContext, LambdaTracing.isColdStart);
65+
}
66+
67+
protected SpanContext extractContext(Tracer tracer, InputStream input) {
68+
return null;
69+
}
70+
}

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

+5-22
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

+9-19
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(e);
5545
}
5646
}
5747

0 commit comments

Comments
 (0)