Skip to content

Commit a3620c9

Browse files
committed
Add AWSResources integration
This integration traces AWS service calls as spans.
1 parent a282c01 commit a3620c9

File tree

6 files changed

+251
-3
lines changed

6 files changed

+251
-3
lines changed

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: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { getCurrentHub } from '@sentry/node';
2+
import { Integration, Span, Transaction } from '@sentry/types';
3+
import { fill } from '@sentry/utils';
4+
import { AWSError } from 'aws-sdk/lib/error';
5+
import { Request } from 'aws-sdk/lib/request';
6+
7+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
8+
type GenericParams = { [key: string]: any };
9+
type MakeRequestCallback<TResult> = (err: AWSError, data: TResult) => void;
10+
interface MakeRequestFunction<TParams, TResult> extends CallableFunction {
11+
(operation: string, params?: TParams, callback?: MakeRequestCallback<TResult>): Request<TResult, AWSError>;
12+
}
13+
14+
/** AWS resource requests tracking */
15+
export class AWSResources implements Integration {
16+
/**
17+
* @inheritDoc
18+
*/
19+
public static id: string = 'AWSResources';
20+
21+
/**
22+
* @inheritDoc
23+
*/
24+
public name: string = AWSResources.id;
25+
26+
/**
27+
* @inheritDoc
28+
*/
29+
public setupOnce(): void {
30+
trackService(require('aws-sdk/clients/lambda'), describeLambdaOperation);
31+
trackService(require('aws-sdk/clients/s3'), describeS3Operation);
32+
}
33+
}
34+
35+
/**
36+
* Function that wraps `makeRequest` method of an AWS service class.
37+
*
38+
* @param service Class of AWS service.
39+
* @param describe Function that returns a description string of for particular service operation.
40+
*/
41+
function trackService<TService extends ObjectConstructor>(
42+
service: TService,
43+
describe: (operation: string, params?: GenericParams) => string,
44+
): void {
45+
fill(
46+
service.prototype,
47+
'makeRequest',
48+
<T, TResult>(orig: MakeRequestFunction<GenericParams, TResult>): MakeRequestFunction<GenericParams, TResult> =>
49+
function(this: T, operation: string, params?: GenericParams, callback?: MakeRequestCallback<TResult>) {
50+
let transaction: Transaction | undefined;
51+
let span: Span | undefined;
52+
const scope = getCurrentHub().getScope();
53+
if (scope) {
54+
transaction = scope.getTransaction();
55+
}
56+
const req = orig.call(this, operation, params);
57+
req.on('afterBuild', () => {
58+
if (transaction) {
59+
span = transaction.startChild({
60+
description: describe(operation, params),
61+
op: 'request',
62+
});
63+
}
64+
});
65+
req.on('complete', () => {
66+
if (span) {
67+
span.finish();
68+
}
69+
});
70+
71+
if (callback) {
72+
req.send(callback);
73+
}
74+
return req;
75+
},
76+
);
77+
}
78+
79+
/** Describes an operation on AWS Lambda service */
80+
function describeLambdaOperation(operation: string, params?: GenericParams): string {
81+
let ret = `aws.lambda.${operation}`;
82+
if (params && 'FunctionName' in params) {
83+
ret += ` ${params.FunctionName}`;
84+
}
85+
return ret;
86+
}
87+
88+
/** Describes an operation on AWS S3 service */
89+
function describeS3Operation(operation: string, params?: GenericParams): string {
90+
let ret = `aws.s3.${operation}`;
91+
if (params && 'Bucket' in params) {
92+
ret += ` ${params.Bucket}`;
93+
}
94+
return ret;
95+
}

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();
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({ 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
@@ -4047,6 +4047,21 @@ available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2:
40474047
dependencies:
40484048
array-filter "^1.0.0"
40494049

4050+
aws-sdk@^2.765.0:
4051+
version "2.765.0"
4052+
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.765.0.tgz#34e71dc7336b4593d2f521cdcbbf7542a0d1f8cb"
4053+
integrity sha512-FQdPKJ5LAhNxkpqwrjQ+hiEqEOezV/PfZBn5RcBG6vu8K3VuT4dE12mGTY3qkdHy7lhymeRS5rWcagTmabKFtA==
4054+
dependencies:
4055+
buffer "4.9.2"
4056+
events "1.1.1"
4057+
ieee754 "1.1.13"
4058+
jmespath "0.15.0"
4059+
querystring "0.2.0"
4060+
sax "1.2.1"
4061+
url "0.10.3"
4062+
uuid "3.3.2"
4063+
xml2js "0.4.19"
4064+
40504065
aws-sign2@~0.7.0:
40514066
version "0.7.0"
40524067
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
@@ -5688,7 +5703,7 @@ buffer-xor@^1.0.3:
56885703
resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
56895704
integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=
56905705

5691-
buffer@^4.3.0:
5706+
buffer@4.9.2, buffer@^4.3.0:
56925707
version "4.9.2"
56935708
resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8"
56945709
integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==
@@ -8830,6 +8845,11 @@ events-to-array@^1.0.1:
88308845
resolved "https://registry.yarnpkg.com/events-to-array/-/events-to-array-1.1.2.tgz#2d41f563e1fe400ed4962fe1a4d5c6a7539df7f6"
88318846
integrity sha1-LUH1Y+H+QA7Uli/hpNXGp1Od9/Y=
88328847

8848+
8849+
version "1.1.1"
8850+
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
8851+
integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=
8852+
88338853
events@^3.0.0:
88348854
version "3.2.0"
88358855
resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379"
@@ -10600,7 +10620,7 @@ icss-utils@^4.0.0, icss-utils@^4.1.1:
1060010620
dependencies:
1060110621
postcss "^7.0.14"
1060210622

10603-
ieee754@^1.1.4:
10623+
ieee754@1.1.13, ieee754@^1.1.4:
1060410624
version "1.1.13"
1060510625
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
1060610626
integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
@@ -11769,6 +11789,11 @@ jest@^24.7.1:
1176911789
import-local "^2.0.0"
1177011790
jest-cli "^24.9.0"
1177111791

11792+
11793+
version "0.15.0"
11794+
resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217"
11795+
integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=
11796+
1177211797
jquery@^3.5.0:
1177311798
version "3.5.1"
1177411799
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.1.tgz#d7b4d08e1bfdb86ad2f1a3d039ea17304717abb5"
@@ -13565,6 +13590,16 @@ no-case@^3.0.3:
1356513590
lower-case "^2.0.1"
1356613591
tslib "^1.10.0"
1356713592

13593+
nock@^13.0.4:
13594+
version "13.0.4"
13595+
resolved "https://registry.yarnpkg.com/nock/-/nock-13.0.4.tgz#9fb74db35d0aa056322e3c45be14b99105cd7510"
13596+
integrity sha512-alqTV8Qt7TUbc74x1pKRLSENzfjp4nywovcJgi/1aXDiUxXdt7TkruSTF5MDWPP7UoPVgea4F9ghVdmX0xxnSA==
13597+
dependencies:
13598+
debug "^4.1.0"
13599+
json-stringify-safe "^5.0.1"
13600+
lodash.set "^4.3.2"
13601+
propagate "^2.0.0"
13602+
1356813603
1356913604
version "1.0.5"
1357013605
resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.5.tgz#fa930275f5bf5dae188d6192b24b4c8bbac3d76a"
@@ -15139,6 +15174,11 @@ prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.6.2, prop-types@^15.7.2:
1513915174
object-assign "^4.1.1"
1514015175
react-is "^16.8.1"
1514115176

15177+
propagate@^2.0.0:
15178+
version "2.0.1"
15179+
resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45"
15180+
integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==
15181+
1514215182
proto-list@~1.2.1:
1514315183
version "1.2.4"
1514415184
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
@@ -16295,7 +16335,12 @@ sane@^4.0.0, sane@^4.0.3, sane@^4.1.0:
1629516335
minimist "^1.1.1"
1629616336
walker "~1.0.5"
1629716337

16298-
sax@^1.2.4, sax@~1.2.4:
16338+
16339+
version "1.2.1"
16340+
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
16341+
integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o=
16342+
16343+
sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4:
1629916344
version "1.2.4"
1630016345
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
1630116346
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
@@ -18242,6 +18287,14 @@ url-to-options@^1.0.1:
1824218287
resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9"
1824318288
integrity sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=
1824418289

18290+
18291+
version "0.10.3"
18292+
resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64"
18293+
integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=
18294+
dependencies:
18295+
punycode "1.3.2"
18296+
querystring "0.2.0"
18297+
1824518298
url@^0.11.0:
1824618299
version "0.11.0"
1824718300
resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
@@ -18326,6 +18379,11 @@ [email protected]:
1832618379
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
1832718380
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
1832818381

18382+
18383+
version "3.3.2"
18384+
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
18385+
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
18386+
1832918387
uuid@^3.0.1, uuid@^3.3.2:
1833018388
version "3.4.0"
1833118389
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
@@ -18911,6 +18969,19 @@ xml-name-validator@^3.0.0:
1891118969
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
1891218970
integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
1891318971

18972+
18973+
version "0.4.19"
18974+
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
18975+
integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==
18976+
dependencies:
18977+
sax ">=0.6.0"
18978+
xmlbuilder "~9.0.1"
18979+
18980+
xmlbuilder@~9.0.1:
18981+
version "9.0.7"
18982+
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
18983+
integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=
18984+
1891418985
xmlchars@^2.1.1, xmlchars@^2.2.0:
1891518986
version "2.2.0"
1891618987
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"

0 commit comments

Comments
 (0)