Skip to content

Commit

Permalink
Enable the include processor to target a sub document of the log record
Browse files Browse the repository at this point in the history
  • Loading branch information
cressie176 committed Mar 13, 2024
1 parent 81c22f8 commit 0c4b2d3
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 53 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to Semantic Versioning](https://semver.org/spec/v2.0.0.html)

## 4.3.0

- Enable the include processor to target a sub document of the log record

## 4.2.1

- Add precondition to include to improve performance
Expand Down
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,16 +326,17 @@ Shallow copies the specified paths into a new log record. This is useful to avoi

It has the following options:

| name | type | required | default | notes |
| ------------ | -------- | -------- | ---------- | ---------------------------------------------------------- |
| paths | array | no | [] | Specifies the paths of the fields to include |
| precondition | function | no | () => true | A function which must return true for the processor to run |
| name | type | required | default | notes |
| ------------ | -------- | -------- | ---------- | ------------------------------------------------------------------------------ |
| basePath | string | no | | Specifies the base path to work from. Other properties will be copied verbatim |
| paths | array | no | [] | Specifies the paths (relative to any base path) of the fields to include |
| precondition | function | no | () => true | A function which must return true for the processor to run |

#### example

```js
const logger = new Logger({
processors: [context(), include({ paths: ["error.request.method", "error.request.url", "error.response.status"]), json()],
processors: [context(), include({ basePath: "error", paths: ["request.method", "request.url", "response.status"]), json()],
});
logger.error(httpError);
```
Expand All @@ -357,7 +358,7 @@ logger.error(httpError);
}
```
You can use array notation (e.g. `items[0].name`), however the resulting document will still be yielded as an object, i.e.
Paths can reference arrays (e.g. items[0]), however the resulting document will still be yielded as an object, i.e.
```js
const logger = new Logger({
Expand All @@ -371,7 +372,9 @@ logger.info("How blissful it is, for one who has nothing", { items: ["a", "b", "
"items": {
"1": "b",
"2": "c"
}
},
"message": "How blissful it is, for one who has nothing",
"level": "INFO"
}
```
Expand Down
40 changes: 31 additions & 9 deletions lib/processors/include.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,45 @@ const toPath = require("to-path");
const ALWAYS_PASS = () => true;

module.exports = (params = {}) => {
return params.basePath === undefined ? include(params) : includeBasePath(params);
};

function include(params) {
const { paths = [], precondition = ALWAYS_PASS } = params;
return ({ level, message, context, record }) => {
if (!precondition({ level, message, context, record })) return record;
return getPatch(paths, record);
};
}

function includeBasePath(params) {
const { basePath, paths = [], precondition = ALWAYS_PASS } = params;
return ({ level, message, context, record }) => {
if (!precondition({ level, message, context, record })) return record;
if (!has(record, basePath, { split: objectsAndArrays })) return record;

const included = paths.reduce((acc, path) => {
if (!has(record, path, { split: getSplit })) return acc;
const value = get(record, path, { split: getSplit });
return set(acc, path, value, { split: setSplit });
}, {});
return { ...included };
const target = get(record, basePath, { split: objectsAndArrays });
const patch = getPatch(paths, target);
return applyPatch(record, basePath, patch);
};
};
}

function getSplit(path) {
function objectsAndArrays(path) {
return toPath(path).map((p) => (Number.isNaN(Number(p)) ? p : Number(p)));
}

function setSplit(path) {
function objectsOnly(path) {
return toPath(path);
}

function getPatch(paths, record) {
return paths.reduce((acc, path) => {
if (!has(record, path, { split: objectsAndArrays })) return acc;
const value = get(record, path, { split: objectsAndArrays });
return set(acc, path, value, { split: objectsOnly });
}, {});
}

function applyPatch(record, basePath, patch) {
return set(structuredClone(record), basePath, patch, { split: objectsOnly });
}
132 changes: 95 additions & 37 deletions test/processors/include.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,107 @@ const {
} = require("../..");

describe("include", () => {
it("should only include the specified object paths", () => {
const fn = include({ paths: ["a.b", "x"] });
const result = fn({ record: { a: { b: 1, c: 2 }, m: 1, x: { y: 1, z: 2 } } });
eq(result, { a: { b: 1 }, x: { y: 1, z: 2 } });
});
describe("without base path", () => {
it("should only include the specified object paths", () => {
const fn = include({ paths: ["a.b", "x"] });
const result = fn({ record: { a: { b: 1, c: 2 }, m: 1, x: { y: 1, z: 2 } } });
eq(result, { a: { b: 1 }, x: { y: 1, z: 2 } });
});

it("should only include the specified array paths", () => {
const fn = include({ paths: ["a[0].b", "x[1]"] });
const result = fn({ record: { a: [{ b: 1 }, { c: 2 }], m: 1, x: [{ y: 1 }, { z: 2 }] } });
eq(result, { a: { 0: { b: 1 } }, x: { 1: { z: 2 } } });
});
it("should only include the specified array paths", () => {
const fn = include({ paths: ["a[0].b", "x[1]"] });
const result = fn({ record: { a: [{ b: 1 }, { c: 2 }], m: 1, x: [{ y: 1 }, { z: 2 }] } });
eq(result, { a: { 0: { b: 1 } }, x: { 1: { z: 2 } } });
});

it("should ignore missing paths", () => {
const fn = include({ paths: ["a.d"] });
const result = fn({ record: { a: { b: 1, c: 2 } } });
eq(result, {});
});
it("should ignore missing paths", () => {
const fn = include({ paths: ["a.d"] });
const result = fn({ record: { a: { b: 1, c: 2 } } });
eq(result, {});
});

// See https://github.com/jonschlinkert/get-value/issues/29
// it("should work with recursive documents", () => {
// const fn = include({ paths: ["a.b.c"] });
// const a = { c: 2 };
// a.b = a;
// const result = fn({ record: a });
// eq(result, { a: { b: { c: 2 } } });
// });

it("should not mutate original record", () => {
const record = { a: { b: 1, c: 2 }, m: 1, x: { y: 1, z: 2 } };
const fn = include({ paths: ["a.b", "x"] });
fn({ record });
eq(record, { a: { b: 1, c: 2 }, m: 1, x: { y: 1, z: 2 } });
// See https://github.com/jonschlinkert/get-value/issues/29
// it("should work with recursive documents", () => {
// const fn = include({ paths: ["a.b.c"] });
// const a = { c: 2 };
// a.b = a;
// const result = fn({ record: a });
// eq(result, { a: { b: { c: 2 } } });
// });

it("should not mutate original record", () => {
const record = { a: { b: 1, c: 2 }, m: 1, x: { y: 1, z: 2 } };
const fn = include({ paths: ["a.b", "x"] });
fn({ record });
eq(record, { a: { b: 1, c: 2 }, m: 1, x: { y: 1, z: 2 } });
});
});

it("should run the processor the precondition passes", () => {
const fn = include({ precondition: ({ record }) => record.a.b === 1, paths: ["a.b", "x"] });
const result = fn({ record: { a: { b: 1, c: 2 }, m: 1, x: { y: 1, z: 2 } } });
eq(result, { a: { b: 1 }, x: { y: 1, z: 2 } });
describe("with base path", () => {
it("should only include the specified object paths", () => {
const fn = include({ basePath: "r", paths: ["a.b", "x"] });
const result = fn({ record: { r: { a: { b: 1, c: 2 }, m: 1, x: { y: 1, z: 2 } }, keep: 1 } });
eq(result, { r: { a: { b: 1 }, x: { y: 1, z: 2 } }, keep: 1 });
});

it("should only include the specified array paths", () => {
const fn = include({ basePath: "r", paths: ["a[0].b", "x[1]"] });
const result = fn({ record: { r: { a: [{ b: 1 }, { c: 2 }], m: 1, x: [{ y: 1 }, { z: 2 }] }, keep: 1 } });
eq(result, { r: { a: { 0: { b: 1 } }, x: { 1: { z: 2 } } }, keep: 1 });
});

it("should support array base paths", () => {
const fn = include({ basePath: "r[1]", paths: ["a[0].b", "x[1]"] });
const result = fn({ record: { r: [1, { a: [{ b: 1 }, { c: 2 }], m: 1, x: [{ y: 1 }, { z: 2 }] }, 3], keep: 1 } });
eq(result, { r: [1, { a: { 0: { b: 1 } }, x: { 1: { z: 2 } } }, 3], keep: 1 });
});

it("should support array base paths 2", () => {
const fn = include({ basePath: "r", paths: ["[1].a[0].b", "[1].x[1]"] });
const result = fn({ record: { r: [1, { a: [{ b: 1 }, { c: 2 }], m: 1, x: [{ y: 1 }, { z: 2 }] }, 3], keep: 1 } });
eq(result, { r: { 1: { a: { 0: { b: 1 } }, x: { 1: { z: 2 } } } }, keep: 1 });
});

it("should ignore missing paths", () => {
const fn = include({ basePath: "r", paths: ["a.d"] });
const result = fn({ record: { r: { a: { b: 1, c: 2 } }, keep: 1 } });
eq(result, { r: {}, keep: 1 });
});

it("should ignore missing base path", () => {
const fn = include({ basePath: "n", paths: ["a.d"] });
const result = fn({ record: { r: { a: { b: 1, c: 2 } }, keep: 1 } });
eq(result, { r: { a: { b: 1, c: 2 } }, keep: 1 });
});

// See https://github.com/jonschlinkert/get-value/issues/29
// it("should work with recursive documents", () => {
// const fn = include({ basePath: 'r' paths: ["a.b.c"] });
// const a = { c: 2 };
// a.b = a;
// const result = fn({ record: { r: { a }, keep: 1 } });
// eq(result, { r: { a: { b: { c: 2 } }, keep: 1 });
// });

it("should not mutate original record", () => {
const record = { r: { a: { b: 1, c: 2 }, m: 1, x: { y: 1, z: 2 } }, keep: 1 };
const fn = include({ basePath: "r", paths: ["a.b", "x"] });
fn({ record });
eq(record, { r: { a: { b: 1, c: 2 }, m: 1, x: { y: 1, z: 2 } }, keep: 1 });
});
});

it("should bypass the processor the precondition fails", () => {
const fn = include({ precondition: () => false, paths: ["a.b", "x"] });
const result = fn({ record: { a: { b: 1, c: 2 }, m: 1, x: { y: 1, z: 2 } } });
eq(result, { a: { b: 1, c: 2 }, m: 1, x: { y: 1, z: 2 } });
describe("with precondition", () => {
it("should run the processor the precondition passes", () => {
const fn = include({ precondition: ({ record }) => record.a.b === 1, paths: ["a.b", "x"] });
const result = fn({ record: { a: { b: 1, c: 2 }, m: 1, x: { y: 1, z: 2 } } });
eq(result, { a: { b: 1 }, x: { y: 1, z: 2 } });
});

it("should bypass the processor the precondition fails", () => {
const fn = include({ precondition: () => false, paths: ["a.b", "x"] });
const result = fn({ record: { a: { b: 1, c: 2 }, m: 1, x: { y: 1, z: 2 } } });
eq(result, { a: { b: 1, c: 2 }, m: 1, x: { y: 1, z: 2 } });
});
});
});

0 comments on commit 0c4b2d3

Please sign in to comment.