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

Add skip option #16

Merged
merged 6 commits into from
Jun 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
### Added
- coveralls integration
- automatic logger name in non-pretty loggers
- `options.skip` settings for custom log filtering in Express

## Changed
- refactoring of express handlers
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,18 @@ app.use(logger.expressError)

All those log messages will contain request and possibly response, error, time from request to response, status code and `user-agent`, `x-deviceid` and `authorization` request headers.

### Request skipping
You might want to omit some requests from logging completely. Right now, there are two ways to do it and you can even use both at once.
1) Use `options.ignoredHttpMethods` to define an array of HTTP methods you want to omit. By default all `OPTIONS` requests are ommited. See [options](#options) for details
smoliji marked this conversation as resolved.
Show resolved Hide resolved
2) Use `options.skip` method to define custom rules for requests skipping. Set it to a function which accepts an Express's `Request` and returns `boolean`. If the return value is `true`, request (and corresponding response) will not be logged. You might want to use `matchPath` helper to ignore requests based on the [`req.originalUrl` value](https://expressjs.com/en/4x/api.html#req.originalUrl)

```js
const { matchPath } = require('cosmas/utils');
const logger = require('cosmas').default({
skip: matchPath(/heal.h/),
});
```

## Environment-specific behavior
`cosmas` is meant to be used throughout different environments (development, testing, production) and some of its configuration is setup differently based on the environment it runs in.

Expand All @@ -132,6 +144,7 @@ Options override both default logger configuration and environment-specific conf
- `streams` - list of stream objects, which will be passed directly to [pino-multistream's multistream function](https://github.com/pinojs/pino-multi-stream#pinomsmultistreamstreams) instead of default `cosmas` stream
- `pretty` - if set to `true`, logger will use [pino pretty human-readable logs](https://github.com/pinojs/pino/blob/master/docs/API.md#pretty). This option can be overriden by `streams`
- `disableStackdriverFormat` - if set to `false`, logger will add `severity` field to all log objects, so that log levels in Google Stackdriver work as expected. Defaults to `false`
- `skip` - Function to be used in express middlewares for filtering request logs. If the function returns `true` for a given request, no message will be logged. No default value.
- `config` - object, which will be passed to underlying logger object. Right now, underlying logger is [pino](https://github.com/pinojs/pino), so for available options see [pino API docs](https://github.com/pinojs/pino/blob/master/docs/API.md#pinooptions-stream)

## Default serializers
Expand Down
4 changes: 4 additions & 0 deletions src/express.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ const expressOnFinished = (logger: AckeeLogger, req: AckeeRequest) => (_err: Err
const error = res[errorSymbol];
const reqOut = `${res.statusCode} ${req.method} ${req.originalUrl} ${res.time} ms ${req.headers['user-agent']}`;
if (logger.options.ignoredHttpMethods && logger.options.ignoredHttpMethods.includes(req.method)) {
// left here for BC
return;
}
if (logger.options.skip && logger.options.skip(req)) {
return;
}
const standardOutput = {
Expand Down
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ const defaultLogger = (options: AckeeLoggerOptions & { loggerName?: string } = {
const logger = (pino(
// no deep-merging needed, so assign is OK
Object.assign(
{},
{
messageKey,
base: {},
Expand Down
2 changes: 2 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Request } from 'express';
import * as pino from 'pino';

interface LoggerOptions extends pino.LoggerOptions {
Expand All @@ -20,4 +21,5 @@ export interface AckeeLoggerOptions {
ignoredHttpMethods?: string[];
config?: LoggerOptions;
pretty?: boolean;
skip?: (req: Request) => boolean;
}
135 changes: 135 additions & 0 deletions src/tests/express.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import * as express from 'express';
import 'jest-extended';
import { Writable } from 'stream';
import * as supertest from 'supertest';

let loggerFactory;

beforeEach(() => {
jest.resetModules();
loggerFactory = require('..').default;
});

const testWriteStream = (resolve, assert) => ({
stream: new Writable({
write: (chunk, encoding, next) => {
const json = JSON.parse(chunk);
assert(json);
next();
resolve();
},
}),
});

test('express binds', () => {
const logger = loggerFactory();
const app = express();
const request = supertest(app);
app.use(logger.express);
return request.get('/');
});

test('GET requests are logged by default', () =>
new Promise((resolve, reject) => {
const logger = loggerFactory({
streams: [testWriteStream(resolve, json => expect(json.req.method).toBe('GET'))],
});
const app = express();
const request = supertest(app);
app.use(logger.express);
request.get('/').then(() => null);
}));

test('OPTIONS requests are ignored by default', () => {
const loggerWrites = jest.fn();
const logger = loggerFactory({
streams: [
{
stream: new Writable({
write: (chunk, encoding, next) => {
loggerWrites();
next();
},
}),
},
],
});
const app = express();
const request = supertest(app);
app.use(logger.express);
return request.options('/').then(() => {
expect(loggerWrites).not.toBeCalled();
});
});

['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'].forEach(method => {
test(`${method} HTTP method can be ignored by options`, () => {
const loggerWrites = jest.fn();
const logger = loggerFactory({
ignoredHttpMethods: [method],
streams: [
{
stream: new Writable({
write: (chunk, encoding, next) => {
loggerWrites();
next();
},
}),
},
],
});
const app = express();
const request = supertest(app);
app.use(logger.express);
return request[method.toLowerCase()]('/').then(() => {
expect(loggerWrites).not.toBeCalled();
});
});
});

test('route can be ignored by logger options', () => {
const loggerWrites = jest.fn();
const logger = loggerFactory({
streams: [
{
stream: new Writable({
write: (chunk, encoding, next) => {
loggerWrites();
next();
},
}),
},
],
skip: (req: express.Request) => req.url === '/not-logged',
});
const app = express();
const request = supertest(app);
app.use(logger.express);
return request.get('/not-logged').then(() => {
expect(loggerWrites).not.toBeCalled();
});
});

test('route can be ignored using regexp helper', () => {
const { matchPath } = require('../utils');
const loggerWrites = jest.fn();
const logger = loggerFactory({
streams: [
{
stream: new Writable({
write: (chunk, encoding, next) => {
loggerWrites();
next();
},
}),
},
],
skip: matchPath(/heal.h/),
});
const app = express();
const request = supertest(app);
app.use(logger.express);
return request.get('/healthcheck').then(() => {
expect(loggerWrites).not.toBeCalled();
});
});
68 changes: 0 additions & 68 deletions src/tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import * as express from 'express';
import 'jest-extended';
import { Writable } from 'stream';
import * as supertest from 'supertest';
import { levels } from '../levels';

let loggerFactory;
Expand Down Expand Up @@ -70,72 +68,6 @@ test('child logger has warning level', () =>
childLogger.warning('Hello');
}));

test('express binds', () => {
const logger = loggerFactory();
const app = express();
const request = supertest(app);
app.use(logger.express);
return request.get('/');
});

test('GET requests are logged by default', () =>
new Promise((resolve, reject) => {
const logger = loggerFactory({
streams: [testWriteStream(resolve, json => expect(json.req.method).toBe('GET'))],
});
const app = express();
const request = supertest(app);
app.use(logger.express);
request.get('/').then(() => null);
}));

test('OPTIONS requests are ignored by default', () => {
const loggerWrites = jest.fn();
const logger = loggerFactory({
streams: [
{
stream: new Writable({
write: (chunk, encoding, next) => {
loggerWrites();
next();
},
}),
},
],
});
const app = express();
const request = supertest(app);
app.use(logger.express);
return request.options('/').then(() => {
expect(loggerWrites).not.toBeCalled();
});
});

['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'].forEach(method => {
test(`${method} HTTP method can be ignored by options`, () => {
const loggerWrites = jest.fn();
const logger = loggerFactory({
ignoredHttpMethods: [method],
streams: [
{
stream: new Writable({
write: (chunk, encoding, next) => {
loggerWrites();
next();
},
}),
},
],
});
const app = express();
const request = supertest(app);
app.use(logger.express);
return request[method.toLowerCase()]('/').then(() => {
expect(loggerWrites).not.toBeCalled();
});
});
});

test('severity field is automatically added to log object', () =>
new Promise((resolve, reject) => {
const logger = loggerFactory({
Expand Down
5 changes: 4 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Request } from 'express';
import { Dictionary } from 'lodash';
import isEmpty = require('lodash.isempty');
import omit = require('omit-deep');

const removeEmpty = (obj: Dictionary<any>): object =>
omit(obj, Object.keys(obj).filter(key => obj[key] === undefined || isEmpty(obj[key])));

export { removeEmpty };
const matchPath = (pattern: RegExp) => (req: Request): boolean => req.originalUrl.match(pattern) !== null;

export { removeEmpty, matchPath };