Skip to content

Commit

Permalink
feat(aws-sdk): lambda client instrumentation (open-telemetry#916)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisrichardsevergreen committed Mar 13, 2022
1 parent cc37e3f commit dc6c2b5
Show file tree
Hide file tree
Showing 8 changed files with 557 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ Specific service logic currently implemented for:

- [SQS](./docs/sqs.md)
- [SNS](./docs/sns.md)
- [Lambda](./docs/lambda.md)
- DynamoDb

## Potential Side Effects
Expand Down
12 changes: 12 additions & 0 deletions plugins/node/opentelemetry-instrumentation-aws-sdk/doc/lambda.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Lambda

Lambda is Amazon's function-as-a-service (FaaS) platform. This instrumentation follows the [OpenTelemetry specification for FaaS systems](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/faas.md).

## Specific trace semantics

The following methods are automatically enhanced:

### Invoke

- Attributes are added by this instrumentation according to the [spec for Outgoing Invocations of a FaaS from a client](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/faas.md#outgoing-invocations) .
- OpenTelemetry trace context is injected into the `ClientContext` parameter, allowing functions to extract this using the `Custom` property within the function.
Original file line number Diff line number Diff line change
Expand Up @@ -53,24 +53,25 @@
},
"devDependencies": {
"@aws-sdk/client-dynamodb": "3.37.0",
"@aws-sdk/client-lambda": "3.37.0",
"@aws-sdk/client-s3": "3.37.0",
"@aws-sdk/client-sqs": "3.37.0",
"@aws-sdk/types": "3.37.0",
"@opentelemetry/api": "1.0.1",
"@opentelemetry/contrib-test-utils": "^0.29.0",
"@opentelemetry/sdk-trace-base": "1.0.1",
"@types/mocha": "8.2.3",
"@types/node": "16.11.21",
"@types/sinon": "10.0.6",
"aws-sdk": "2.1008.0",
"eslint": "8.7.0",
"expect": "27.4.2",
"gts": "3.1.0",
"mocha": "7.2.0",
"nock": "13.2.1",
"nyc": "15.1.0",
"rimraf": "3.0.2",
"sinon": "13.0.1",
"gts": "3.1.0",
"@opentelemetry/contrib-test-utils": "^0.29.0",
"test-all-versions": "5.0.1",
"ts-mocha": "8.0.0",
"typescript": "4.3.4"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,9 +311,11 @@ export class AwsInstrumentation extends InstrumentationBase<typeof AWS> {
}
delete v2Request[REQUEST_SPAN_KEY];

const requestId = response.requestId;
const normalizedResponse: NormalizedResponse = {
data: response.data,
request: normalizedRequest,
requestId: requestId,
};

self._callUserResponseHook(span, normalizedResponse);
Expand All @@ -328,7 +330,7 @@ export class AwsInstrumentation extends InstrumentationBase<typeof AWS> {
);
}

span.setAttribute(AttributeNames.AWS_REQUEST_ID, response.requestId);
span.setAttribute(AttributeNames.AWS_REQUEST_ID, requestId);

const httpStatusCode = response.httpResponse?.statusCode;
if (httpStatusCode) {
Expand Down Expand Up @@ -503,6 +505,7 @@ export class AwsInstrumentation extends InstrumentationBase<typeof AWS> {
const normalizedResponse: NormalizedResponse = {
data: response.output,
request: normalizedRequest,
requestId: requestId,
};
self.servicesExtensions.responseHook(
normalizedResponse,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from '../types';
import { DynamodbServiceExtension } from './dynamodb';
import { SnsServiceExtension } from './sns';
import { LambdaServiceExtension } from './lambda';

export class ServicesExtensions implements ServiceExtension {
services: Map<string, ServiceExtension> = new Map();
Expand All @@ -31,6 +32,7 @@ export class ServicesExtensions implements ServiceExtension {
this.services.set('SQS', new SqsServiceExtension());
this.services.set('SNS', new SnsServiceExtension());
this.services.set('DynamoDB', new DynamodbServiceExtension());
this.services.set('Lambda', new LambdaServiceExtension());
}

requestPreSpanHook(request: NormalizedRequest): RequestMetadata {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.
*/
import {
Span,
SpanKind,
Tracer,
diag,
SpanAttributes,
} from '@opentelemetry/api';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
import {
AwsSdkInstrumentationConfig,
NormalizedRequest,
NormalizedResponse,
} from '../types';
import { RequestMetadata, ServiceExtension } from './ServiceExtension';
import { context, propagation } from '@opentelemetry/api';

class LambdaCommands {
public static readonly Invoke: string = 'Invoke';
}

export class LambdaServiceExtension implements ServiceExtension {
requestPreSpanHook(request: NormalizedRequest): RequestMetadata {
const functionName = this.extractFunctionName(request.commandInput);

let spanAttributes: SpanAttributes = {};
let spanName: string | undefined;

switch (request.commandName) {
case 'Invoke':
spanAttributes = {
[SemanticAttributes.FAAS_INVOKED_NAME]: functionName,
[SemanticAttributes.FAAS_INVOKED_PROVIDER]: 'aws',
};
if (request.region) {
spanAttributes[SemanticAttributes.FAAS_INVOKED_REGION] =
request.region;
}
spanName = `${functionName} ${LambdaCommands.Invoke}`;
break;
}
return {
isIncoming: false,
spanAttributes,
spanKind: SpanKind.CLIENT,
spanName,
};
}

requestPostSpanHook = (request: NormalizedRequest) => {
switch (request.commandName) {
case LambdaCommands.Invoke:
{
if (request.commandInput) {
request.commandInput.ClientContext = injectLambdaPropagationContext(
request.commandInput.ClientContext
);
}
}
break;
}
};

responseHook(
response: NormalizedResponse,
span: Span,
tracer: Tracer,
config: AwsSdkInstrumentationConfig
) {
switch (response.request.commandName) {
case LambdaCommands.Invoke:
{
span.setAttribute(
SemanticAttributes.FAAS_EXECUTION,
response.requestId
);
}
break;
}
}

extractFunctionName = (commandInput: Record<string, any>): string => {
return commandInput?.FunctionName;
};
}

const injectLambdaPropagationContext = (
clientContext: string | undefined
): string | undefined => {
try {
const propagatedContext = {};
propagation.inject(context.active(), propagatedContext);

const parsedClientContext = clientContext
? JSON.parse(Buffer.from(clientContext, 'base64').toString('utf8'))
: {};

const updatedClientContext = {
...parsedClientContext,
Custom: {
...parsedClientContext.Custom,
...propagatedContext,
},
};

const encodedClientContext = Buffer.from(
JSON.stringify(updatedClientContext)
).toString('base64');

// The length of client context is capped at 3583 bytes of base64 encoded data
// (https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html#API_Invoke_RequestSyntax)
if (encodedClientContext.length > 3583) {
diag.warn(
'lambda instrumentation: cannot set context propagation on lambda invoke parameters due to ClientContext length limitations.'
);
return clientContext;
}

return encodedClientContext;
} catch (e) {
diag.debug(
'lambda instrumentation: failed to set context propagation on ClientContext',
e
);
return clientContext;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface NormalizedRequest {
export interface NormalizedResponse {
data: any;
request: NormalizedRequest;
requestId: string;
}

export interface AwsSdkRequestHookInformation {
Expand Down
Loading

0 comments on commit dc6c2b5

Please sign in to comment.