From be0ae462d3b3b4c0186d12982c02783294cf0d76 Mon Sep 17 00:00:00 2001 From: James Sumners Date: Tue, 20 Feb 2018 19:34:07 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 13 +++++ .gitignore | 53 ++++++++++++++++++++ .travis.yml | 20 ++++++++ Readme.md | 92 ++++++++++++++++++++++++++++++++++ index.js | 26 ++++++++++ lib/err.js | 17 +++++++ lib/req.js | 72 +++++++++++++++++++++++++++ lib/res.js | 47 ++++++++++++++++++ package.json | 36 ++++++++++++++ test/err.test.js | 24 +++++++++ test/req.test.js | 127 +++++++++++++++++++++++++++++++++++++++++++++++ test/res.test.js | 70 ++++++++++++++++++++++++++ 12 files changed, 597 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 Readme.md create mode 100644 index.js create mode 100644 lib/err.js create mode 100644 lib/req.js create mode 100644 lib/res.js create mode 100644 package.json create mode 100644 test/err.test.js create mode 100644 test/req.test.js create mode 100644 test/res.test.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3fec5c5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +# [*.md] +# trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e09c5df --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +# Lock files +shrinkwrap.yaml +package-lock.json +yarn.lock + +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +node_modules + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# 0x +.__browserify_string_empty.js +profile-* +*.flamegraph + +# tap --cov +.nyc_output/ + +# JetBrains IntelliJ IDEA +.idea/ +*.iml + +# VS Code +.vscode/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e4f9c5a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +language: node_js + +node_js: + - "9" + - "8" + - "6" + +# before_install: +# - curl -L https://unpkg.com/@pnpm/self-installer | node +# install: +# - pnpm install + +script: + - npm run lint-ci + - npm run test-ci + +notifications: + email: + on_success: never + on_failure: always diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..a8aaeb1 --- /dev/null +++ b/Readme.md @@ -0,0 +1,92 @@ +# pino-std-serializers + +This module provides a set of standard object serializers for the +[Pino](https://getpino.io) logger. + +## Serializers + +### `exports.err(error)` +Serializes an `Error` like object. Returns an object: + +```js +{ + type: 'string', // The name of the object's constructor. + message: 'string', // The supplied error message. + stack: 'string' // The stack when the error was generated. +} +``` + +Any other extra properties, e.g. `statusCode`, that have been attached to the +object will also be present on the serialized object. + +### `exports.mapHttpResponse(response)` +Used internally by Pino for general response logging. Returns an object: + +```js +{ + res: {} +} +``` + +Where `res` is the `response` as serialized by the standard response serializer. + +### `exports.mapHttpRequest(request)` +Used internall by Pino for general request logging. Returns an object: + +```js +{ + req: {} +} +``` + +Where `req` is the `request` as serialized by the standard request serializer. + +### `exports.req(request)` +The default `request` serializer. Returns and object: + +```js +{ + id: 'string', // Default is an empty string. Attach a synchronous function + // to the input `request` that returns an identifier to have + // the value filled. + method: 'string', + url: 'string', + headers: Object, + remoteAddress: 'string', + remotePort: Number, + raw: Object // Non-enumerable, i.e. will not be in the output, original + // request object. This is available for subsequent serializers + // to use. +} +``` + +### `exports.res(response)` +The default `response` serializer. Returns an object: + +```js +{ + statusCode: Number, + header: Array, // The list of headers to be sent in the response. + raw: Object // Non-enumerable, i.e. will not be in the output, original + // response object. This is available for subsequent serializers + // to use. +} +``` + +### `exports.wrapRequestSerializer(customSerializer)` +A utility method for wrapping the default request serializer. This allows +custom serializers to work with the already serialized object. + +The `customSerializer` accepts one parameter: the newly serialized request +object. + +### `exports.wrapResponseSerializer(customSerializer)` +A utility method for wrapping the default response serializer. This allows +custom serializers to work with the already serialized object. + +The `customSerializer` accepts one parameter: the newly serialized response +object. + +## License + +MIT License diff --git a/index.js b/index.js new file mode 100644 index 0000000..28501b1 --- /dev/null +++ b/index.js @@ -0,0 +1,26 @@ +'use srict' + +var reqSerializers = require('./lib/req') +var resSerializers = require('./lib/res') + +module.exports = { + err: require('./lib/err'), + mapHttpRequest: reqSerializers.mapHttpRequest, + mapHttpResponse: resSerializers.mapHttpResponse, + req: reqSerializers.reqSerializer, + res: resSerializers.resSerializer, + + wrapRequestSerializer: function wrapRequestSerializer (customSerializer) { + if (customSerializer === reqSerializers.reqSerializer) return customSerializer + return function wrappedReqSerializer (req) { + return customSerializer(reqSerializers.reqSerializer(req)) + } + }, + + wrapResponseSerializer: function wrapResponseSerializer (customSerializer) { + if (customSerializer === resSerializers.resSerializer) return customSerializer + return function wrappedResSerializer (res) { + return customSerializer(resSerializers.resSerializer(res)) + } + } +} diff --git a/lib/err.js b/lib/err.js new file mode 100644 index 0000000..d473d84 --- /dev/null +++ b/lib/err.js @@ -0,0 +1,17 @@ +'use strict' + +module.exports = errSerializer + +function errSerializer (err) { + var obj = { + type: err.constructor.name, + message: err.message, + stack: err.stack + } + for (var key in err) { + if (obj[key] === undefined) { + obj[key] = err[key] + } + } + return obj +} diff --git a/lib/req.js b/lib/req.js new file mode 100644 index 0000000..293fc58 --- /dev/null +++ b/lib/req.js @@ -0,0 +1,72 @@ +'use strict' + +module.exports = { + mapHttpRequest, + reqSerializer +} + +var rawSymbol = Symbol('pino-raw-req-ref') +var pinoReqProto = Object.create({}, { + id: { + enumerable: true, + writable: true, + value: '' + }, + method: { + enumerable: true, + writable: true, + value: '' + }, + url: { + enumerable: true, + writable: true, + value: '' + }, + headers: { + enumerable: true, + writable: true, + value: {} + }, + remoteAddress: { + enumerable: true, + writable: true, + value: '' + }, + remotePort: { + enumerable: true, + writable: true, + value: '' + }, + raw: { + enumerable: false, + get: function () { + return this[rawSymbol] + }, + set: function (val) { + this[rawSymbol] = val + } + } +}) +Object.defineProperty(pinoReqProto, rawSymbol, { + writable: true, + value: {} +}) + +function reqSerializer (req) { + var connection = req.connection + const _req = Object.create(pinoReqProto) + _req.id = typeof req.id === 'function' ? req.id() : req.id + _req.method = req.method + _req.url = req.url + _req.headers = req.headers + _req.remoteAddress = connection && connection.remoteAddress + _req.remotePort = connection && connection.remotePort + _req.raw = req + return _req +} + +function mapHttpRequest (req) { + return { + req: reqSerializer(req) + } +} diff --git a/lib/res.js b/lib/res.js new file mode 100644 index 0000000..229f5dd --- /dev/null +++ b/lib/res.js @@ -0,0 +1,47 @@ +'use strict' + +module.exports = { + mapHttpResponse, + resSerializer +} + +var rawSymbol = Symbol('pino-raw-res-ref') +var pinoResProto = Object.create({}, { + statusCode: { + enumerable: true, + writable: true, + value: 0 + }, + header: { + enumerable: true, + writable: true, + value: '' + }, + raw: { + enumerable: false, + get: function () { + return this[rawSymbol] + }, + set: function (val) { + this[rawSymbol] = val + } + } +}) +Object.defineProperty(pinoResProto, rawSymbol, { + writable: true, + value: {} +}) + +function resSerializer (res) { + const _res = Object.create(pinoResProto) + _res.statusCode = res.statusCode + _res.header = res._header + _res.raw = res + return _res +} + +function mapHttpResponse (res) { + return { + res: resSerializer(res) + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8cc4a85 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "pino-std-serializers", + "version": "1.0.0", + "description": "A collection of standard object serializers for Pino", + "main": "index.js", + "scripts": { + "lint": "standard | snazzy", + "lint-ci": "standard", + "test": "tap --no-cov 'test/**/*.test.js'", + "test-ci": "tap --cov --coverage-report=text 'test/**/*.test.js'" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/pinojs/pino-std-serializers.git" + }, + "keywords": [ + "pino", + "logging" + ], + "author": "James Sumners ", + "license": "MIT", + "bugs": { + "url": "https://github.com/pinojs/pino-std-serializers/issues" + }, + "homepage": "https://github.com/pinojs/pino-std-serializers#readme", + "precommit": [ + "lint", + "test" + ], + "devDependencies": { + "pre-commit": "^1.2.2", + "snazzy": "^7.0.0", + "standard": "^11.0.0", + "tap": "^11.1.1" + } +} diff --git a/test/err.test.js b/test/err.test.js new file mode 100644 index 0000000..0d032be --- /dev/null +++ b/test/err.test.js @@ -0,0 +1,24 @@ +'use strict' + +var test = require('tap').test +const serializer = require('../lib/err') + +test('serializes Error objects', function (t) { + t.plan(3) + var serialized = serializer(Error('foo')) + t.is(serialized.type, 'Error') + t.is(serialized.message, 'foo') + t.match(serialized.stack, /err\.test\.js:/) +}) + +test('serializes Error objects with extra properties', function (t) { + t.plan(5) + var err = Error('foo') + err.statusCode = 500 + var serialized = serializer(err) + t.is(serialized.type, 'Error') + t.is(serialized.message, 'foo') + t.ok(serialized.statusCode) + t.is(serialized.statusCode, 500) + t.match(serialized.stack, /err\.test\.js:/) +}) diff --git a/test/req.test.js b/test/req.test.js new file mode 100644 index 0000000..da2fc5c --- /dev/null +++ b/test/req.test.js @@ -0,0 +1,127 @@ +'use strict' + +var http = require('http') +var test = require('tap').test +var serializers = require('../lib/req') +var wrapRequestSerializer = require('../').wrapRequestSerializer + +test('maps request', function (t) { + t.plan(2) + + var server = http.createServer(handler) + server.unref() + server.listen(0, () => { + http.get(server.address(), () => {}) + }) + + t.tearDown(() => server.close()) + + function handler (req, res) { + var serialized = serializers.mapHttpRequest(req) + t.ok(serialized.req) + t.ok(serialized.req.method) + t.end() + res.end() + } +}) + +test('does not return excessively long object', function (t) { + t.plan(1) + + var server = http.createServer(handler) + server.unref() + server.listen(0, () => { + http.get(server.address(), () => {}) + }) + + t.tearDown(() => server.close()) + + function handler (req, res) { + var serialized = serializers.reqSerializer(req) + t.is(Object.keys(serialized).length, 6) + res.end() + } +}) + +test('req.raw is available', function (t) { + t.plan(2) + + var server = http.createServer(handler) + server.unref() + server.listen(0, () => { + http.get(server.address(), () => {}) + }) + + t.tearDown(() => server.close()) + + function handler (req, res) { + req.foo = 'foo' + var serialized = serializers.reqSerializer(req) + t.ok(serialized.raw) + t.is(serialized.raw.foo, 'foo') + res.end() + } +}) + +test('req.id has a non-function value', function (t) { + t.plan(1) + + var server = http.createServer(handler) + server.unref() + server.listen(0, () => { + http.get(server.address(), () => {}) + }) + + t.tearDown(() => server.close()) + + function handler (req, res) { + var serialized = serializers.reqSerializer(req) + t.is(typeof serialized.id === 'function', false) + res.end() + } +}) + +test('req.id has a non-function value with custom id function', function (t) { + t.plan(2) + + var server = http.createServer(handler) + server.unref() + server.listen(0, () => { + http.get(server.address(), () => {}) + }) + + t.tearDown(() => server.close()) + + function handler (req, res) { + req.id = function () { return 42 } + var serialized = serializers.reqSerializer(req) + t.is(typeof serialized.id === 'function', false) + t.is(serialized.id, 42) + res.end() + } +}) + +test('can wrap request serializers', function (t) { + t.plan(3) + + var server = http.createServer(handler) + server.unref() + server.listen(0, () => { + http.get(server.address(), () => {}) + }) + + t.tearDown(() => server.close()) + + var serailizer = wrapRequestSerializer(function (req) { + t.ok(req.method) + t.is(req.method, 'GET') + delete req.method + return req + }) + + function handler (req, res) { + var serialized = serailizer(req) + t.notOk(serialized.method) + res.end() + } +}) diff --git a/test/res.test.js b/test/res.test.js new file mode 100644 index 0000000..4f85aa5 --- /dev/null +++ b/test/res.test.js @@ -0,0 +1,70 @@ +'use strict' + +var http = require('http') +var test = require('tap').test +var serializers = require('../lib/res') +var wrapResponseSerializer = require('../').wrapResponseSerializer + +test('res.raw is not enumerable', function (t) { + t.plan(1) + + var server = http.createServer(handler) + server.unref() + server.listen(0, () => { + http.get(server.address(), () => {}) + }) + + t.tearDown(() => server.close()) + + function handler (req, res) { + var serialized = serializers.resSerializer(res) + t.is(serialized.propertyIsEnumerable('raw'), false) + res.end() + } +}) + +test('res.raw is available', function (t) { + t.plan(2) + + var server = http.createServer(handler) + server.unref() + server.listen(0, () => { + http.get(server.address(), () => {}) + }) + + t.tearDown(() => server.close()) + + function handler (req, res) { + res.statusCode = 200 + var serialized = serializers.resSerializer(res) + t.ok(serialized.raw) + t.is(serialized.raw.statusCode, 200) + res.end() + } +}) + +test('can wrap response serializers', function (t) { + t.plan(3) + + var server = http.createServer(handler) + server.unref() + server.listen(0, () => { + http.get(server.address(), () => {}) + }) + + t.tearDown(() => server.close()) + + var serailizer = wrapResponseSerializer(function (res) { + t.ok(res.statusCode) + t.is(res.statusCode, 200) + delete res.statusCode + return res + }) + + function handler (req, res) { + res.statusCode = 200 + var serialized = serailizer(res) + t.notOk(serialized.statusCode) + res.end() + } +})