diff --git a/CHANGELOG.md b/CHANGELOG.md index e166b40..5a87166 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 8aa55e4..731538d 100644 --- a/README.md +++ b/README.md @@ -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); ``` @@ -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({ @@ -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" } ``` diff --git a/lib/processors/include.js b/lib/processors/include.js index b63f0d8..d2079f4 100644 --- a/lib/processors/include.js +++ b/lib/processors/include.js @@ -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 }); +} diff --git a/test/processors/include.test.js b/test/processors/include.test.js index 469c8e8..3da49b8 100644 --- a/test/processors/include.test.js +++ b/test/processors/include.test.js @@ -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 } }); + }); }); });