Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(aws-lambda): added eventContextExtractor config option #566

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -47,6 +47,7 @@ In your Lambda function configuration, add or update the `NODE_OPTIONS` environm
| `requestHook` | `RequestHook` (function) | Hook for adding custom attributes before lambda starts handling the request. Receives params: `span, { event, context }` |
| `responseHook` | `ResponseHook` (function) | Hook for adding custom attributes before lambda returns the response. Receives params: `span, { err?, res? } ` |
| `disableAwsContextPropagation` | `boolean` | By default, this instrumentation will try to read the context from the `_X_AMZN_TRACE_ID` environment variable set by Lambda, set this to `true` to disable this behavior |
| `eventContextExtractor` | `EventContextExtractor` (function) | Function for providing custom context extractor in order to support different event types that are handled by AWS Lambda (e.g., SQS, CloudWatch, Kinesis, API Gateway). Applied only when `disableAwsContextPropagation` is set to `true`. Receives params: `event` |

### Hooks Usage Example

Expand Down
Expand Up @@ -53,7 +53,11 @@ import {
Handler,
} from 'aws-lambda';

import { LambdaModule, AwsLambdaInstrumentationConfig } from './types';
import {
LambdaModule,
AwsLambdaInstrumentationConfig,
EventContextExtractor,
} from './types';
import { VERSION } from './version';

const awsPropagator = new AWSXRayPropagator();
Expand Down Expand Up @@ -149,11 +153,12 @@ export class AwsLambdaInstrumentation extends InstrumentationBase {
context: Context,
callback: Callback
) {
const httpHeaders =
typeof event.headers === 'object' ? event.headers : {};
const config = plugin._config;
const parent = AwsLambdaInstrumentation._determineParent(
httpHeaders,
plugin._config.disableAwsContextPropagation === true
event,
config.disableAwsContextPropagation === true,
config.eventContextExtractor ||
AwsLambdaInstrumentation._defaultEventContextExtractor
);

const name = context.functionName;
Expand All @@ -173,9 +178,9 @@ export class AwsLambdaInstrumentation extends InstrumentationBase {
parent
);

if (plugin._config.requestHook) {
if (config.requestHook) {
prsnca marked this conversation as resolved.
Show resolved Hide resolved
safeExecuteInTheMiddle(
() => plugin._config.requestHook!(span, { event, context }),
() => config.requestHook!(span, { event, context }),
e => {
if (e)
diag.error('aws-lambda instrumentation: requestHook error', e);
Expand Down Expand Up @@ -300,12 +305,18 @@ export class AwsLambdaInstrumentation extends InstrumentationBase {
return undefined;
}

private static _defaultEventContextExtractor(event: any): OtelContext {
// The default extractor tries to get sampled trace header from HTTP headers.
const httpHeaders = event.headers || {};
return propagation.extract(otelContext.active(), httpHeaders, headerGetter);
}

private static _determineParent(
httpHeaders: APIGatewayProxyEventHeaders,
disableAwsContextPropagation: boolean
event: any,
disableAwsContextPropagation: boolean,
eventContextExtractor: EventContextExtractor
): OtelContext {
let parent: OtelContext | undefined = undefined;

if (!disableAwsContextPropagation) {
const lambdaTraceHeader = process.env[traceContextEnvironmentKey];
if (lambdaTraceHeader) {
Expand All @@ -328,15 +339,19 @@ export class AwsLambdaInstrumentation extends InstrumentationBase {
}
}
}

// There was not a sampled trace header from Lambda so try from HTTP headers.
const httpContext = propagation.extract(
otelContext.active(),
httpHeaders,
headerGetter
const extractedContext = safeExecuteInTheMiddle(
() => eventContextExtractor(event),
e => {
if (e)
diag.error(
'aws-lambda instrumentation: eventContextExtractor error',
e
);
},
true
);
if (trace.getSpan(httpContext)?.spanContext()) {
return httpContext;
if (trace.getSpan(extractedContext)?.spanContext()) {
return extractedContext;
prsnca marked this conversation as resolved.
Show resolved Hide resolved
}
if (!parent) {
// No context in Lambda environment or HTTP headers.
Expand Down
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { Span } from '@opentelemetry/api';
import { Span, Context as OtelContext } from '@opentelemetry/api';
import { InstrumentationConfig } from '@opentelemetry/instrumentation';
import { Handler, Context } from 'aws-lambda';

Expand All @@ -33,8 +33,10 @@ export type ResponseHook = (
}
) => void;

export type EventContextExtractor = (event: any) => OtelContext;
export interface AwsLambdaInstrumentationConfig extends InstrumentationConfig {
requestHook?: RequestHook;
responseHook?: ResponseHook;
disableAwsContextPropagation?: boolean;
eventContextExtractor?: EventContextExtractor;
}
Expand Up @@ -37,7 +37,9 @@ import {
ResourceAttributes,
} from '@opentelemetry/semantic-conventions';
import {
Context as OtelContext,
context,
propagation,
trace,
SpanContext,
SpanKind,
Expand Down Expand Up @@ -166,6 +168,17 @@ describe('lambda handler', () => {
new HttpTraceContextPropagator()
);

const sampledGenericSpanContext: SpanContext = {
traceId: '8a3c60f7d188f8fa79d48a391a778faa',
spanId: '0000000000000460',
traceFlags: 1,
isRemote: true,
};
const sampledGenericSpan = serializeSpanContext(
sampledGenericSpanContext,
new HttpTraceContextPropagator()
);

beforeEach(() => {
oldEnv = { ...process.env };
process.env.LAMBDA_TASK_ROOT = path.resolve(__dirname, '..');
Expand Down Expand Up @@ -577,6 +590,40 @@ describe('lambda handler', () => {
);
assert.strictEqual(span.parentSpanId, sampledHttpSpanContext.spanId);
});

it('takes sampled custom context over sampled lambda context if "eventContextExtractor" is defined', async () => {
process.env[traceContextEnvironmentKey] = sampledAwsHeader;
const customExtractor = (event: any): OtelContext => {
return propagation.extract(context.active(), event.contextCarrier);
};

initializeHandler('lambda-test/async.handler', {
disableAwsContextPropagation: true,
eventContextExtractor: customExtractor,
});

const otherEvent = {
contextCarrier: {
traceparent: sampledGenericSpan,
},
};

const result = await lambdaRequire('lambda-test/async').handler(
otherEvent,
ctx
);

assert.strictEqual(result, 'ok');
const spans = memoryExporter.getFinishedSpans();
const [span] = spans;
assert.strictEqual(spans.length, 1);
assertSpanSuccess(span);
assert.strictEqual(
span.spanContext().traceId,
sampledGenericSpanContext.traceId
);
assert.strictEqual(span.parentSpanId, sampledGenericSpanContext.spanId);
});
});

describe('hooks', () => {
Expand Down