Skip to content
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
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,14 +274,19 @@ Every format used by `createLogger` is also exported for direct use with your ow
| `prettyConsoleFormat` | Applies `colorize` and `timestamp`, then renders logs as coloured YAML using [`yamlify-object`](https://www.npmjs.com/package/yamlify-object). |
| `mapAuditLevelForOtel` | Rewrites the triple-beam `LEVEL` symbol from `audit` to `info` and copies the original onto `logLevel` so custom levels survive OTEL's severity enumeration. |

`redactFormat` paths accept plain keys (`email`, matched at every level), dot-notation paths (`user.email`), and `[*]` array wildcards to iterate every element of an array segment — for example `files[*].name`, `users[*].addresses[*].zip`, or `tags[*]` to redact each element of a primitive array.

Direct usage example:

```ts
import { format, createLogger, transports } from 'winston'
import { redactFormat, serializeErrorFormat } from '@makerx/node-winston'

const logger = createLogger({
format: format.combine(serializeErrorFormat(), redactFormat({ paths: ['user.email'] })),
format: format.combine(
serializeErrorFormat(),
redactFormat({ paths: ['user.email', 'files[*].name'] }),
),
transports: [new transports.Console({ format: format.json() })],
})
```
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@makerx/node-winston",
"version": "2.0.1",
"version": "2.0.2",
"private": false,
"description": "A set of winston formats, console transport and logger creation functions",
"author": "MakerX",
Expand Down
41 changes: 41 additions & 0 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,47 @@ describe('createLogger redactPaths', () => {
const info = transport.logs[0] as { users: { email: string }[] }
expect(info.users.map((u) => u.email)).toEqual(['<redacted>', '<redacted>'])
})

it('redacts every array element using a [*] wildcard path', () => {
const transport = new InMemoryTransport({})
const logger = createLogger({
consoleOptions: { silent: true },
transports: [transport],
redactPaths: ['files[*].name'],
})
logger.info('hello', {
files: [
{ name: 'a.txt', size: 1 },
{ name: 'b.txt', size: 2 },
],
name: 'top',
})
const info = transport.logs[0] as { files: { name: string; size: number }[]; name: string }
expect(info.files.map((f) => f.name)).toEqual(['<redacted>', '<redacted>'])
expect(info.files.map((f) => f.size)).toEqual([1, 2])
// Wildcard is path-scoped, so a top-level `name` is not touched.
expect(info.name).toBe('top')
})

it('supports nested [*] wildcards and bare-array element redaction', () => {
const transport = new InMemoryTransport({})
const logger = createLogger({
consoleOptions: { silent: true },
transports: [transport],
redactPaths: ['users[*].addresses[*].zip', 'tags[*]'],
})
logger.info('hello', {
users: [{ addresses: [{ zip: '1000' }, { zip: '2000' }] }, { addresses: [{ zip: '3000' }] }],
tags: ['secret', 'public'],
})
const info = transport.logs[0] as {
users: { addresses: { zip: string }[] }[]
tags: string[]
}
expect(info.users[0].addresses.map((a) => a.zip)).toEqual(['<redacted>', '<redacted>'])
expect(info.users[1].addresses[0].zip).toBe('<redacted>')
expect(info.tags).toEqual(['<redacted>', '<redacted>'])
})
})

describe('createLogger child loggers', () => {
Expand Down
30 changes: 27 additions & 3 deletions src/redact-values.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,34 @@
import { cloneDeep, forOwn, get, isNil, isObject, set } from 'es-toolkit/compat'

// Expands a single path against the current node, supporting `[*]` to iterate every element of an
// array segment. Without `[*]` it falls back to lodash-style get/set on a dot path.
const applyPath = (current: unknown, path: string, redactedValue: string) => {
const wildcardIdx = path.indexOf('[*]')
if (wildcardIdx === -1) {
if (!isNil(get(current, path))) set(current as object, path, redactedValue)
return
}
const prefix = path.slice(0, wildcardIdx)
const afterWildcard = path.slice(wildcardIdx + 3)
const suffix = afterWildcard.startsWith('.') ? afterWildcard.slice(1) : afterWildcard
const arr = prefix ? get(current, prefix) : current
if (!Array.isArray(arr)) return
arr.forEach((item, i) => {
if (!suffix) {
if (!isNil(item)) arr[i] = redactedValue
} else if (isObject(item)) {
applyPath(item, suffix, redactedValue)
}
})
}

/**
* Recursively replaces values in an object with '<redacted>' for the specified keys. Enumerates arrays and applies the same redaction to elements.
* @param obj The object to redact
* @param keys The keys to redact, can be a dot-separated path (uses es-toolkit/compat's get/set).
* Use dot notation to specify more specific keys.
* @param keys The keys to redact. Each key may be:
* - a plain key (`email`) — matched at every level via recursion
* - a dot-separated path (`user.email`) — uses es-toolkit/compat's get/set
* - a path with `[*]` wildcards (`files[*].name`, `users[*].addresses[*].zip`, `tags[*]`) — iterates each element of the array at that segment
* Key checks are applied at every level of the object via recursion.
* @returns A new object with the specified keys redacted
*/
Expand All @@ -14,7 +38,7 @@ export const redactValuesWith =
(obj: any, ...keys: string[]) => {
return (function redact(current) {
for (const k of keys) {
if (!isNil(get(current, k))) set(current, k, redactedValue)
applyPath(current, k, redactedValue)
}
// isObject returns true for arrays too, so this recurses into both arrays and plain objects
forOwn(current, (value) => {
Expand Down