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-sdk): lambda client instrumentation #916

Merged

Conversation

chrisrichardsevergreen
Copy link
Contributor

Which problem is this PR solving?

Implements instrumentation of the AWS SDK 'Lambda' calls (invoke) using FAAS trace semantics - this follows on from discussions on PR #877

Short description of the changes

  • Add lambda service extension adding faas_* attributes to the request span, and propagating the context into the ClientContext custom field on the Invoke request.

Checklist

  • Ran npm run test-all-versions for the edited package(s) on the latest commit if applicable.

@codecov
Copy link

codecov bot commented Feb 22, 2022

Codecov Report

Merging #916 (1eedc98) into main (cc37e3f) will increase coverage by 0.14%.
The diff coverage is 97.95%.

@@            Coverage Diff             @@
##             main     #916      +/-   ##
==========================================
+ Coverage   95.91%   96.05%   +0.14%     
==========================================
  Files          13       24      +11     
  Lines         856     1394     +538     
  Branches      178      289     +111     
==========================================
+ Hits          821     1339     +518     
- Misses         35       55      +20     
Impacted Files Coverage Δ
...try-instrumentation-aws-sdk/src/services/lambda.ts 97.77% <97.77%> (ø)
...entelemetry-instrumentation-aws-sdk/src/aws-sdk.ts 97.44% <100.00%> (ø)
...ntation-aws-sdk/src/services/ServicesExtensions.ts 100.00% <100.00%> (ø)
...emetry-instrumentation-aws-sdk/src/services/sns.ts 93.75% <0.00%> (ø)
...etry-instrumentation-aws-sdk/src/services/index.ts 100.00% <0.00%> (ø)
...y-instrumentation-aws-sdk/src/services/dynamodb.ts 100.00% <0.00%> (ø)
...opentelemetry-instrumentation-aws-sdk/src/utils.ts 97.36% <0.00%> (ø)
...opentelemetry-instrumentation-aws-sdk/src/enums.ts 100.00% <0.00%> (ø)
...tapackages/auto-instrumentations-node/src/utils.ts 97.87% <0.00%> (ø)
...emetry-instrumentation-aws-sdk/src/services/sqs.ts 89.65% <0.00%> (ø)
... and 3 more

@blumamir blumamir changed the title Aws sdk lambda instrumentation feat(aws-sdk): lambda client instrumentation Feb 25, 2022

// 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 (encodedPayload.length > 3583) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the function is invoked multiple times (for example, traceparent and tracestate for W3CTraceContextPropagator, or the 4 fields of B3MultiPropagator propagator), it is possible that only part of the context will be injected and some will be missing after the limit is reached.
For SQS / SNS, the inject function is first checking if all context fields can be injected successfully and only then attempts any modifications to the request.

What do you think about using a similar approach here as well?

This will also be more performant, as currently, we decode the base64 string, JSON.parse it, then JSON stringify it and encode it back to base64, again and again for each field.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noted that approach, but wasn't sure if there was a precise enough equivalent to the 'will this send it over the limit' question, when we first don't know the length of the key + value (the SQS/SNS equivalent is for max number of keys, the values are free in length, so don't need checking) and we would have to use a bodgy encoded => base64 length mapping formula. I'll give it a try to see if I can get all tests passing but it might need a final check before setting just in case despite best endeavours it still has managed to go over the limit.

Copy link
Member

@blumamir blumamir Feb 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I imagined is something like this (haven't really run it, it's just a scatch):

const injectPropagationContext = (
  invocationRequest: InvocationRequest
): InvocationRequest => {
  try {
    const propagationKeysValues = {};
    propagation.inject(context.active(), propagationKeysValues);

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

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

    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 invocationRequest;      
    }

    return { 
      ...invocationRequest,
      ClientContext: encodedClientContext,
    }

  } catch(err) {
    diag.error(
      'lambda instrumentation: failed to set context propagation on ClientContext',
      err
    );
    return invocationRequest;
  }
};

This code will either inject the entire propagation data, or none of it. It will also JSON.parse + Buffer.from and JSON.stringify + toString('base64') the client context at most once, regardless of the number of propagation keys.
It will also create a copy of the request params, instead of modifying the user's object which might introduce side effects

Copy link
Member

@blumamir blumamir left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing everything and for writing all these high-quality tests 🥇
We are almost ready, added a few more minor comments.
@NathanielRN @willarmiros do you want to review as well?

Copy link
Contributor

@NathanielRN NathanielRN left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks good to me from a high level. I added a few comments, but I'll defer to @blumamir for the final review.

) {
const operation = response.request.commandName;

if (operation === 'Invoke') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a switch statement too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably a space below this would make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The linter keeps removing that, so unfortunately I can't

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants