Skip to content

Commit 19b6573

Browse files
committed
Add AWSResources integration
This integration traces AWS service calls as spans.
1 parent 19b1040 commit 19b6573

File tree

7 files changed

+270
-3
lines changed

7 files changed

+270
-3
lines changed

packages/serverless/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,14 @@ exports.handler = Sentry.AWSLambda.wrapHandler((event, context, callback) => {
4141
throw new Error('oh, hello there!');
4242
});
4343
```
44+
45+
If you also want to trace AWS service calls, add an `AWSResources` integration. They will be available to observe as nested spans for each transaction.
46+
47+
```javascript
48+
Sentry.init({
49+
dsn: '__DSN__',
50+
integrations: [
51+
new Sentry.AWSResources(),
52+
],
53+
});
54+
```

packages/serverless/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@
2828
"@types/aws-lambda": "^8.10.62",
2929
"@types/express": "^4.17.2",
3030
"@types/node": "^14.6.4",
31+
"aws-sdk": "^2.765.0",
3132
"eslint": "7.6.0",
3233
"jest": "^24.7.1",
34+
"nock": "^13.0.4",
3335
"npm-run-all": "^4.1.2",
3436
"prettier": "1.19.0",
3537
"rimraf": "^2.6.3",
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { getCurrentHub } from '@sentry/node';
2+
import { Integration, Span, Transaction } from '@sentry/types';
3+
import { fill } from '@sentry/utils';
4+
import * as AWS from 'aws-sdk/global';
5+
6+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7+
type GenericParams = { [key: string]: any };
8+
type MakeRequestCallback<TResult> = (err: AWS.AWSError, data: TResult) => void;
9+
interface MakeRequestFunction<TParams, TResult> extends CallableFunction {
10+
(operation: string, params?: TParams, callback?: MakeRequestCallback<TResult>): AWS.Request<TResult, AWS.AWSError>;
11+
}
12+
interface AWSService {
13+
serviceIdentifier: string;
14+
}
15+
16+
/** AWS resource requests tracking */
17+
export class AWSResources implements Integration {
18+
/**
19+
* @inheritDoc
20+
*/
21+
public static id: string = 'AWSResources';
22+
23+
/**
24+
* @inheritDoc
25+
*/
26+
public name: string = AWSResources.id;
27+
28+
/**
29+
* @inheritDoc
30+
*/
31+
public setupOnce(): void {
32+
const awsModule = require('aws-sdk/global') as typeof AWS;
33+
fill(
34+
awsModule.Service.prototype,
35+
'makeRequest',
36+
<TService extends AWSService, TResult>(
37+
orig: MakeRequestFunction<GenericParams, TResult>,
38+
): MakeRequestFunction<GenericParams, TResult> =>
39+
function(this: TService, operation: string, params?: GenericParams, callback?: MakeRequestCallback<TResult>) {
40+
let transaction: Transaction | undefined;
41+
let span: Span | undefined;
42+
const scope = getCurrentHub().getScope();
43+
if (scope) {
44+
transaction = scope.getTransaction();
45+
}
46+
const req = orig.call(this, operation, params);
47+
req.on('afterBuild', () => {
48+
if (transaction) {
49+
span = transaction.startChild({
50+
description: describe(this, operation, params),
51+
op: 'request',
52+
});
53+
}
54+
});
55+
req.on('complete', () => {
56+
if (span) {
57+
span.finish();
58+
}
59+
});
60+
61+
if (callback) {
62+
req.send(callback);
63+
}
64+
return req;
65+
},
66+
);
67+
}
68+
}
69+
70+
/** Describes an operation on generic AWS service */
71+
function describe<TService extends AWSService>(service: TService, operation: string, params?: GenericParams): string {
72+
let ret = `aws.${service.serviceIdentifier}.${operation}`;
73+
if (params === undefined) {
74+
return ret;
75+
}
76+
switch (service.serviceIdentifier) {
77+
case 's3':
78+
ret += describeS3Operation(operation, params);
79+
break;
80+
case 'lambda':
81+
ret += describeLambdaOperation(operation, params);
82+
break;
83+
}
84+
return ret;
85+
}
86+
87+
/** Describes an operation on AWS Lambda service */
88+
function describeLambdaOperation(_operation: string, params: GenericParams): string {
89+
let ret = '';
90+
if ('FunctionName' in params) {
91+
ret += ` ${params.FunctionName}`;
92+
}
93+
return ret;
94+
}
95+
96+
/** Describes an operation on AWS S3 service */
97+
function describeS3Operation(_operation: string, params: GenericParams): string {
98+
let ret = '';
99+
if ('Bucket' in params) {
100+
ret += ` ${params.Bucket}`;
101+
}
102+
return ret;
103+
}

packages/serverless/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ import * as AWSLambda from './awslambda';
33
import * as GCPFunction from './gcpfunction';
44
export { AWSLambda, GCPFunction };
55

6+
export * from './awsresources';
67
export * from '@sentry/node';

packages/serverless/test/__mocks__/@sentry/node.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,25 @@ export const Severity = {
44
};
55
export const fakeParentScope = {
66
setSpan: jest.fn(),
7+
getTransaction: jest.fn(() => fakeTransaction),
78
};
89
export const fakeHub = {
910
configureScope: jest.fn((fn: (arg: any) => any) => fn(fakeParentScope)),
11+
getScope: jest.fn(() => fakeParentScope),
1012
};
1113
export const fakeScope = {
1214
addEventProcessor: jest.fn(),
1315
setTransactionName: jest.fn(),
1416
setTag: jest.fn(),
1517
setContext: jest.fn(),
1618
};
19+
export const fakeSpan = {
20+
finish: jest.fn(),
21+
};
1722
export const fakeTransaction = {
1823
finish: jest.fn(),
1924
setHttpStatus: jest.fn(),
25+
startChild: jest.fn(() => fakeSpan),
2026
};
2127
export const getCurrentHub = jest.fn(() => fakeHub);
2228
export const startTransaction = jest.fn(_ => fakeTransaction);
@@ -28,8 +34,12 @@ export const flush = jest.fn(() => Promise.resolve());
2834
export const resetMocks = (): void => {
2935
fakeTransaction.setHttpStatus.mockClear();
3036
fakeTransaction.finish.mockClear();
37+
fakeTransaction.startChild.mockClear();
38+
fakeSpan.finish.mockClear();
3139
fakeParentScope.setSpan.mockClear();
40+
fakeParentScope.getTransaction.mockClear();
3241
fakeHub.configureScope.mockClear();
42+
fakeHub.getScope.mockClear();
3343

3444
fakeScope.addEventProcessor.mockClear();
3545
fakeScope.setTransactionName.mockClear();
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import * as AWS from 'aws-sdk';
2+
import * as nock from 'nock';
3+
4+
import * as Sentry from '../src';
5+
import { AWSResources } from '../src/awsresources';
6+
7+
/**
8+
* Why @ts-ignore some Sentry.X calls
9+
*
10+
* A hack-ish way to contain everything related to mocks in the same __mocks__ file.
11+
* Thanks to this, we don't have to do more magic than necessary. Just add and export desired method and assert on it.
12+
*/
13+
14+
describe('AWSResources', () => {
15+
beforeAll(() => {
16+
new AWSResources().setupOnce();
17+
});
18+
afterEach(() => {
19+
// @ts-ignore see "Why @ts-ignore" note
20+
Sentry.resetMocks();
21+
});
22+
afterAll(() => {
23+
nock.restore();
24+
});
25+
26+
describe('S3', () => {
27+
const s3 = new AWS.S3({ accessKeyId: '-', secretAccessKey: '-' });
28+
29+
test('getObject', async () => {
30+
nock('https://foo.s3.amazonaws.com')
31+
.get('/bar')
32+
.reply(200, 'contents');
33+
const data = await s3.getObject({ Bucket: 'foo', Key: 'bar' }).promise();
34+
expect(data.Body?.toString('utf-8')).toEqual('contents');
35+
// @ts-ignore see "Why @ts-ignore" note
36+
expect(Sentry.fakeTransaction.startChild).toBeCalledWith({ op: 'request', description: 'aws.s3.getObject foo' });
37+
// @ts-ignore see "Why @ts-ignore" note
38+
expect(Sentry.fakeSpan.finish).toBeCalled();
39+
});
40+
41+
test('getObject with callback', done => {
42+
expect.assertions(3);
43+
nock('https://foo.s3.amazonaws.com')
44+
.get('/bar')
45+
.reply(200, 'contents');
46+
s3.getObject({ Bucket: 'foo', Key: 'bar' }, (err, data) => {
47+
expect(err).toBeNull();
48+
expect(data.Body?.toString('utf-8')).toEqual('contents');
49+
done();
50+
});
51+
// @ts-ignore see "Why @ts-ignore" note
52+
expect(Sentry.fakeTransaction.startChild).toBeCalledWith({ op: 'request', description: 'aws.s3.getObject foo' });
53+
});
54+
});
55+
56+
describe('Lambda', () => {
57+
const lambda = new AWS.Lambda({ accessKeyId: '-', secretAccessKey: '-', region: 'eu-north-1' });
58+
59+
test('invoke', async () => {
60+
nock('https://lambda.eu-north-1.amazonaws.com')
61+
.post('/2015-03-31/functions/foo/invocations')
62+
.reply(201, 'reply');
63+
const data = await lambda.invoke({ FunctionName: 'foo' }).promise();
64+
expect(data.Payload?.toString('utf-8')).toEqual('reply');
65+
// @ts-ignore see "Why @ts-ignore" note
66+
expect(Sentry.fakeTransaction.startChild).toBeCalledWith({ op: 'request', description: 'aws.lambda.invoke foo' });
67+
});
68+
});
69+
});

yarn.lock

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4226,6 +4226,21 @@ available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2:
42264226
dependencies:
42274227
array-filter "^1.0.0"
42284228

4229+
aws-sdk@^2.765.0:
4230+
version "2.765.0"
4231+
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.765.0.tgz#34e71dc7336b4593d2f521cdcbbf7542a0d1f8cb"
4232+
integrity sha512-FQdPKJ5LAhNxkpqwrjQ+hiEqEOezV/PfZBn5RcBG6vu8K3VuT4dE12mGTY3qkdHy7lhymeRS5rWcagTmabKFtA==
4233+
dependencies:
4234+
buffer "4.9.2"
4235+
events "1.1.1"
4236+
ieee754 "1.1.13"
4237+
jmespath "0.15.0"
4238+
querystring "0.2.0"
4239+
sax "1.2.1"
4240+
url "0.10.3"
4241+
uuid "3.3.2"
4242+
xml2js "0.4.19"
4243+
42294244
aws-sign2@~0.7.0:
42304245
version "0.7.0"
42314246
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
@@ -5887,7 +5902,7 @@ buffer-xor@^1.0.3:
58875902
resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
58885903
integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=
58895904

5890-
buffer@^4.3.0:
5905+
buffer@4.9.2, buffer@^4.3.0:
58915906
version "4.9.2"
58925907
resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8"
58935908
integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==
@@ -9029,6 +9044,11 @@ events-to-array@^1.0.1:
90299044
resolved "https://registry.yarnpkg.com/events-to-array/-/events-to-array-1.1.2.tgz#2d41f563e1fe400ed4962fe1a4d5c6a7539df7f6"
90309045
integrity sha1-LUH1Y+H+QA7Uli/hpNXGp1Od9/Y=
90319046

9047+
9048+
version "1.1.1"
9049+
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
9050+
integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=
9051+
90329052
events@^3.0.0:
90339053
version "3.2.0"
90349054
resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379"
@@ -10799,7 +10819,7 @@ icss-utils@^4.0.0, icss-utils@^4.1.1:
1079910819
dependencies:
1080010820
postcss "^7.0.14"
1080110821

10802-
ieee754@^1.1.4:
10822+
ieee754@1.1.13, ieee754@^1.1.4:
1080310823
version "1.1.13"
1080410824
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
1080510825
integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
@@ -11968,6 +11988,11 @@ jest@^24.7.1:
1196811988
import-local "^2.0.0"
1196911989
jest-cli "^24.9.0"
1197011990

11991+
11992+
version "0.15.0"
11993+
resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217"
11994+
integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=
11995+
1197111996
jquery@^3.5.0:
1197211997
version "3.5.1"
1197311998
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.1.tgz#d7b4d08e1bfdb86ad2f1a3d039ea17304717abb5"
@@ -13764,6 +13789,16 @@ no-case@^3.0.3:
1376413789
lower-case "^2.0.1"
1376513790
tslib "^1.10.0"
1376613791

13792+
nock@^13.0.4:
13793+
version "13.0.4"
13794+
resolved "https://registry.yarnpkg.com/nock/-/nock-13.0.4.tgz#9fb74db35d0aa056322e3c45be14b99105cd7510"
13795+
integrity sha512-alqTV8Qt7TUbc74x1pKRLSENzfjp4nywovcJgi/1aXDiUxXdt7TkruSTF5MDWPP7UoPVgea4F9ghVdmX0xxnSA==
13796+
dependencies:
13797+
debug "^4.1.0"
13798+
json-stringify-safe "^5.0.1"
13799+
lodash.set "^4.3.2"
13800+
propagate "^2.0.0"
13801+
1376713802
1376813803
version "1.0.5"
1376913804
resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.5.tgz#fa930275f5bf5dae188d6192b24b4c8bbac3d76a"
@@ -15338,6 +15373,11 @@ prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.6.2, prop-types@^15.7.2:
1533815373
object-assign "^4.1.1"
1533915374
react-is "^16.8.1"
1534015375

15376+
propagate@^2.0.0:
15377+
version "2.0.1"
15378+
resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45"
15379+
integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==
15380+
1534115381
proto-list@~1.2.1:
1534215382
version "1.2.4"
1534315383
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
@@ -16494,7 +16534,12 @@ sane@^4.0.0, sane@^4.0.3, sane@^4.1.0:
1649416534
minimist "^1.1.1"
1649516535
walker "~1.0.5"
1649616536

16497-
sax@^1.2.4, sax@~1.2.4:
16537+
16538+
version "1.2.1"
16539+
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
16540+
integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o=
16541+
16542+
sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4:
1649816543
version "1.2.4"
1649916544
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
1650016545
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
@@ -18441,6 +18486,14 @@ url-to-options@^1.0.1:
1844118486
resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9"
1844218487
integrity sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=
1844318488

18489+
18490+
version "0.10.3"
18491+
resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64"
18492+
integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=
18493+
dependencies:
18494+
punycode "1.3.2"
18495+
querystring "0.2.0"
18496+
1844418497
url@^0.11.0:
1844518498
version "0.11.0"
1844618499
resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
@@ -18525,6 +18578,11 @@ [email protected]:
1852518578
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
1852618579
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
1852718580

18581+
18582+
version "3.3.2"
18583+
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
18584+
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
18585+
1852818586
uuid@^3.0.1, uuid@^3.3.2:
1852918587
version "3.4.0"
1853018588
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
@@ -19120,6 +19178,19 @@ xml-name-validator@^3.0.0:
1912019178
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
1912119179
integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
1912219180

19181+
19182+
version "0.4.19"
19183+
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
19184+
integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==
19185+
dependencies:
19186+
sax ">=0.6.0"
19187+
xmlbuilder "~9.0.1"
19188+
19189+
xmlbuilder@~9.0.1:
19190+
version "9.0.7"
19191+
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
19192+
integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=
19193+
1912319194
xmlchars@^2.1.1, xmlchars@^2.2.0:
1912419195
version "2.2.0"
1912519196
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"

0 commit comments

Comments
 (0)