Skip to content

Commit fcdff3e

Browse files
authored
feat: Replace Epsagon with Lumigo (#1168)
Epsagon is no longer operating. They took down their website on 3 October and backend services have been gradually removed, and since 10 October any service still making requests to Espagon servers faces delays and Lambda timeouts. We have now migrated to [Lumigo]. Their platform supports [auto-tracing], allowing us to begin monitoring without deploying updates to Lambda Wrapper, however we are missing our metrics and labels which correspond to [Execution Tags] in Lumigo. This PR makes the change to wrap handlers with Lumigo's tracer, and updates our logger to use Execution Tags for metrics and labels. To enable Lumigo tracing and tags, set `LUMIGO_TRACER_TOKEN` in your Lambda environment to your [Lumigo token]. Note that if you have enabled auto-tracing, this will be set automatically and tracing should "just work". There are a couple of other little things that need doing (e.g. removing the `raiseOnEpsagon` flag) that I'll cover in separate PRs. Jira: [ENG-2764] [Lumigo]: https://lumigo.io/ [auto-tracing]: https://docs.lumigo.io/docs/serverless-applications#automatic-instrumentation [Execution Tags]: https://docs.lumigo.io/docs/execution-tags [Lumigo token]: https://docs.lumigo.io/docs/lumigo-tokens BREAKING CHANGE: We no longer use Epsagon for monitoring. Lambda Wrapper now supports [Lumigo](https://lumigo.io/) instead.
1 parent 15742f4 commit fcdff3e

File tree

6 files changed

+187
-792
lines changed

6 files changed

+187
-792
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,14 @@ lambdaWrapper.configure<WithSQSServiceConfig & WithOtherServiceConfig>({
204204
});
205205
```
206206

207+
## Monitoring
208+
209+
At Comic Relief we use [Lumigo](https://lumigo.io/) for monitoring and observability of our deployed services. Lambda Wrapper includes the Lumigo tracer to allow us to tag traces with custom labels and metrics ([execution tags](https://docs.lumigo.io/docs/execution-tags)).
210+
211+
Lumigo integration works out-of-the-box with Lumigo's [auto-trace feature](https://docs.lumigo.io/docs/serverless-applications#automatic-instrumentation). If you prefer manual tracing, enable it by setting `LUMIGO_TRACER_TOKEN` in your Lambda environment variables.
212+
213+
And if you don't use Lumigo, don't worry, their tracer will not be instantiated in your functions and no calls will be made to their servers unless `LUMIGO_TRACER_TOKEN` is set.
214+
207215
## Notes
208216

209217
Lambda Wrapper's dependency injection relies on class names being preserved. If your build process includes minifying or uglifying your code, you'll need to disable these transformations.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,12 @@
4747
"aws-sdk": "^2.831.0"
4848
},
4949
"dependencies": {
50+
"@lumigo/tracer": "^1.87.0",
5051
"@sentry/node": "^6.0.1",
5152
"@types/aws-lambda": "^8.10.120",
5253
"alai": "1.0.3",
5354
"async": "^3.2.4",
5455
"axios": "^0.27.2",
55-
"epsagon": "^1.123.3",
5656
"useragent": "2.3.0",
5757
"uuid": "^9.0.1",
5858
"validate.js": "0.13.1",

src/core/LambdaWrapper.ts

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import Epsagon from 'epsagon';
1+
import * as lumigo from '@lumigo/tracer';
22

33
import { Context } from '../index';
44
import ResponseModel from '../models/ResponseModel';
@@ -86,19 +86,45 @@ export default class LambdaWrapper<TConfig extends LambdaWrapperConfig = LambdaW
8686
}
8787
};
8888

89-
// If Epsagon is enabled, wrap the instance in the Epsagon wrapper
90-
if (process.env.EPSAGON_TOKEN && process.env.EPSAGON_SERVICE_NAME) {
91-
Epsagon.init({
92-
token: process.env.EPSAGON_TOKEN,
93-
appName: process.env.EPSAGON_SERVICE_NAME,
94-
});
89+
// If Lumigo is enabled, wrap the handler in the Lumigo wrapper
90+
if (LambdaWrapper.isLumigoEnabled && !LambdaWrapper.isLumigoWrappingUs) {
91+
const tracer = lumigo.initTracer({ token: process.env.LUMIGO_TRACER_TOKEN });
9592

96-
wrapper = Epsagon.lambdaWrapper(wrapper);
93+
// Lumigo's wrapper works with both callbacks or promises handlers, and
94+
// the returned function behaves the same way as the original. For our
95+
// promise-based handler we can safely coerce the type.
96+
wrapper = tracer.trace(wrapper) as (event: any, context: Context) => Promise<any>;
9797
}
9898

9999
return wrapper;
100100
}
101101

102+
/**
103+
* `true` if we will send traces to Lumigo.
104+
*
105+
* The `LUMIGO_TRACER_TOKEN` env var is present in both manually traced and
106+
* auto-traced functions.
107+
*/
108+
static get isLumigoEnabled(): boolean {
109+
return !!process.env.LUMIGO_TRACER_TOKEN;
110+
}
111+
112+
/**
113+
* `true` if the Lambda function is already being traced by a higher-level
114+
* Lumigo wrapper, in which case we don't need to manually wrap our handlers.
115+
*
116+
* There are two ways that this can be done, based on the documentation
117+
* [here](https://docs.lumigo.io/docs/lambda-layers): using a Lambda runtime
118+
* wrapper, or handler redirection. Each method can be detected via its
119+
* environment variables. Auto-trace uses the runtime wrapper.
120+
*/
121+
static get isLumigoWrappingUs(): boolean {
122+
return this.isLumigoEnabled && (
123+
process.env.AWS_LAMBDA_EXEC_WRAPPER === '/opt/lumigo_wrapper'
124+
|| !!process.env.LUMIGO_ORIGINAL_HANDLER
125+
);
126+
}
127+
102128
/**
103129
* Process the result once we have one.
104130
*
@@ -115,11 +141,10 @@ export default class LambdaWrapper<TConfig extends LambdaWrapperConfig = LambdaW
115141
}
116142

117143
/**
118-
* Gracefully handles an error, logging in Epsagon and generating a response
144+
* Gracefully handles an error, logging in Lumigo and generating a response
119145
* reflecting the `code` of the error, if defined.
120146
*
121-
* Note about Epsagon:
122-
* Epsagon generates alerts for logs on level ERROR. This means that
147+
* Lumigo generates alerts for logs on level ERROR. This means that
123148
* `logger.error` will produce an alert. To avoid meaningless notifications,
124149
* most likely coming from tests, we log INFO unless either:
125150
*

src/services/LoggerService.ts

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import * as lumigo from '@lumigo/tracer';
12
import * as Sentry from '@sentry/node';
23
import { AxiosError } from 'axios';
3-
import Epsagon from 'epsagon';
44
import Winston from 'winston';
55

66
import DependencyAwareClass from '../core/DependencyAwareClass';
77
import DependencyInjection from '../core/DependencyInjection';
8+
import LambdaWrapper from '../core/LambdaWrapper';
89

910
const sentryIsAvailable = typeof process.env.RAVEN_DSN !== 'undefined' && typeof process.env.RAVEN_DSN === 'string' && process.env.RAVEN_DSN !== 'undefined';
1011

@@ -170,14 +171,8 @@ export default class LoggerService extends DependencyAwareClass {
170171
Sentry.captureException(error);
171172
}
172173

173-
if (
174-
typeof process.env.EPSAGON_TOKEN === 'string'
175-
&& process.env.EPSAGON_TOKEN !== 'undefined'
176-
&& typeof process.env.EPSAGON_SERVICE_NAME === 'string'
177-
&& process.env.EPSAGON_SERVICE_NAME !== 'undefined'
178-
&& error instanceof Error
179-
) {
180-
Epsagon.setError(error);
174+
if (LambdaWrapper.isLumigoEnabled && error instanceof Error) {
175+
lumigo.error(message || error.message, { err: error });
181176
}
182177

183178
this.logger.log('error', message, { error: LoggerService.processMessage(error) });
@@ -221,13 +216,8 @@ export default class LoggerService extends DependencyAwareClass {
221216
* @param silent If `false`, the label will also be logged. (default: false)
222217
*/
223218
label(descriptor: string, silent = false) {
224-
if (
225-
typeof process.env.EPSAGON_TOKEN === 'string'
226-
&& process.env.EPSAGON_TOKEN !== 'undefined'
227-
&& typeof process.env.EPSAGON_SERVICE_NAME === 'string'
228-
&& process.env.EPSAGON_SERVICE_NAME !== 'undefined'
229-
) {
230-
Epsagon.label(descriptor, true);
219+
if (LambdaWrapper.isLumigoEnabled) {
220+
lumigo.addExecutionTag(descriptor, true);
231221
}
232222

233223
if (!silent) {
@@ -243,13 +233,8 @@ export default class LoggerService extends DependencyAwareClass {
243233
* @param silent If `false`, the metric will also be logged. (default: false)
244234
*/
245235
metric(descriptor: string, stat: number | string, silent = false) {
246-
if (
247-
typeof process.env.EPSAGON_TOKEN === 'string'
248-
&& process.env.EPSAGON_TOKEN !== 'undefined'
249-
&& typeof process.env.EPSAGON_SERVICE_NAME === 'string'
250-
&& process.env.EPSAGON_SERVICE_NAME !== 'undefined'
251-
) {
252-
Epsagon.label(descriptor, stat);
236+
if (LambdaWrapper.isLumigoEnabled) {
237+
lumigo.addExecutionTag(descriptor, stat);
253238
}
254239

255240
if (silent === false) {

tests/unit/core/LambdaWrapper.spec.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,95 @@ describe('unit.core.LambdaWrapper', () => {
256256
});
257257
});
258258

259+
describe('isLumigoEnabled', () => {
260+
describe('when a Lumigo token is present', () => {
261+
beforeAll(() => {
262+
process.env.LUMIGO_TRACER_TOKEN = 'test';
263+
});
264+
265+
afterAll(() => {
266+
delete process.env.LUMIGO_TRACER_TOKEN;
267+
});
268+
269+
it('should return true', () => {
270+
expect(LambdaWrapper.isLumigoEnabled).toBe(true);
271+
});
272+
});
273+
274+
describe('when there is no Lumigo token', () => {
275+
beforeAll(() => {
276+
delete process.env.LUMIGO_TRACER_TOKEN;
277+
});
278+
279+
it('should return false', () => {
280+
expect(LambdaWrapper.isLumigoEnabled).toBe(false);
281+
});
282+
});
283+
});
284+
285+
describe('isLumigoWrappingUs', () => {
286+
describe('when using the runtime wrapper (e.g. auto-trace)', () => {
287+
beforeAll(() => {
288+
process.env.AWS_LAMBDA_EXEC_WRAPPER = '/opt/lumigo_wrapper';
289+
delete process.env.LUMIGO_ORIGINAL_HANDLER;
290+
process.env.LUMIGO_TRACER_TOKEN = 'test';
291+
});
292+
293+
afterAll(() => {
294+
delete process.env.AWS_LAMBDA_EXEC_WRAPPER;
295+
delete process.env.LUMIGO_TRACER_TOKEN;
296+
});
297+
298+
it('should return true', () => {
299+
expect(LambdaWrapper.isLumigoWrappingUs).toBe(true);
300+
});
301+
});
302+
303+
describe('when using handler redirection', () => {
304+
beforeAll(() => {
305+
delete process.env.AWS_LAMBDA_EXEC_WRAPPER;
306+
process.env.LUMIGO_ORIGINAL_HANDLER = 'handler.js';
307+
process.env.LUMIGO_TRACER_TOKEN = 'test';
308+
});
309+
310+
afterAll(() => {
311+
delete process.env.LUMIGO_ORIGINAL_HANDLER;
312+
delete process.env.LUMIGO_TRACER_TOKEN;
313+
});
314+
315+
it('should return true', () => {
316+
expect(LambdaWrapper.isLumigoWrappingUs).toBe(true);
317+
});
318+
});
319+
320+
describe('when there is only a Lumigo token', () => {
321+
beforeAll(() => {
322+
delete process.env.AWS_LAMBDA_EXEC_WRAPPER;
323+
delete process.env.LUMIGO_ORIGINAL_HANDLER;
324+
process.env.LUMIGO_TRACER_TOKEN = 'test';
325+
});
326+
327+
afterAll(() => {
328+
delete process.env.LUMIGO_TRACER_TOKEN;
329+
});
330+
331+
it('should return false', () => {
332+
expect(LambdaWrapper.isLumigoWrappingUs).toBe(false);
333+
});
334+
});
335+
336+
describe('when there is no Lumigo token', () => {
337+
beforeAll(() => {
338+
delete process.env.AWS_LAMBDA_EXEC_WRAPPER;
339+
delete process.env.LUMIGO_TRACER_TOKEN;
340+
});
341+
342+
it('should return false', () => {
343+
expect(LambdaWrapper.isLumigoWrappingUs).toBe(false);
344+
});
345+
});
346+
});
347+
259348
describe('handleError', () => {
260349
([
261350
[undefined, 400, 0],

0 commit comments

Comments
 (0)