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

fix(tracer): use instance as this in capture decorators #1055

Merged
merged 7 commits into from
Aug 16, 2022

Conversation

misterjoshua
Copy link
Contributor

@misterjoshua misterjoshua commented Aug 12, 2022

Description of your changes

This PR fixes bugs in the tracer.captureMethod() and tracer.captureLambdaHandler() decorators. These decorators provide the class prototype as the this scope and not the instance of the class. While the prototype scope allows member functions to be run, it does not allow the customer to access instance member variables, as these instance member variables are not part of the class prototype.

Shout out to @dreamorosi for assistance in building an acceptable proof of concept in Slack.

How to verify this change

I have added two new unit tests which reproduce the bug. You may run them to verify the change.

Related issues, RFCs

This is related to #1028. #1026 previously attempted to fix this and got it us part of the way to a fix.

Issue number: #1056

PR status

Is this ready for review?: YES
Is it a breaking change?: NO

Checklist

  • My changes meet the tenets criteria
  • I have performed a self-review of my own code
  • I have commented my code where necessary, particularly in areas that should be flagged with a TODO, or hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have made corresponding changes to the examples
  • My changes generate no new warnings
  • The code coverage hasn't decreased
  • I have added tests that prove my change is effective and works
  • New and existing unit tests pass locally and in Github Actions
  • Any dependent changes have been merged and published in downstream module
  • The PR title follows the conventional commit semantics

Breaking change checklist

  • I have documented the migration process
  • I have added, implemented necessary warnings (if it can live side by side)

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@misterjoshua
Copy link
Contributor Author

misterjoshua commented Aug 13, 2022

It's unclear from the contribution guide whether, upon acceptance, the reviewer(s) will squash-merge this Pull Request, or if I should rebase from the main branch. Please let me know if you want me to rebase before this PR is acceptable. For now, I've avoided rebasing in favour of a linear feature branch timeline.

On another note: Currently, GitHub says that there are 3 workflows awaiting approval. Once these are approved, I can act on any information I gain from their running.

@misterjoshua misterjoshua marked this pull request as ready for review August 13, 2022 00:08
@misterjoshua misterjoshua changed the title fix(tracer): use instance scope in capture decorators fix(tracer): use instance as this in capture decorators Aug 13, 2022
@github-actions github-actions bot added the bug Something isn't working label Aug 13, 2022
Copy link
Contributor

@dreamorosi dreamorosi left a comment

Choose a reason for hiding this comment

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

Thank you for working on this and providing a potential fix. The proposed change seems appropriate and the unit tests are passing.

I will however momentarily put this PR on hold as I need to do more tests and potentially investigate internally at AWS.

The reason why I'm saying this is that the proposed change works locally or on our CI but doesn't work when run in AWS Lambda.

Take this simplified example:

class DecoratorUtilClass {
  public decorate() {
    return function (
      target: any,
      propertyKey: string,
      descriptor: PropertyDescriptor
    ) {
      const originalMethod = descriptor.value;

      console.log(`Evaluation: Target name ${target.constructor.name}`);
      console.log(
        `Evaluation: ${JSON.stringify(
          Object.getOwnPropertyNames(target),
          null,
          2
        )}`
      );

      descriptor.value = function (...args: any[]) {
        console.log(`Call: ${this.constructor.name}`);
        console.log(
          `Evaluation: ${JSON.stringify(
            Object.getOwnPropertyNames(this),
            null,
            2
          )}`
        );

        // Do something before
        const result = originalMethod.call(this, args);
        // Do something after

        return result;
      };

      return descriptor;
    };
  }
}

const decoratorUtil = new DecoratorUtilClass();

class Lambda {
  greeting: string;

  constructor(message: string) {
    this.greeting = message;
  }

  @decoratorUtil.decorate()
  handler() {
    console.log(`Current Node version ${process.version}`);
    console.log(this.buildGreeting());
  }

  buildGreeting() {
    return "Hello, " + this.greeting;
  }
}

const lambda = new Lambda("world");
module.exports = lambda;

Transpile it to JS with esbuild (esbuild --bundle index.ts --target=node16 --platform=node --outfile=dist/index.js --sourcemap) and then run it both locally & then in a Lambda function.

Locally the result will be:

Evaluation: Target name Lambda
Evaluation: [
  "constructor",
  "handler",
  "buildGreeting"
]
Call: Lambda
Evaluation: [
  "greeting"
]
Current Node version v16.15.0
Hello, world

In Lambda the value of this inside the descriptor.value = function (...args: any[]) { function is not the class but something else, resulting the function to throw error.

packages/tracer/src/Tracer.ts Outdated Show resolved Hide resolved
packages/tracer/src/Tracer.ts Outdated Show resolved Hide resolved
packages/tracer/src/Tracer.ts Outdated Show resolved Hide resolved
packages/tracer/src/Tracer.ts Outdated Show resolved Hide resolved
packages/tracer/src/Tracer.ts Outdated Show resolved Hide resolved
packages/tracer/src/Tracer.ts Show resolved Hide resolved
packages/tracer/src/Tracer.ts Outdated Show resolved Hide resolved
@dreamorosi dreamorosi added on-hold This item is on-hold and will be revisited in the future do-not-merge This item should not be merged labels Aug 13, 2022
@misterjoshua
Copy link
Contributor Author

@dreamorosi I believe I've applied all of your review changes. I'll keep an eye out for your findings. I'll investigate in a Lambda environment as well. If I find anything myself, I'll comment here.

@dreamorosi
Copy link
Contributor

dreamorosi commented Aug 13, 2022

Thank you @misterjoshua, I think I have made some progress.

Running a version of Tracer from this branch in a Lambda environment (you can generate an installer by running npm pack in the packages/tracker folder & then install it in your function pointing to the file), and having a handler like this works:

class Lambda implements LambdaInterface {
  greeting: string;

  constructor(message: string) {
    this.greeting = message;
  }

  @tracer.captureMethod()
  public async getGreeting(): string {
    /* ... */
    return `Hello ${this.greeting}`;
  }

  public async handler(_event: any, _context: any): Promise<void> {
    console.log(await this.getGreeting());
  }
}

export const myFunction = new Lambda("World");
export const handler = myFunction.handler.bind(myFunction);

image

The key is the last line where we re-bind the instance of the class back to the handler. Without this I haven't been able to find any way of making it work (been trying for the past few hours).

The only other way, would be to transform the handler to arrow functions (& possibly all other class methods in the Lambda class that access this). But doing that messes the current implementation & typing of the decorators so I wouldn't advise doing that.

I'm still trying to wrap my head around it but that seems to be the main discriminant. I'm still not sure why locally in unit tests it works and in Lambda it doesn't, but I'm suspecting it's due to how the Lambda service calls the function and its scope.

Let me know if you get to a different conclusion.

@dreamorosi
Copy link
Contributor

If you agree with my assessment, would you mind also adding the .bind() in these places:

For the last two, I think it would be best to add a side note (see screenshot below) that explains that this is really needed only when the decorated methods need to access to the this keyword.

image

To add a note like the above the line would become:

export const handlerClass = new Lambda();
export const handler = handlerClass.handler.bind(handlerClass); // (1)

And then write the note after the example like here.

@misterjoshua
Copy link
Contributor Author

misterjoshua commented Aug 13, 2022

...

The key is the last line where we re-bind the instance of the class back to the handler. Without this I haven't been able to find any way of making it work (been trying for the past few hours).

The only other way, would be to transform the handler to arrow functions (& possibly all other class methods in the Lambda class that access this). But doing that messes the current implementation & typing of the decorators so I wouldn't advise doing that.

I'm still trying to wrap my head around it but that seems to be the main discriminant. I'm still not sure why locally in unit tests it works and in Lambda it doesn't, but I'm suspecting it's due to how the Lambda service calls the function and its scope.

Let me know if you get to a different conclusion.

I see what you're saying. Yes, that makes sense, and I can reproduce the problem exactly as you say. In JavaScript, the this depends on how the function is invoked. When we use a reference to the function without its corresponding object, as in const handler = myFunction.handler; handler(...) JavaScript will operate as though there's no object, hence the need to bind.

I'll make the adjustments you suggest.

@dreamorosi dreamorosi added tracer This item relates to the Tracer Utility and removed on-hold This item is on-hold and will be revisited in the future do-not-merge This item should not be merged labels Aug 13, 2022
@dreamorosi
Copy link
Contributor

I have created a more detailed issue for this #1056, if you could leave a comment under the issue I'd like to assign it to you so that it'll be reflected that you're the one who fixed it.

If you're available and can, I would appreciate if you could also make a couple of small changes to the integration/e2e tests that are run in a real AWS environment (you won't have to run them though):

I think the easiest change would be to:

  • Add a constructor & one attribute to the classes
  • Pass the the customResponseValue to the constructor (which we use to do assertion in the actual tests)
  • Assign the value from the constructor to the attribute
  • Access this.attribute from a decorated method
export class MyFunctionWithDecorator { 
  returnValue: string;

  constructor(customResponseValue: string) {
    this.returnValue = customResponseValue;
  }

  ....

  @tracer.captureMethod()
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  // @ts-ignore
  public myMethod(): string {
    return this.returnValue;
  }
}

export const handlerClass = new MyFunctionWithDecorator(customResponseValue);
export const handler = handlerClass.handler.bind(handlerClass);

After the change is done I'll run the e2e tests and share the result here. If successful I'll approve the PR.

Finally, to manage expectations, if everything goes well and at least another maintainer is available, we are looking at releasing this change on Tue/Wed next week (Monday is a bank holiday in most of our countries so it's likely we won't be able to review/release).

I will also be creating two more issues related to Logger & Metrics since they also are affected from the same issue you're fixing here.

@dreamorosi dreamorosi linked an issue Aug 13, 2022 that may be closed by this pull request
@misterjoshua
Copy link
Contributor Author

@dreamorosi Alright. I've made the E2E test changes, and as of 8ee7f96 they succeed on my account, but notwithstanding, I suggest independently verifying. Aside: It's pretty cool using the CDK to test like this. :)

image

@dreamorosi
Copy link
Contributor

dreamorosi commented Aug 13, 2022

Thank you for the changes & for running them, I have also ran them and was able to see the same result.

Thanks, we are using CDK as part of the Jest phases to temporarily provision the functions & other adjacent infrastructure. If this was a branch from one of the maintainers we would be able to run them also in GitHub Actions and see the result here without having to pull the code. We are working on making this possible safely also for code that comes from forks, so that contributors don't have to provision infrastructure in their environments.

For the next maintainer, see full logs below:

```sh npm run test:e2e -w packages/tracer

@aws-lambda-powertools/tracer@1.1.0 test:e2e
jest --group=e2e

Bundling asset Tracer-E2E-nodejs16x-c697f-AllFeatures-Manual/Tracer-E2E-nodejs16x-c697f-AllFeatures-Manual/Code/Stage...
Bundling asset Tracer-E2E-nodejs16x-05f0d-AllFeatures-Decorator/Tracer-E2E-nodejs16x-d261f-AllFeatures-Decoratory-AllFlagsEnable/Code/Stage...
Bundling asset Tracer-E2E-nodejs16x-0e176-AllFeatures-Decorator/Tracer-E2E-nodejs16x-29b90-AllFeatures-Decoratory-AllFlagsEnable/Code/Stage...
Bundling asset Tracer-E2E-nodejs16x-e8523-AllFeatures-Middy/Tracer-E2E-nodejs16x-f685c-AllFeatures-Middy-AllFlagsEnabled/Code/Stage...

...d265293d330dc7fd874e12c32628ec2ddf9e779df177a313de45199/index.js 1.7mb ⚠️

⚡ Done in 510ms

...9671ee48425e7d7c883c2d5be1ef1fb251d8813dd015d126719311c/index.js 1.7mb ⚠️

⚡ Done in 512ms

...c82f38f581b0cd2e72f135a501be636966737e480f273cd3f5883be/index.js 1.7mb ⚠️

⚡ Done in 558ms

...401571eb8514599311322bc71e70bb8de47e817d23685c51cf8613f/index.js 1.7mb ⚠️

⚡ Done in 607ms
[0%] start: Publishing 54ae90b50204a279fa5fa9def02c02ff54aa0e3036d8fce93baff2348019180f:current_account-current_region
[0%] start: Publishing 781cfdaa026c46d09442324bd985eb98c9f6867060b7b82084802b499a5589b4:current_account-current_region
[0%] start: Publishing 644a47beca698a5f3937031ad15164df129081465e1812a276925f4db1edeec0:current_account-current_region
[0%] start: Publishing a8d7e59d59685600df631a781769d1b0b4d8c844af4394b92278e24504d7059d:current_account-current_region
[0%] start: Publishing bfe6f010ef68896826960e58e5c6d2537ddc3d7a27104fa292c717a28353a34a:current_account-current_region
[0%] start: Publishing b6f72016edbfd5918c76e841851d244b1e1c6149cc40b2cf0eb3bb12ad26df8d:current_account-current_region
[0%] start: Publishing 4c3021b4bbbcf0779d63001db8fd4e7555aaeb0751bf4f7fbdb0f59dd858d52c:current_account-current_region
[0%] start: Publishing bcd6c7f4dd44d44cf659ebd85ede81d0b241174ab45e51d016b061f6873f0b5c:current_account-current_region
[50%] success: Published 4c3021b4bbbcf0779d63001db8fd4e7555aaeb0751bf4f7fbdb0f59dd858d52c:current_account-current_region
[50%] success: Published 54ae90b50204a279fa5fa9def02c02ff54aa0e3036d8fce93baff2348019180f:current_account-current_region
[100%] success: Published 781cfdaa026c46d09442324bd985eb98c9f6867060b7b82084802b499a5589b4:current_account-current_region
[50%] success: Published b6f72016edbfd5918c76e841851d244b1e1c6149cc40b2cf0eb3bb12ad26df8d:current_account-current_region
[50%] success: Published a8d7e59d59685600df631a781769d1b0b4d8c844af4394b92278e24504d7059d:current_account-current_region
[100%] success: Published bcd6c7f4dd44d44cf659ebd85ede81d0b241174ab45e51d016b061f6873f0b5c:current_account-current_region
Tracer-E2E-nodejs16x-c697f-AllFeatures-Manual: creating CloudFormation changeset...
Tracer-E2E-nodejs16x-e8523-AllFeatures-Middy: creating CloudFormation changeset...
[100%] success: Published bfe6f010ef68896826960e58e5c6d2537ddc3d7a27104fa292c717a28353a34a:current_account-current_region
[100%] success: Published 644a47beca698a5f3937031ad15164df129081465e1812a276925f4db1edeec0:current_account-current_region
Tracer-E2E-nodejs16x-05f0d-AllFeatures-Decorator: creating CloudFormation changeset...
Tracer-E2E-nodejs16x-0e176-AllFeatures-Decorator: creating CloudFormation changeset...
PASS AWS Lambda Powertools utility: TRACER tests/e2e/asyncHandler.decorator.test.ts (116.723 s)
● Console

console.log
  Manual query: aws xray get-trace-summaries --start-time 1660424081 --end-time 1660424156 --filter-expression 'resource.arn = "arn:aws:lambda:eu-west-1:264025721901:function:Tracer-E2E-nodejs16x-d261f-AllFeatures-Decoratory-AllFlagsEnable"'

  at tests/helpers/tracesUtils.ts:94:13

console.log
  Manual query: aws xray get-trace-summaries --start-time 1660424081 --end-time 1660424161 --filter-expression 'resource.arn = "arn:aws:lambda:eu-west-1:264025721901:function:Tracer-E2E-nodejs16x-d261f-AllFeatures-Decoratory-AllFlagsEnable"'

  at tests/helpers/tracesUtils.ts:94:13

console.log
  Manual query: aws xray get-trace-summaries --start-time 1660424081 --end-time 1660424162 --filter-expression 'resource.arn = "arn:aws:lambda:eu-west-1:264025721901:function:Tracer-E2E-nodejs16x-d261f-AllFeatures-Decoratory-AllFlagsEnable"'

  at tests/helpers/tracesUtils.ts:94:13

PASS AWS Lambda Powertools utility: TRACER tests/e2e/allFeatures.middy.test.ts (121.675 s)
● Console

console.log
  Manual query: aws xray get-trace-summaries --start-time 1660424081 --end-time 1660424160 --filter-expression 'resource.arn = "arn:aws:lambda:eu-west-1:264025721901:function:Tracer-E2E-nodejs16x-f685c-AllFeatures-Middy-AllFlagsEnabled"'

  at tests/helpers/tracesUtils.ts:94:13

console.log
  Manual query: aws xray get-trace-summaries --start-time 1660424081 --end-time 1660424166 --filter-expression 'resource.arn = "arn:aws:lambda:eu-west-1:264025721901:function:Tracer-E2E-nodejs16x-f685c-AllFeatures-Middy-AllFlagsEnabled"'

  at tests/helpers/tracesUtils.ts:94:13

console.log
  Manual query: aws xray get-trace-summaries --start-time 1660424081 --end-time 1660424166 --filter-expression 'resource.arn = "arn:aws:lambda:eu-west-1:264025721901:function:Tracer-E2E-nodejs16x-f685c-AllFeatures-Middy-AllFlagsEnabled"'

  at tests/helpers/tracesUtils.ts:94:13

console.log
  Manual query: aws xray get-trace-summaries --start-time 1660424081 --end-time 1660424167 --filter-expression 'resource.arn = "arn:aws:lambda:eu-west-1:264025721901:function:Tracer-E2E-nodejs16x-0137c-AllFeatures-Middy-NoCaptureErrorOrRes"'

  at tests/helpers/tracesUtils.ts:94:13

console.log
  Manual query: aws xray get-trace-summaries --start-time 1660424081 --end-time 1660424167 --filter-expression 'resource.arn = "arn:aws:lambda:eu-west-1:264025721901:function:Tracer-E2E-nodejs16x-3c044-AllFeatures-Middy-TracerDisabled"'

  at tests/helpers/tracesUtils.ts:94:13

PASS AWS Lambda Powertools utility: TRACER tests/e2e/allFeatures.manual.test.ts (124.108 s)
● Console

console.log
  Manual query: aws xray get-trace-summaries --start-time 1660424081 --end-time 1660424163 --filter-expression 'resource.arn = "arn:aws:lambda:eu-west-1:264025721901:function:Tracer-E2E-nodejs16x-c697f-AllFeatures-Manual"'

  at tests/helpers/tracesUtils.ts:94:13

console.log
  Manual query: aws xray get-trace-summaries --start-time 1660424081 --end-time 1660424169 --filter-expression 'resource.arn = "arn:aws:lambda:eu-west-1:264025721901:function:Tracer-E2E-nodejs16x-c697f-AllFeatures-Manual"'

  at tests/helpers/tracesUtils.ts:94:13

PASS AWS Lambda Powertools utility: TRACER tests/e2e/allFeatures.decorator.test.ts (131.776 s)
● Console

console.log
  Manual query: aws xray get-trace-summaries --start-time 1660424081 --end-time 1660424165 --filter-expression 'resource.arn = "arn:aws:lambda:eu-west-1:264025721901:function:Tracer-E2E-nodejs16x-29b90-AllFeatures-Decoratory-AllFlagsEnable"'

  at tests/helpers/tracesUtils.ts:94:13

console.log
  Manual query: aws xray get-trace-summaries --start-time 1660424081 --end-time 1660424171 --filter-expression 'resource.arn = "arn:aws:lambda:eu-west-1:264025721901:function:Tracer-E2E-nodejs16x-29b90-AllFeatures-Decoratory-AllFlagsEnable"'

  at tests/helpers/tracesUtils.ts:94:13

console.log
  Manual query: aws xray get-trace-summaries --start-time 1660424081 --end-time 1660424171 --filter-expression 'resource.arn = "arn:aws:lambda:eu-west-1:264025721901:function:Tracer-E2E-nodejs16x-29b90-AllFeatures-Decoratory-AllFlagsEnable"'

  at tests/helpers/tracesUtils.ts:94:13

console.log
  Manual query: aws xray get-trace-summaries --start-time 1660424081 --end-time 1660424171 --filter-expression 'resource.arn = "arn:aws:lambda:eu-west-1:264025721901:function:Tracer-E2E-nodejs16x-eafd1-AllFeatures-Decorator-NoCaptureErrorO"'

  at tests/helpers/tracesUtils.ts:94:13

console.log
  Manual query: aws xray get-trace-summaries --start-time 1660424081 --end-time 1660424171 --filter-expression 'resource.arn = "arn:aws:lambda:eu-west-1:264025721901:function:Tracer-E2E-nodejs16x-4e0cb-AllFeatures-Decorator-TracerDisabled"'

  at tests/helpers/tracesUtils.ts:94:13

console.log
  Manual query: aws xray get-trace-summaries --start-time 1660424081 --end-time 1660424177 --filter-expression 'resource.arn = "arn:aws:lambda:eu-west-1:264025721901:function:Tracer-E2E-nodejs16x-4e0cb-AllFeatures-Decorator-TracerDisabled"'

  at tests/helpers/tracesUtils.ts:94:13

Test Suites: 4 passed, 4 of 9 total
Tests: 12 passed, 12 total
Snapshots: 0 total
Time: 132.442 s, estimated 707 s
Ran all test suites.

</details>

Copy link
Contributor

@dreamorosi dreamorosi left a comment

Choose a reason for hiding this comment

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

Thanks a lot @misterjoshua for taking the time to investigate & make all the changes, I'm sure this will help a lot of other customers.

@dreamorosi
Copy link
Contributor

I have requested a review from two other maintainers, we have a two approvals model so as soon as one of them reviews (and any comment they might have is addressed), we'll be able to merge.

I have also opened two more issues that document the same exact issue but for Logger & Metrics:

Not sure if you're interested or if you're even an user of those two utilities, but in case you'd like to contribute please feel free to leave a comment under the relevant issue. I'll be happy to work with you to review the PR. If not, thanks a lot for the great work you've done already to uncover & fix the issue with Tracer.

@misterjoshua
Copy link
Contributor Author

I have requested a review from two other maintainers, we have a two approvals model so as soon as one of them reviews (and any comment they might have is addressed), we'll be able to merge.

Thank you. I'll keep an eye on this PR in case there are any changes.

Not sure if you're interested or if you're even an user of those two utilities, but in case you'd like to contribute please feel free to leave a comment under the relevant issue. I'll be happy to work with you to review the PR. If not, thanks a lot for the great work you've done already to uncover & fix the issue with Tracer.

Unfortunately, I can't take on #1057 or #1058 at this time, but I'll check back in a few weeks to see where we're at.

@dreamorosi
Copy link
Contributor

Note to reviewers: don't merge this until #1054 is merged.

Copy link
Contributor

@ijemmy ijemmy 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 this PR. It's a great catch.

One change I would request is to update the examples/cdk/src/*.Decorator.ts files. They still don't use .bind() method.

Copy link
Contributor

@ijemmy ijemmy left a comment

Choose a reason for hiding this comment

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

Approved.

Discussed with @dreamorosi , we'll go with this to push the fix first.

The example will be updated in a separated PR.

@dreamorosi dreamorosi merged commit 107fa04 into aws-powertools:main Aug 16, 2022
@misterjoshua misterjoshua deleted the fix-capture-scope branch August 16, 2022 15:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working tracer This item relates to the Tracer Utility
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Bug: decorated class methods cannot access this in Tracer
3 participants