Skip to content

Commit

Permalink
feat(serializers): Implemented "error with cause" serializer (#130)
Browse files Browse the repository at this point in the history
  • Loading branch information
Yuval-Peled authored Apr 9, 2023
1 parent b4edb5d commit 0ccfa26
Show file tree
Hide file tree
Showing 9 changed files with 389 additions and 41 deletions.
76 changes: 74 additions & 2 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,84 @@ Serializes an `Error` like object. Returns an object:
raw: Error // Non-enumerable, i.e. will not be in the output, original
// Error object. This is available for subsequent serializers
// to use.
[...any additional Enumerable property the original Error had]
}
```

Any other extra properties, e.g. `statusCode`, that have been attached to the
object will also be present on the serialized object.

If the error object has a [`cause`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) property, the `cause`'s `message` and `stack` will be appended to the top-level `message` and `stack`. All other parameters that belong to the `error.cause` object will be omitted.

Example:

```js
const serializer = require('pino-std-serializers').err;

const innerError = new Error("inner error");
innerError.isInner = true;
const outerError = new Error("outer error", { cause: innerError });
outerError.isInner = false;

const serialized = serializer(outerError);
/* Result:
{
"type": "Error",
"message": "outer error: inner error",
"isInner": false,
"stack": "Error: outer error
at <...omitted..>
caused by: Error: inner error
at <...omitted..>
}
*/

### `exports.errWithCause(error)`
Serializes an `Error` like object, including any `error.cause`. 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.
cause?: Error, // If the original error had an error.cause, it will be serialized here
raw: Error // Non-enumerable, i.e. will not be in the output, original
// Error object. This is available for subsequent serializers
// to use.
[...any additional Enumerable property the original Error had]
}
```

Any other extra properties, e.g. `statusCode`, that have been attached to the object will also be present on the serialized object.

Example:
```javascript
const serializer = require('pino-std-serializers').errWithCause;
const innerError = new Error("inner error");
innerError.isInner = true;
const outerError = new Error("outer error", { cause: innerError });
outerError.isInner = false;
const serialized = serializer(outerError);
/* Result:
{
"type": "Error",
"message": "outer error",
"isInner": false,
"stack": "Error: outer error
at <...omitted..>",
"cause": {
"type": "Error",
"message": "inner error",
"isInner": true,
"stack": "Error: inner error
at <...omitted..>"
},
}
*/
```

### `exports.mapHttpResponse(response)`
Used internally by Pino for general response logging. Returns an object:

Expand Down Expand Up @@ -49,7 +121,7 @@ The default `request` serializer. Returns an object:

```js
{
id: 'string', // Defaults to `undefined`, unless there is an `id` property
id: 'string', // Defaults to `undefined`, unless there is an `id` property
// already attached to the `request` object or to the `request.info`
// object. Attach a synchronous function
// to the `request.id` that returns an identifier to have
Expand All @@ -64,7 +136,7 @@ The default `request` serializer. Returns an object:
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. In cases where the `request` input already has
// to use. In cases where the `request` input already has
// a `raw` property this will replace the original `request.raw`
// property
}
Expand Down
8 changes: 7 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,16 @@ export interface SerializedError {
}

/**
* Serializes an Error object.
* Serializes an Error object. Does not serialize "err.cause" fields (will append the err.cause.message to err.message
* and err.cause.stack to err.stack)
*/
export function err(err: Error): SerializedError;

/**
* Serializes an Error object, including full serialization for any err.cause fields recursively.
*/
export function errWithCause(err: Error): SerializedError;

export interface SerializedRequest {
/**
* Defaults to `undefined`, unless there is an `id` property already attached to the `request` object or
Expand Down
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
'use strict'

const errSerializer = require('./lib/err')
const errWithCauseSerializer = require('./lib/err-with-cause')
const reqSerializers = require('./lib/req')
const resSerializers = require('./lib/res')

module.exports = {
err: errSerializer,
errWithCause: errWithCauseSerializer,
mapHttpRequest: reqSerializers.mapHttpRequest,
mapHttpResponse: resSerializers.mapHttpResponse,
req: reqSerializers.reqSerializer,
Expand Down
48 changes: 48 additions & 0 deletions lib/err-proto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use strict'

const seen = Symbol('circular-ref-tag')
const rawSymbol = Symbol('pino-raw-err-ref')

const pinoErrProto = Object.create({}, {
type: {
enumerable: true,
writable: true,
value: undefined
},
message: {
enumerable: true,
writable: true,
value: undefined
},
stack: {
enumerable: true,
writable: true,
value: undefined
},
aggregateErrors: {
enumerable: true,
writable: true,
value: undefined
},
raw: {
enumerable: false,
get: function () {
return this[rawSymbol]
},
set: function (val) {
this[rawSymbol] = val
}
}
})
Object.defineProperty(pinoErrProto, rawSymbol, {
writable: true,
value: {}
})

module.exports = {
pinoErrProto,
pinoErrorSymbols: {
seen,
rawSymbol
}
}
48 changes: 48 additions & 0 deletions lib/err-with-cause.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use strict'

module.exports = errWithCauseSerializer

const { isErrorLike } = require('./err-helpers')
const { pinoErrProto, pinoErrorSymbols } = require('./err-proto')
const { seen } = pinoErrorSymbols

const { toString } = Object.prototype

function errWithCauseSerializer (err) {
if (!isErrorLike(err)) {
return err
}

err[seen] = undefined // tag to prevent re-looking at this
const _err = Object.create(pinoErrProto)
_err.type = toString.call(err.constructor) === '[object Function]'
? err.constructor.name
: err.name
_err.message = err.message
_err.stack = err.stack

if (Array.isArray(err.errors)) {
_err.aggregateErrors = err.errors.map(err => errWithCauseSerializer(err))
}

if (isErrorLike(err.cause) && !Object.prototype.hasOwnProperty.call(err.cause, seen)) {
_err.cause = errWithCauseSerializer(err.cause)
}

for (const key in err) {
if (_err[key] === undefined) {
const val = err[key]
if (isErrorLike(val)) {
if (!Object.prototype.hasOwnProperty.call(val, seen)) {
_err[key] = errWithCauseSerializer(val)
}
} else {
_err[key] = val
}
}
}

delete err[seen] // clean up tag in case err is serialized again later
_err.raw = err
return _err
}
39 changes: 2 additions & 37 deletions lib/err.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,10 @@
module.exports = errSerializer

const { messageWithCauses, stackWithCauses, isErrorLike } = require('./err-helpers')
const { pinoErrProto, pinoErrorSymbols } = require('./err-proto')
const { seen } = pinoErrorSymbols

const { toString } = Object.prototype
const seen = Symbol('circular-ref-tag')
const rawSymbol = Symbol('pino-raw-err-ref')
const pinoErrProto = Object.create({}, {
type: {
enumerable: true,
writable: true,
value: undefined
},
message: {
enumerable: true,
writable: true,
value: undefined
},
stack: {
enumerable: true,
writable: true,
value: undefined
},
aggregateErrors: {
enumerable: true,
writable: true,
value: undefined
},
raw: {
enumerable: false,
get: function () {
return this[rawSymbol]
},
set: function (val) {
this[rawSymbol] = val
}
}
})
Object.defineProperty(pinoErrProto, rawSymbol, {
writable: true,
value: {}
})

function errSerializer (err) {
if (!isErrorLike(err)) {
Expand Down
Loading

0 comments on commit 0ccfa26

Please sign in to comment.