Skip to content
This repository has been archived by the owner on Jun 21, 2023. It is now read-only.

Add sentry logging #31

Merged
merged 12 commits into from
Nov 25, 2019
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### Added
- parent logger name inheritance
- logging to sentry

### Changed
- `pino.pretty` replaced with `util.inspect`
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,30 @@ All loglevels up to warning (exclusive) - trace, debug and info - are logged to

All loglevels from warning up (inclusive) - warning, error, fatal - are logged to `stderr` **only**.

## Using Sentry

Cosmas logs every message to [Sentry](https://sentry.io/) for you, when configured. This feature is disabled by default.

Sentry SDK `@sentry/node` is a peer dependency. If you want cosmas to use it, install it in your project.

```js
// (1) Let cosmas initialize sentry with provided DSN
const myLogger = logger({ sentry: 'https://<key>@sentry.io/<project>' })

// (2) Configure sentry yourself and let cosmas use it
Sentry.init({/*...*/})
const myLogger = logger({ sentry: true })

// (3) Disable sentry (default, no need to send false option)
const myLogger = logger({ sentry: false })
```

When configured, cosmas (additionally to standard logging) captures all logs via Sentry SDK. Logs containing `stack` are logged as exceptions via `captureException` (preserves stack trace) and all other messages via `captureMessage`.

Either way, scope is appropriately set, as well as all payload is passed on in scope's metadata.



## Express middleware

`cosmas` contains an express middleware which you can use to log all requests and responses of your express application.
Expand Down
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
]
},
"author": "Michal Vlasák <michal.vlasak@ackee.cz>",
"contributors": [
"Michal Vlasák <michal.vlasak@ackee.cz>",
"Jaroslav Šmolík <grissius@gmail.com>"
],
"license": "ISC",
"engines": {
"node": ">=6"
Expand All @@ -44,7 +48,11 @@
"pino": "^5.13.2",
"pino-multi-stream": "^4.2.0"
},
"peerDependencies": {
"@sentry/node": "^5.9.0"
},
"devDependencies": {
"@sentry/node": "^5.9.0",
"@types/jest": "^24.0.13",
"@types/lodash.foreach": "^4.5.4",
"@types/lodash.isempty": "^4.4.4",
Expand Down
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ const defaultLogger = (options: AckeeLoggerOptions & { loggerName?: string } = {
serializers.disablePaths(options.disableFields);
serializers.enablePaths(options.enableFields);

if (options.sentry) {
const sentry = require('@sentry/node');
if (typeof options.sentry === 'string') {
sentry.init({ dsn: options.sentry });
}
}

const isTesting = process.env.NODE_ENV === 'test';
const defaultLevel: Level = options.defaultLevel || (isTesting ? 'silent' : 'debug');
const messageKey = 'message'; // best option for Google Stackdriver,
Expand Down
1 change: 1 addition & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ export interface AckeeLoggerOptions {
ignoredHttpMethods?: string[];
config?: LoggerOptions;
pretty?: boolean;
sentry?: string | boolean;
skip?: (req: Request, res?: Response) => boolean;
}
37 changes: 37 additions & 0 deletions src/sentry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { captureException, captureMessage, Severity, withScope } from '@sentry/node';
import { Transform, TransformCallback } from 'stream';

const reportToSentry = (obj: any) => {
if (!obj.stack) {
return captureMessage(obj.message || obj);
}
const error = new Error(obj.message);
error.message = obj.message;
error.stack = obj.stack;
error.name = obj.name;
return captureException(error);
};

const PINO_TO_SENTRY: { [key: number]: Severity } = {
10: Severity.Debug,
20: Severity.Debug,
30: Severity.Info,
40: Severity.Warning,
50: Severity.Error,
60: Severity.Critical,
};

class SentryTransformStream extends Transform {
// tslint:disable-next-line:function-name
public _transform(chunk: any, _encoding: string, callback: TransformCallback) {
const obj = JSON.parse(chunk);
withScope(scope => {
scope.setLevel(PINO_TO_SENTRY[obj.level]);
scope.setExtras(obj);
reportToSentry(obj);
});
this.push(chunk);
callback();
}
}
export { SentryTransformStream };
6 changes: 6 additions & 0 deletions src/streams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as util from 'util';
import { loggerNameKey, pkgVersionKey } from '.';
import { AckeeLoggerOptions, AckeeLoggerStream } from './interfaces';
import { levels } from './levels';
import { SentryTransformStream } from './sentry';
import { StackDriverFormatStream } from './stackdriver';

const pkgJson = JSON.parse(fs.readFileSync(path.resolve(path.join(__dirname, '..', 'package.json')), 'utf8'));
Expand Down Expand Up @@ -71,6 +72,11 @@ const initLoggerStreams = (
}

streams = decorateStreams(streams, getDefaultTransformStream(options));

if (options.sentry) {
streams = decorateStreams(streams, SentryTransformStream);
}

return streams;
};

Expand Down
111 changes: 111 additions & 0 deletions src/tests/sentry-mocked.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
let loggerFactory;
const scope: any = {};
const withScope = jest.fn(fn =>
fn({
setContext: (key: string, val: any) => {
scope.context = { [key]: val };
},
setExtras: (val: any) => {
scope.extras = val;
},
setLevel: (level: any) => {
scope.level = level;
},
})
);

const createCapture = (cb = () => {}) => data => {
cb();
return { data, scope };
};

const captureException = jest.fn(createCapture());
const captureMessage = jest.fn(createCapture());
const init = jest.fn();

describe('sentry mocked', () => {
beforeAll(() => {
jest.mock('@sentry/node', () => {
return {
captureException,
captureMessage,
withScope,
init,
Severity: {
Debug: 'debug',
Info: 'info',
Warning: 'warning',
Error: 'error',
Critical: 'critical',
},
};
});
loggerFactory = require('..').default;
});
beforeEach(() => {
captureException.mockReset();
captureMessage.mockReset();
});
test('can create logger with options', () => {
expect(() => loggerFactory()).not.toThrowError();
expect(() => loggerFactory({ sentry: true })).not.toThrowError();
expect(init).not.toHaveBeenCalled();
expect(() => loggerFactory({ sentry: 'dummy' })).not.toThrowError();
expect(init.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"dsn": "dummy",
},
]
`);
});

test('sentry captureMessage is called with correct scope', async () => {
await new Promise((resolve, reject) => {
const logger = loggerFactory({
sentry: 'DSN',
});
captureMessage.mockImplementation(createCapture(resolve));
logger.info('Foo');
});
expect(captureMessage).toHaveBeenCalledTimes(1);
expect(captureException).not.toHaveBeenCalled();
expect(captureMessage.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"Foo",
]
`);
expect(captureMessage.mock.results[0].value).toMatchInlineSnapshot(`
Object {
"data": "Foo",
"scope": Object {
"extras": Object {
"level": 30,
"message": "Foo",
"v": 1,
},
"level": "info",
},
}
`);
});

test('sentry captureException with stack and correct levels', async () => {
await new Promise((resolve, reject) => {
const logger = loggerFactory({
sentry: 'DSN',
});
captureException.mockReset();
captureException.mockImplementation(createCapture(resolve));
logger.error(new Error());
});
expect(captureException).toHaveBeenCalledTimes(1);
expect(captureMessage).not.toHaveBeenCalled();
expect(captureException.mock.results[0].value).toMatchObject({
data: expect.any(Error),
scope: {
level: 'error',
},
});
});
});
15 changes: 15 additions & 0 deletions src/tests/sentry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import loggerFactory from '..';

describe('sentry not available', () => {
beforeAll(() => {
jest.mock('@sentry/node', () => {
throw new Error("Cannot find module '@sentry/node' from 'index.ts'");
});
});
test('without sentry lib works by default, but crashes on provided', () => {
expect(() => loggerFactory()).not.toThrowError();
expect(() => loggerFactory({ sentry: 'DSN' })).toThrowErrorMatchingInlineSnapshot(
`"Cannot find module '@sentry/node' from 'index.ts'"`
);
});
});