Skip to content

Commit af71624

Browse files
marshall-leekamilogorek
authored andcommitted
Add AWSResources integration
This integration traces AWS service calls as spans.
1 parent b4a29be commit af71624

File tree

8 files changed

+293
-5
lines changed

8 files changed

+293
-5
lines changed

packages/serverless/README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ Currently supported environment:
2121

2222
*AWS Lambda*
2323

24-
To use this SDK, call `Sentry.init(options)` at the very beginning of your JavaScript file.
24+
To use this SDK, call `Sentry.AWSLambda.init(options)` at the very beginning of your JavaScript file.
2525

2626
```javascript
2727
import * as Sentry from '@sentry/serverless';
2828

29-
Sentry.init({
29+
Sentry.AWSLambda.init({
3030
dsn: '__DSN__',
3131
// ...
3232
});
@@ -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 performance of all the incoming requests and also outgoing AWS service requests, just set the `tracesSampleRate` option.
46+
47+
```javascript
48+
import * as Sentry from '@sentry/serverless';
49+
50+
Sentry.AWSLambda.init({
51+
dsn: '__DSN__',
52+
tracesSampleRate: 1.0,
53+
});
54+
```

packages/serverless/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@
2727
"@sentry-internal/eslint-config-sdk": "5.25.0",
2828
"@types/aws-lambda": "^8.10.62",
2929
"@types/node": "^14.6.4",
30+
"aws-sdk": "^2.765.0",
3031
"eslint": "7.6.0",
3132
"jest": "^24.7.1",
33+
"nock": "^13.0.4",
3234
"npm-run-all": "^4.1.2",
3335
"prettier": "1.19.0",
3436
"rimraf": "^2.6.3",

packages/serverless/src/awslambda.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
startTransaction,
1010
withScope,
1111
} from '@sentry/node';
12+
import * as Sentry from '@sentry/node';
13+
import { Integration } from '@sentry/types';
1214
import { addExceptionMechanism } from '@sentry/utils';
1315
// NOTE: I have no idea how to fix this right now, and don't want to waste more time, as it builds just fine — Kamil
1416
// eslint-disable-next-line import/no-unresolved
@@ -17,6 +19,10 @@ import { hostname } from 'os';
1719
import { performance } from 'perf_hooks';
1820
import { types } from 'util';
1921

22+
import { AWSServices } from './awsservices';
23+
24+
export * from '@sentry/node';
25+
2026
const { isPromise } = types;
2127

2228
// https://www.npmjs.com/package/aws-lambda-consumer
@@ -39,6 +45,18 @@ export interface WrapperOptions {
3945
timeoutWarningLimit: number;
4046
}
4147

48+
export const defaultIntegrations: Integration[] = [...Sentry.defaultIntegrations, new AWSServices()];
49+
50+
/**
51+
* @see {@link Sentry.init}
52+
*/
53+
export function init(options: Sentry.NodeOptions = {}): void {
54+
if (options.defaultIntegrations === undefined) {
55+
options.defaultIntegrations = defaultIntegrations;
56+
}
57+
return Sentry.init(options);
58+
}
59+
4260
/**
4361
* Add event processor that will override SDK details to point to the serverless SDK instead of Node,
4462
* as well as set correct mechanism type, which should be set to `handled: false`.
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { getCurrentHub } from '@sentry/node';
2+
import { Integration, Span, Transaction } from '@sentry/types';
3+
import { fill } from '@sentry/utils';
4+
// 'aws-sdk/global' import is expected to be type-only so it's erased in the final .js file.
5+
// When TypeScript compiler is upgraded, use `import type` syntax to explicitly assert that we don't want to load a module here.
6+
import * as AWS from 'aws-sdk/global';
7+
8+
type GenericParams = { [key: string]: any }; // eslint-disable-line @typescript-eslint/no-explicit-any
9+
type MakeRequestCallback<TResult> = (err: AWS.AWSError, data: TResult) => void;
10+
// This interace could be replaced with just type alias once the `strictBindCallApply` mode is enabled.
11+
interface MakeRequestFunction<TParams, TResult> extends CallableFunction {
12+
(operation: string, params?: TParams, callback?: MakeRequestCallback<TResult>): AWS.Request<TResult, AWS.AWSError>;
13+
}
14+
interface AWSService {
15+
serviceIdentifier: string;
16+
}
17+
18+
/** AWS service requests tracking */
19+
export class AWSServices implements Integration {
20+
/**
21+
* @inheritDoc
22+
*/
23+
public static id: string = 'AWSServices';
24+
25+
/**
26+
* @inheritDoc
27+
*/
28+
public name: string = AWSServices.id;
29+
30+
/**
31+
* @inheritDoc
32+
*/
33+
public setupOnce(): void {
34+
const awsModule = require('aws-sdk/global') as typeof AWS;
35+
fill(
36+
awsModule.Service.prototype,
37+
'makeRequest',
38+
<TService extends AWSService, TResult>(
39+
orig: MakeRequestFunction<GenericParams, TResult>,
40+
): MakeRequestFunction<GenericParams, TResult> =>
41+
function(this: TService, operation: string, params?: GenericParams, callback?: MakeRequestCallback<TResult>) {
42+
let transaction: Transaction | undefined;
43+
let span: Span | undefined;
44+
const scope = getCurrentHub().getScope();
45+
if (scope) {
46+
transaction = scope.getTransaction();
47+
}
48+
const req = orig.call(this, operation, params);
49+
req.on('afterBuild', () => {
50+
if (transaction) {
51+
span = transaction.startChild({
52+
description: describe(this, operation, params),
53+
op: 'request',
54+
});
55+
}
56+
});
57+
req.on('complete', () => {
58+
if (span) {
59+
span.finish();
60+
}
61+
});
62+
63+
if (callback) {
64+
req.send(callback);
65+
}
66+
return req;
67+
},
68+
);
69+
}
70+
}
71+
72+
/** Describes an operation on generic AWS service */
73+
function describe<TService extends AWSService>(service: TService, operation: string, params?: GenericParams): string {
74+
let ret = `aws.${service.serviceIdentifier}.${operation}`;
75+
if (params === undefined) {
76+
return ret;
77+
}
78+
switch (service.serviceIdentifier) {
79+
case 's3':
80+
ret += describeS3Operation(operation, params);
81+
break;
82+
case 'lambda':
83+
ret += describeLambdaOperation(operation, params);
84+
break;
85+
}
86+
return ret;
87+
}
88+
89+
/** Describes an operation on AWS Lambda service */
90+
function describeLambdaOperation(_operation: string, params: GenericParams): string {
91+
let ret = '';
92+
if ('FunctionName' in params) {
93+
ret += ` ${params.FunctionName}`;
94+
}
95+
return ret;
96+
}
97+
98+
/** Describes an operation on AWS S3 service */
99+
function describeS3Operation(_operation: string, params: GenericParams): string {
100+
let ret = '';
101+
if ('Bucket' in params) {
102+
ret += ` ${params.Bucket}`;
103+
}
104+
return ret;
105+
}

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 './awsservices';
67
export * from '@sentry/node';

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,31 @@
11
const origSentry = jest.requireActual('@sentry/node');
2+
export const defaultIntegrations = origSentry.defaultIntegrations; // eslint-disable-line @typescript-eslint/no-unsafe-member-access
23
export const Handlers = origSentry.Handlers; // eslint-disable-line @typescript-eslint/no-unsafe-member-access
34
export const SDK_VERSION = '6.6.6';
45
export const Severity = {
56
Warning: 'warning',
67
};
78
export const fakeParentScope = {
89
setSpan: jest.fn(),
10+
getTransaction: jest.fn(() => fakeTransaction),
911
};
1012
export const fakeHub = {
1113
configureScope: jest.fn((fn: (arg: any) => any) => fn(fakeParentScope)),
14+
getScope: jest.fn(() => fakeParentScope),
1215
};
1316
export const fakeScope = {
1417
addEventProcessor: jest.fn(),
1518
setTransactionName: jest.fn(),
1619
setTag: jest.fn(),
1720
setContext: jest.fn(),
1821
};
22+
export const fakeSpan = {
23+
finish: jest.fn(),
24+
};
1925
export const fakeTransaction = {
2026
finish: jest.fn(),
2127
setHttpStatus: jest.fn(),
28+
startChild: jest.fn(() => fakeSpan),
2229
};
2330
export const getCurrentHub = jest.fn(() => fakeHub);
2431
export const startTransaction = jest.fn(_ => fakeTransaction);
@@ -30,8 +37,12 @@ export const flush = jest.fn(() => Promise.resolve());
3037
export const resetMocks = (): void => {
3138
fakeTransaction.setHttpStatus.mockClear();
3239
fakeTransaction.finish.mockClear();
40+
fakeTransaction.startChild.mockClear();
41+
fakeSpan.finish.mockClear();
3342
fakeParentScope.setSpan.mockClear();
43+
fakeParentScope.getTransaction.mockClear();
3444
fakeHub.configureScope.mockClear();
45+
fakeHub.getScope.mockClear();
3546

3647
fakeScope.addEventProcessor.mockClear();
3748
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 { AWSServices } from '../src/awsservices';
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('AWSServices', () => {
15+
beforeAll(() => {
16+
new AWSServices().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+
});

0 commit comments

Comments
 (0)