Skip to content

Commit 3c449ea

Browse files
committed
Added capture method
1 parent f2325b5 commit 3c449ea

File tree

4 files changed

+234
-28
lines changed

4 files changed

+234
-28
lines changed

packages/tracing/README.md

+111-8
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked
9898
"ColdStart": true
9999
},
100100
"metadata": {
101-
"my-service": {
101+
"hello-world": {
102102
"foo-bar-function response": {
103103
"foo": "bar"
104104
}
@@ -217,7 +217,7 @@ new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked
217217
```
218218
</details>
219219

220-
### Add annotation on subsegment
220+
### Adding annotation on subsegment
221221

222222
```typescript
223223
// Environment variables set for the Lambda
@@ -290,7 +290,7 @@ new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked
290290
"my-annotation": "my-value"
291291
},
292292
"metadata": {
293-
"my-service": {
293+
"hello-world": {
294294
"foo-bar-function response": {
295295
"foo": "bar"
296296
}
@@ -309,7 +309,7 @@ new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked
309309
```
310310
</details>
311311

312-
### Add metadata to subsegment
312+
### Adding metadata to subsegment
313313

314314
```typescript
315315
// Environment variables set for the Lambda
@@ -382,7 +382,7 @@ new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked
382382
"ColdStart": true
383383
},
384384
"metadata": {
385-
"my-service": {
385+
"hello-world": {
386386
"foo-bar-function response": {
387387
"foo": "bar"
388388
},
@@ -405,7 +405,110 @@ new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked
405405
```
406406
</details>
407407

408-
### Capture AWS SDK clients
408+
### Capturing other methods
409+
410+
```typescript
411+
// Environment variables set for the Lambda
412+
process.env.POWERTOOLS_SERVICE_NAME = 'hello-world';
413+
414+
const tracer = new Tracer();
415+
416+
class Lambda implements LambdaInterface {
417+
@tracer.captureMethod()
418+
public async dummyMethod(some: string): Promise<string> {
419+
// Some async logic
420+
return new Promise((resolve, _reject) => setTimeout(() => resolve(some), 3000));
421+
}
422+
423+
public async handler<TEvent, TResult>(_event: TEvent, _context: Context, _callback: Callback<TResult>): Promise<TResult> {
424+
const result = await this.dummyMethod('bar');
425+
426+
return new Promise((resolve, _reject) => resolve({
427+
foo: result
428+
} as unknown as TResult));
429+
}
430+
431+
}
432+
433+
new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!'));
434+
435+
```
436+
437+
<details>
438+
<summary>Click to expand and see the trace</summary>
439+
440+
```json
441+
{
442+
"Id": "abcdef123456abcdef123456abcdef123456",
443+
"Duration": 0.656,
444+
"LimitExceeded": false,
445+
"Segments": [
446+
{
447+
"Id": "1234567890abcdef0",
448+
"Document": {
449+
"id": "1234567890abcdef0",
450+
"name": "foo-bar-function",
451+
"start_time": 1638792392.764036,
452+
"trace_id": "abcdef123456abcdef123456abcdef123456",
453+
"end_time": 1638792392.957155,
454+
"parent_id": "abcdef01234567890",
455+
"aws": {
456+
"account_id": "111122223333",
457+
"function_arn": "arn:aws:lambda:us-east-1:111122223333:function:foo-bar-function",
458+
"resource_names": [
459+
"foo-bar-function"
460+
]
461+
},
462+
"origin": "AWS::Lambda::Function",
463+
"subsegments": [
464+
// Initialization subsegment (if any)
465+
{
466+
"id": "4be0933d48d5b52f",
467+
"name": "Invocation",
468+
"start_time": 1638792392.7642102,
469+
"end_time": 1638792392.9384046,
470+
"aws": {
471+
"function_arn": "arn:aws:lambda:eu-west-1:111122223333:function:foo-bar-function"
472+
},
473+
"subsegments": [
474+
{
475+
"id": "aae0c94a16d66abd",
476+
"name": "## foo-bar-function",
477+
"start_time": 1638792392.766,
478+
"end_time": 1638792392.836,
479+
"annotations": {
480+
"ColdStart": true
481+
},
482+
"subsegments": [
483+
{
484+
"id": "b1ac78c231577476",
485+
"name": "### dummyMethod",
486+
"start_time": 1638845552.777,
487+
"end_time": 1638845553.459,
488+
"metadata": {
489+
"hello-world": {
490+
"dummyMethod response": "bar"
491+
}
492+
// Other metadata (if any)
493+
}
494+
// Annotations (if any)
495+
// Other subsegments (if any)
496+
}
497+
]
498+
}
499+
]
500+
},
501+
// Overhead subsegment (if any)
502+
]
503+
}
504+
}
505+
]
506+
}
507+
508+
```
509+
</details>
510+
511+
### Capturing AWS SDK clients
409512

410513
**AWS SDK JS v3**
411514
```typescript
@@ -639,7 +742,7 @@ new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked
639742
```
640743
</details>
641744

642-
### Disable capture response body
745+
### Disabling capture response body
643746

644747
```typescript
645748
// Environment variables set for the Lambda
@@ -713,7 +816,7 @@ new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked
713816
```
714817
</details>
715818

716-
### Disable capture error
819+
### Disabling capture error
717820

718821
```typescript
719822
// Environment variables set for the Lambda

packages/tracing/src/Tracer.ts

+30-11
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,19 @@ class Tracer implements ClassThatTraces {
4444
}
4545

4646
public captureLambdaHanlder(): HandlerMethodDecorator {
47-
return (_target, _propertyKey, descriptor) => {
47+
return (target, _propertyKey, descriptor) => {
4848
const originalMethod = descriptor.value;
4949

50-
descriptor.value = (event, context, callback) => {
50+
descriptor.value = (event, context, callback): any => {
5151
if (this.tracingEnabled === false) {
52-
return originalMethod?.apply(this, [ event, context, callback ]);
52+
return originalMethod?.apply(target, [ event, context, callback ]);
5353
}
5454

55-
this.provider.captureAsyncFunc(`## ${context.functionName}`, async subsegment => {
55+
return this.provider.captureAsyncFunc(`## ${context.functionName}`, async subsegment => {
5656
this.annotateColdStart();
5757
let result;
5858
try {
59-
result = await originalMethod?.apply(this, [ event, context, callback ]);
59+
result = await originalMethod?.apply(target, [ event, context, callback ]);
6060
this.addResponseAsMetadata(result, context.functionName);
6161
} catch (error) {
6262
this.addErrorAsMetadata(error as Error);
@@ -69,19 +69,38 @@ class Tracer implements ClassThatTraces {
6969
return result;
7070
});
7171
};
72+
73+
return descriptor;
7274
};
7375
}
7476

75-
// TODO: Finish implementation, type definition is wrong & it doesn't work. Need help.
7677
public captureMethod(): MethodDecorator {
77-
return (_target, _propertyKey, descriptor) => {
78+
return (target, _propertyKey, descriptor) => {
7879
const originalMethod = descriptor.value;
79-
console.debug(originalMethod);
80-
/* descriptor.value = () => {
80+
81+
descriptor.value = (...args: unknown[]) => {
8182
if (this.tracingEnabled === false) {
82-
return originalMethod?.apply(this, [ ]);
83+
return originalMethod?.apply(target, [...args]);
8384
}
84-
}; */
85+
86+
return this.provider.captureAsyncFunc(`### ${originalMethod.name}`, async subsegment => {
87+
let result;
88+
try {
89+
result = await originalMethod?.apply(this, [...args]);
90+
this.addResponseAsMetadata(result, originalMethod.name);
91+
} catch (error) {
92+
this.addErrorAsMetadata(error as Error);
93+
// TODO: should this error be thrown?? If thrown we get a ERR_UNHANDLED_REJECTION. If not aren't we are basically catching a Customer error?
94+
// throw error;
95+
} finally {
96+
subsegment?.close();
97+
}
98+
99+
return result;
100+
});
101+
};
102+
103+
return descriptor;
85104
};
86105
}
87106

packages/tracing/tests/unit/Tracer.test.ts

+91-7
Original file line numberDiff line numberDiff line change
@@ -551,30 +551,114 @@ describe('Class: Tracer', () => {
551551

552552
describe('Method: captureMethod', () => {
553553

554-
test('when called while tracing is disabled, it does nothing', () => {
554+
test('when called while tracing is disabled, it does nothing', async () => {
555555

556556
// Prepare
557557
const tracer: Tracer = new Tracer({ enabled: false });
558+
const captureAsyncFuncSpy = jest.spyOn(tracer.provider, 'captureAsyncFunc');
558559
class Lambda implements LambdaInterface {
559560

560561
@tracer.captureMethod()
561562
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
562563
// @ts-ignore
563-
public dummyMethod(): boolean {
564-
return true;
564+
public async dummyMethod(some: string): Promise<any> {
565+
return new Promise((resolve, _reject) => resolve(some));
565566
}
566567

567-
public handler<TEvent, TResult>(_event: TEvent, _context: Context, _callback: Callback<TResult>): void | Promise<TResult> {
568-
return new Promise((resolve, _reject) => resolve({} as unknown as TResult));
568+
public async handler<TEvent, TResult>(_event: TEvent, _context: Context, _callback: Callback<TResult>): Promise<TResult> {
569+
const result = await this.dummyMethod('foo bar');
570+
return new Promise((resolve, _reject) => resolve(result as unknown as TResult));
571+
}
572+
573+
}
574+
575+
// Act
576+
await new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!'));
577+
578+
// Assess
579+
expect(captureAsyncFuncSpy).toBeCalledTimes(0);
580+
581+
});
582+
583+
test('when used as decorator and with standard config, it captures the response as metadata', async () => {
584+
585+
// Prepare
586+
const tracer: Tracer = new Tracer();
587+
const newSubsegment: Segment | Subsegment | undefined = new Subsegment('### dummyMethod');
588+
jest.spyOn(tracer.provider, 'getSegment')
589+
.mockImplementation(() => newSubsegment);
590+
setContextMissingStrategy(() => null);
591+
const captureAsyncFuncSpy = jest.spyOn(tracer.provider, 'captureAsyncFunc');
592+
class Lambda implements LambdaInterface {
593+
594+
@tracer.captureMethod()
595+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
596+
// @ts-ignore
597+
public async dummyMethod(some: string): Promise<string> {
598+
return new Promise((resolve, _reject) => setTimeout(() => resolve(some), 3000));
599+
}
600+
601+
public async handler<TEvent, TResult>(_event: TEvent, _context: Context, _callback: Callback<TResult>): Promise<TResult> {
602+
const result = await this.dummyMethod('foo bar');
603+
return new Promise((resolve, _reject) => resolve(result as unknown as TResult));
569604
}
570605

571606
}
572607

573608
// Act
574-
new Lambda().dummyMethod();
609+
await new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!'));
575610

576611
// Assess
577-
expect(console.debug).toBeCalledTimes(1);
612+
expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1);
613+
expect(captureAsyncFuncSpy).toHaveBeenCalledWith('### dummyMethod', expect.anything());
614+
expect(newSubsegment).toEqual(expect.objectContaining({
615+
name: '### dummyMethod',
616+
metadata: {
617+
'hello-world': {
618+
'dummyMethod response': 'foo bar',
619+
},
620+
}
621+
}));
622+
623+
});
624+
625+
test('when used as decorator and with standard config, it captures the exception correctly', async () => {
626+
627+
// Prepare
628+
const tracer: Tracer = new Tracer();
629+
const newSubsegment: Segment | Subsegment | undefined = new Subsegment('### dummyMethod');
630+
jest.spyOn(tracer.provider, 'getSegment')
631+
.mockImplementation(() => newSubsegment);
632+
setContextMissingStrategy(() => null);
633+
const captureAsyncFuncSpy = jest.spyOn(tracer.provider, 'captureAsyncFunc');
634+
const addErrorSpy = jest.spyOn(newSubsegment, 'addError');
635+
class Lambda implements LambdaInterface {
636+
637+
@tracer.captureMethod()
638+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
639+
// @ts-ignore
640+
public async dummyMethod(_some: string): Promise<string> {
641+
throw new Error('Exception thrown!');
642+
}
643+
644+
public async handler<TEvent, TResult>(_event: TEvent, _context: Context, _callback: Callback<TResult>): Promise<TResult> {
645+
const result = await this.dummyMethod('foo bar');
646+
return new Promise((resolve, _reject) => resolve(result as unknown as TResult));
647+
}
648+
649+
}
650+
651+
// Act
652+
await new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!'));
653+
654+
// Assess
655+
expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1);
656+
expect(newSubsegment).toEqual(expect.objectContaining({
657+
name: '### dummyMethod',
658+
}));
659+
expect('cause' in newSubsegment).toBe(true);
660+
expect(addErrorSpy).toHaveBeenCalledTimes(1);
661+
expect(addErrorSpy).toHaveBeenCalledWith(new Error('Exception thrown!'), false);
578662

579663
});
580664

packages/tracing/types/Tracer.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ type TracerOptions = {
2121
customConfigService?: ConfigServiceInterface
2222
};
2323

24+
// TODO: Revisit type below, it doesn't allow to define async handlers.
2425
type HandlerMethodDecorator = (target: LambdaInterface, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<Handler>) => TypedPropertyDescriptor<Handler> | void;
2526

26-
// TODO: Revisit these types that don't work.
27-
type MethodDecorator = <T>(target: LambdaInterface, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
27+
type MethodDecorator = (target: any, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<any>) => any;
2828

2929
export {
3030
ClassThatTraces,

0 commit comments

Comments
 (0)