@darksheep/logger 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +36 -16
  3. package/package.json +7 -5
  4. package/src/create-logger.js +5 -5
  5. package/src/formatters/formatter-console/stack.js +1 -1
  6. package/src/formatters/formatter-console.js +2 -2
  7. package/src/index.js +17 -7
  8. package/src/logger.js +43 -38
  9. package/src/replacer.js +25 -21
  10. package/src/replacers/buffers.js +5 -1
  11. package/src/replacers/error.js +5 -2
  12. package/src/replacers/http-client-request.js +5 -1
  13. package/src/replacers/http-incoming-message.js +7 -2
  14. package/src/replacers/http-server-response.js +5 -1
  15. package/src/replacers/index.js +1 -1
  16. package/src/replacers/long-strings.js +5 -1
  17. package/src/replacers/net-socket.js +5 -1
  18. package/src/replacers/secrets.js +106 -13
  19. package/src/utilities/environment.js +25 -3
  20. package/src/utilities/legacy-secrets.js +36 -0
  21. package/src/utilities/log-types.js +12 -4
  22. package/src/utilities/parse-filters.js +3 -3
  23. package/src/utilities/parse-log-level.js +5 -1
  24. package/src/utilities/resource-usage.js +9 -6
  25. package/types/create-logger.d.ts +3 -2
  26. package/types/formatter.d.ts +2 -2
  27. package/types/formatters/formatter-console/stack.d.ts +15 -11
  28. package/types/formatters/formatter-console.d.ts +2 -2
  29. package/types/formatters/formatter-json.d.ts +1 -1
  30. package/types/index.d.ts +26 -11
  31. package/types/logger.d.ts +150 -61
  32. package/types/replacer.d.ts +16 -16
  33. package/types/replacers/buffers.d.ts +6 -2
  34. package/types/replacers/error.d.ts +13 -12
  35. package/types/replacers/http-client-request.d.ts +6 -2
  36. package/types/replacers/http-incoming-message.d.ts +11 -5
  37. package/types/replacers/http-server-response.d.ts +6 -2
  38. package/types/replacers/index.d.ts +8 -8
  39. package/types/replacers/long-strings.d.ts +6 -2
  40. package/types/replacers/net-socket.d.ts +6 -2
  41. package/types/replacers/secrets.d.ts +8 -4
  42. package/types/stdout-write.d.ts +1 -1
  43. package/types/utilities/colour.d.ts +30 -30
  44. package/types/utilities/environment.d.ts +14 -22
  45. package/types/utilities/json-path.d.ts +1 -1
  46. package/types/utilities/last-callsite.d.ts +1 -1
  47. package/types/utilities/legacy-secrets.d.ts +33 -0
  48. package/types/utilities/log-filters.d.ts +2 -2
  49. package/types/utilities/log-types.d.ts +30 -4
  50. package/types/utilities/parse-filters.d.ts +1 -1
  51. package/types/utilities/parse-log-level.d.ts +6 -2
  52. package/types/utilities/resource-usage.d.ts +2 -2
  53. package/types/utilities/stacktrace.d.ts +15 -15
@@ -1,6 +1,10 @@
1
+ /**
2
+ * @import { Replacer } from '../replacer.js';
3
+ */
4
+
1
5
  import { ServerResponse, STATUS_CODES } from 'node:http';
2
6
 
3
- /** @type {import('../replacer.js').Replacer<ServerResponse>} */
7
+ /** @type {Replacer<ServerResponse>} */
4
8
  export const HttpServerResponseReplacer = {
5
9
  name: 'HttpServerResponse',
6
10
  shouldReplace: (input) => input instanceof ServerResponse,
@@ -5,4 +5,4 @@ export { HttpIncomingMessageReplacer } from './http-incoming-message.js';
5
5
  export { HttpServerResponseReplacer } from './http-server-response.js';
6
6
  export { LongStringReplacer } from './long-strings.js';
7
7
  export { NetSocketReplacer } from './net-socket.js';
8
- export { SecretDelete, SecretObscure } from './secrets.js';
8
+ export { SecretRedact, SecretRemove } from './secrets.js';
@@ -1,6 +1,10 @@
1
+ /**
2
+ * @import { Replacer } from '../replacer.js';
3
+ */
4
+
1
5
  import { environment } from '../utilities/environment.js';
2
6
 
3
- /** @type {import('../replacer.js').Replacer<string>} */
7
+ /** @type {Replacer<string>} */
4
8
  export const LongStringReplacer = {
5
9
  name: 'LongString',
6
10
  shouldReplace: (input) => (
@@ -1,6 +1,10 @@
1
+ /**
2
+ * @import { Replacer } from '../replacer.js';
3
+ */
4
+
1
5
  import { Socket } from 'node:net';
2
6
 
3
- /** @type {import('../replacer.js').Replacer<Socket>} */
7
+ /** @type {Replacer<Socket>} */
4
8
  export const NetSocketReplacer = {
5
9
  name: 'NetSocket',
6
10
  shouldReplace: (input) => input instanceof Socket,
@@ -1,30 +1,110 @@
1
+ /**
2
+ * @import { Replacer } from '../replacer.js';
3
+ */
4
+
1
5
  import { environment } from '../utilities/environment.js';
2
6
  import { nodesToPath } from '../utilities/json-path.js';
3
7
 
4
- /** @type {import('../replacer.js').Replacer} */
5
- export const SecretDelete = {
6
- name: 'SecretDelete',
8
+ /**
9
+ * Whether the subtree at `value` actually contains a node whose path is
10
+ * exempted from removal. Used to decide whether to descend into a node we
11
+ * would otherwise remove wholesale: we only keep the container when a real
12
+ * exempted descendant lives beneath it, so no empty container is left behind.
13
+ *
14
+ * Descendant paths are tested against the compiled exemption patterns, so
15
+ * wildcards behave exactly as they do elsewhere.
16
+ *
17
+ * @param {unknown} value - The node whose descendants to inspect.
18
+ * @param {string} path - The dotted path of `value`.
19
+ * @param {RegExp[]} blocked - The compiled exemption patterns.
20
+ * @param {WeakSet<object>} [seen] - Guards against circular references.
21
+ * @returns {boolean}
22
+ */
23
+ function hasExemptedDescendant(value, path, blocked, seen = new WeakSet()) {
24
+ if (value === null || typeof value !== 'object' || seen.has(value)) {
25
+ return false;
26
+ }
27
+ seen.add(value);
28
+
29
+ const record = /** @type {Record<string, unknown>} */ (value);
30
+ for (const [ key, child ] of Object.entries(record)) {
31
+ const childPath = path === '' ? key : `${path}.${key}`;
32
+
33
+ for (const filter of blocked) {
34
+ if (filter.test(childPath)) {
35
+ return true;
36
+ }
37
+ }
38
+
39
+ if (hasExemptedDescendant(child, childPath, blocked, seen)) {
40
+ return true;
41
+ }
42
+ }
43
+
44
+ return false;
45
+ }
46
+
47
+ /** @type {Replacer} */
48
+ export const SecretRemove = {
49
+ name: 'SecretRemove',
7
50
  stopHere: true,
8
- shouldReplace: (_, original) => {
51
+ shouldReplace: (value, original) => {
9
52
  if (original == null) {
10
53
  return false;
11
54
  }
12
55
 
13
56
  const path = original.join('.');
14
- for (const filter of environment.secretFilters.blocked) {
57
+
58
+ let directlyRemoved = false;
59
+ for (const filter of environment.removeFilters.allowed) {
15
60
  if (filter.test(path)) {
16
- return true;
61
+ directlyRemoved = true;
62
+ break;
17
63
  }
18
64
  }
19
65
 
20
- return false;
66
+ let ancestorRemoved = false;
67
+ if (directlyRemoved === false) {
68
+ for (let i = 1; i < original.length; i++) {
69
+ const ancestorPath = original.slice(0, i).join('.');
70
+ for (const filter of environment.removeFilters.allowed) {
71
+ if (filter.test(ancestorPath)) {
72
+ ancestorRemoved = true;
73
+ break;
74
+ }
75
+ }
76
+ if (ancestorRemoved === true) {
77
+ break;
78
+ }
79
+ }
80
+ }
81
+
82
+ if (directlyRemoved === false && ancestorRemoved === false) {
83
+ return false;
84
+ }
85
+
86
+ for (const filter of environment.removeFilters.blocked) {
87
+ if (filter.test(path)) {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ // This node is slated for removal (directly or via an ancestor). Only keep
93
+ // it when a real exempted descendant lives beneath it, so traversal
94
+ // continues into its children; otherwise remove the whole subtree without
95
+ // leaving an empty container behind.
96
+ if (hasExemptedDescendant(value, path, environment.removeFilters.blocked)) {
97
+ return false;
98
+ }
99
+
100
+ return true;
21
101
  },
22
102
  replace: () => null,
23
103
  };
24
104
 
25
- /** @type {import('../replacer.js').Replacer} */
26
- export const SecretObscure = {
27
- name: 'SecretObscure',
105
+ /** @type {Replacer} */
106
+ export const SecretRedact = {
107
+ name: 'SecretRedact',
28
108
  stopHere: true,
29
109
  shouldReplace: (_, original) => {
30
110
  if (original == null) {
@@ -32,13 +112,26 @@ export const SecretObscure = {
32
112
  }
33
113
 
34
114
  const path = original.join('.');
35
- for (const filter of environment.secretFilters.allowed) {
115
+
116
+ let matched = false;
117
+ for (const filter of environment.redactFilters.allowed) {
36
118
  if (filter.test(path)) {
37
- return true;
119
+ matched = true;
120
+ break;
38
121
  }
39
122
  }
40
123
 
41
- return false;
124
+ if (matched === false) {
125
+ return false;
126
+ }
127
+
128
+ for (const filter of environment.redactFilters.blocked) {
129
+ if (filter.test(path)) {
130
+ return false;
131
+ }
132
+ }
133
+
134
+ return true;
42
135
  },
43
136
  replace: (_, path) => {
44
137
  if (path == null) {
@@ -1,5 +1,6 @@
1
1
  import { getTypedEnv } from '@darksheep/environment';
2
2
 
3
+ import { resolveSecretFilters } from './legacy-secrets.js';
3
4
  import { parseFilters } from './parse-filters.js';
4
5
  import { parseLogLevel } from './parse-log-level.js';
5
6
 
@@ -10,8 +11,10 @@ const {
10
11
  LOG_LEVEL,
11
12
  LOG_MAX_LENGTH = 1024,
12
13
  LOG_MEMORY = false,
13
- LOG_SECRETS,
14
14
  LOG_RELATIVE_TO,
15
+ LOG_SECRETS,
16
+ LOG_SECRET_REDACT,
17
+ LOG_SECRET_REMOVE,
15
18
  NODE_ENV = 'production',
16
19
  } = getTypedEnv({
17
20
  LOG_CALLSITES: '?boolean',
@@ -20,11 +23,29 @@ const {
20
23
  LOG_LEVEL: '?string',
21
24
  LOG_MAX_LENGTH: '?number',
22
25
  LOG_MEMORY: '?boolean',
23
- LOG_SECRETS: '?string',
24
26
  LOG_RELATIVE_TO: '?string',
27
+ LOG_SECRETS: '?string',
28
+ LOG_SECRET_REDACT: '?string',
29
+ LOG_SECRET_REMOVE: '?string',
25
30
  NODE_ENV: '?string',
26
31
  });
27
32
 
33
+ // `LOG_SECRETS` is the pre-split control; map it onto the redact/remove vars
34
+ // so existing configs keep working. The split vars win when set.
35
+ if (LOG_SECRETS != null) {
36
+ process.emitWarning(
37
+ 'LOG_SECRETS is deprecated; use LOG_SECRET_REDACT (obscure values) and ' +
38
+ 'LOG_SECRET_REMOVE (delete keys) instead.',
39
+ { code: 'DEP_LOG_SECRETS', type: 'DeprecationWarning' },
40
+ );
41
+ }
42
+
43
+ const secretFilters = resolveSecretFilters({
44
+ secrets: LOG_SECRETS,
45
+ redact: LOG_SECRET_REDACT,
46
+ remove: LOG_SECRET_REMOVE,
47
+ });
48
+
28
49
  export const environment = {
29
50
  includeCallsite: LOG_CALLSITES,
30
51
  includeCpuUsage: LOG_CPU,
@@ -34,7 +55,8 @@ export const environment = {
34
55
  isTesting: NODE_ENV === 'test',
35
56
  logFilters: parseFilters(LOG_FILTERS, [ /.*/u ]),
36
57
  logLevel: parseLogLevel(LOG_LEVEL, NODE_ENV),
37
- secretFilters: parseFilters(LOG_SECRETS),
58
+ redactFilters: secretFilters.redactFilters,
38
59
  relativeTo: LOG_RELATIVE_TO,
60
+ removeFilters: secretFilters.removeFilters,
39
61
  stringMaxLength: LOG_MAX_LENGTH,
40
62
  };
@@ -0,0 +1,36 @@
1
+ import { parseFilters } from './parse-filters.js';
2
+
3
+ /**
4
+ * @typedef {{ allowed: RegExp[], blocked: RegExp[] }} Filters
5
+ */
6
+
7
+ /**
8
+ * Resolves the redact and remove filters, mapping the legacy `LOG_SECRETS`
9
+ * control onto them when the split vars are not set.
10
+ *
11
+ * In `LOG_SECRETS` a bare pattern obscured (redacted) the value and a
12
+ * `-`-prefixed pattern removed the key -- which is exactly the `allowed` /
13
+ * `blocked` split that `parseFilters` already produces. So the legacy
14
+ * `allowed` patterns become the redact set and the legacy `blocked` patterns
15
+ * become the remove set (neither carried exemptions). Whenever the matching
16
+ * split var is set it wins and the legacy value is ignored for that half.
17
+ *
18
+ * @param {{
19
+ * secrets?: string | undefined;
20
+ * redact?: string | undefined;
21
+ * remove?: string | undefined;
22
+ * }} options - The raw environment values.
23
+ * @returns {{ redactFilters: Filters, removeFilters: Filters }}
24
+ */
25
+ export function resolveSecretFilters({ secrets, redact, remove }) {
26
+ const legacy = parseFilters(secrets);
27
+
28
+ return {
29
+ redactFilters: redact == null
30
+ ? { allowed: legacy.allowed, blocked: [] }
31
+ : parseFilters(redact),
32
+ removeFilters: remove == null
33
+ ? { allowed: legacy.blocked, blocked: [] }
34
+ : parseFilters(remove),
35
+ };
36
+ }
@@ -23,11 +23,19 @@ export const LogNames = Object.fromEntries(
23
23
  Object.entries(LogLevels).map(([ k, v ]) => [ v, k ]),
24
24
  );
25
25
 
26
- /** @typedef {keyof LogLevels} LogLevelNames */
27
- /** @typedef {LogLevels[LogLevelNames]} LogLevel */
26
+ /**
27
+ * @typedef {keyof LogLevels} LogLevelNames
28
+ */
29
+ /**
30
+ * @typedef {LogLevels[LogLevelNames]} LogLevel
31
+ */
28
32
 
29
- /** @typedef {import('./stacktrace.js').Callsite} Callsite */
30
- /** @typedef {import('../replacers/error.js').NormalisedError} NormalisedError */
33
+ /**
34
+ * @typedef {import('./stacktrace.js').Callsite} Callsite
35
+ */
36
+ /**
37
+ * @typedef {import('../replacers/error.js').NormalisedError} NormalisedError
38
+ */
31
39
 
32
40
  /**
33
41
  * @typedef {Object} LogInternal
@@ -29,12 +29,12 @@ export function parseFilters(filter = '', fallback = []) {
29
29
 
30
30
  let regex = channel
31
31
  .replaceAll(/[\s#$()*+,\-.?[\\\]^{|}]/gu, String.raw`\$&`)
32
- .replaceAll(String.raw`\*`, multiLevelWildcard.source)
33
- .replaceAll(String.raw`\+`, oneLevelWildcard.source);
32
+ .replaceAll(String.raw`\*`, () => multiLevelWildcard.source)
33
+ .replaceAll(String.raw`\+`, () => oneLevelWildcard.source);
34
34
 
35
35
  // Allow for multi level wildcards at the start of a filter
36
36
  if (regex.startsWith(String.raw`.*\.`)) {
37
- regex = `(?:.*\\.)?${regex.slice(4)}`;
37
+ regex = String.raw`(?:.*\.)?${regex.slice(4)}`;
38
38
  }
39
39
 
40
40
  const pattern = new RegExp(`^${regex}$`);
@@ -1,10 +1,14 @@
1
+ /**
2
+ * @import { LogLevel } from './log-types.js';
3
+ */
4
+
1
5
  import { LogLevels } from './log-types.js';
2
6
 
3
7
  /**
4
8
  * Convert a given string to a LogLevel.
5
9
  * @param {string} [input] - The string to convert.
6
10
  * @param {string} [node] - The current NODE_ENV.
7
- * @returns {import('./log-types.js').LogLevel}
11
+ * @returns {LogLevel}
8
12
  */
9
13
  export function parseLogLevel(input, node) {
10
14
  switch (input) {
@@ -14,8 +14,11 @@ export function getMemoryUsage() {
14
14
  };
15
15
  }
16
16
 
17
- let lastCalled = performance.now();
18
- let lastValue = process.cpuUsage();
17
+ /** Previous sample, kept on one object so `getCPUUsage` mutates no bindings. */
18
+ const last = {
19
+ called: performance.now(),
20
+ value: process.cpuUsage(),
21
+ };
19
22
 
20
23
  /**
21
24
  * Get the callsite that we think is outside the package.
@@ -30,12 +33,12 @@ export function getCPUUsage() {
30
33
  /**
31
34
  * Time difference in microseconds.
32
35
  */
33
- const elapsed = Math.floor(1000 * (currentCalled - lastCalled));
36
+ const elapsed = Math.floor(1000 * (currentCalled - last.called));
34
37
 
35
- lastCalled = currentCalled;
36
- lastValue = process.cpuUsage(lastValue);
38
+ last.called = currentCalled;
39
+ last.value = process.cpuUsage(last.value);
37
40
 
38
- const { system, user } = lastValue;
41
+ const { system, user } = last.value;
39
42
  const total = system + user;
40
43
 
41
44
  return { elapsed, total, system, user };
@@ -1,5 +1,6 @@
1
+ import { Logger } from './logger.js';
1
2
  /**
2
3
  * @param {string} [channel] - The logging channel.
3
- * @returns {import('./logger.js').Logger}
4
+ * @returns {Logger}
4
5
  */
5
- export function createLogger(channel?: string): import("./logger.js").Logger;
6
+ export declare function createLogger(channel?: string): Logger;
@@ -1,6 +1,6 @@
1
+ export type Formatter = (logEntry: import('./utilities/log-types.js').LogEntry) => string;
1
2
  /**
2
3
  * @typedef {(logEntry: import('./utilities/log-types.js').LogEntry) => string} Formatter
3
4
  */
4
5
  /** @type {Formatter} */
5
- export const formatter: Formatter;
6
- export type Formatter = (logEntry: import("./utilities/log-types.js").LogEntry) => string;
6
+ export declare const formatter: Formatter;
@@ -1,18 +1,22 @@
1
1
  /**
2
- * @param {Callsite[] | string | undefined} stack - The stack trace to format.
3
- * @param {number} indent - The indent to render from.
4
- * @returns {string}
2
+ * @import { ColourOptions } from '../../utilities/colour.js';
3
+ * @import { Callsite } from '../../utilities/log-types.js';
5
4
  */
6
- export function formatStack(stack: Callsite[] | string | undefined, indent: number): string;
5
+ import type { ColourOptions } from '../../utilities/colour.js';
6
+ import type { Callsite } from '../../utilities/log-types.js';
7
7
  export type HideOption = {
8
8
  hide?: boolean;
9
9
  };
10
10
  export type LineOptions = (HideOption & {
11
- Line?: Omit<ColourOptions, "reset">;
12
- FunctionName?: Omit<ColourOptions, "reset"> & HideOption;
13
- FilePath?: Omit<ColourOptions, "reset">;
14
- LineNumber?: Omit<ColourOptions, "reset"> & HideOption;
15
- ColumnNumber?: Omit<ColourOptions, "reset"> & HideOption;
11
+ Line?: Omit<ColourOptions, 'reset'>;
12
+ FunctionName?: Omit<ColourOptions, 'reset'> & HideOption;
13
+ FilePath?: Omit<ColourOptions, 'reset'>;
14
+ LineNumber?: Omit<ColourOptions, 'reset'> & HideOption;
15
+ ColumnNumber?: Omit<ColourOptions, 'reset'> & HideOption;
16
16
  });
17
- import type { Callsite } from '../../utilities/log-types.js';
18
- import type { ColourOptions } from '../../utilities/colour.js';
17
+ /**
18
+ * @param {Callsite[] | string | undefined} stack - The stack trace to format.
19
+ * @param {number} indent - The indent to render from.
20
+ * @returns {string}
21
+ */
22
+ export declare function formatStack(stack: Callsite[] | string | undefined, indent: number): string;
@@ -1,6 +1,6 @@
1
+ import type { LogEntry } from '../utilities/log-types.js';
1
2
  /**
2
3
  * @param {LogEntry} logEntry - The Log entry which we're going to convert to a splatted string.
3
4
  * @returns {string}
4
5
  */
5
- export function formatterConsole(logEntry: LogEntry): string;
6
- import type { LogEntry } from '../utilities/log-types.js';
6
+ export declare function formatterConsole(logEntry: LogEntry): string;
@@ -2,4 +2,4 @@
2
2
  * @param {import('../utilities/log-types.js').LogEntry} logEntry - The Log entry which we're going to convert to JSON.
3
3
  * @returns {string}
4
4
  */
5
- export function formatterJson(logEntry: import("../utilities/log-types.js").LogEntry): string;
5
+ export declare function formatterJson(logEntry: import('../utilities/log-types.js').LogEntry): string;
package/types/index.d.ts CHANGED
@@ -1,11 +1,26 @@
1
- export { createLogger } from "./create-logger.js";
2
- export const logger: import("./logger.js").Logger;
3
- export { Logger } from "./logger.js";
4
- export { environment } from "./utilities/environment.js";
5
- export { LogLevels } from "./utilities/log-types.js";
6
- export type LogContext = import("./utilities/log-types.js").LogContext;
7
- export type LogEntry = import("./utilities/log-types.js").LogEntry;
8
- export type LogLevel = import("./utilities/log-types.js").LogLevel;
9
- export type Formatter = import("./formatter.js").Formatter;
10
- export type Replacer = import("./replacer.js").Replacer;
11
- export { BufferReplacer, ErrorReplacer, HttpClientRequestReplacer, HttpIncomingMessageReplacer, HttpServerResponseReplacer, LongStringReplacer, NetSocketReplacer, SecretDelete, SecretObscure } from "./replacers/index.js";
1
+ export type LogContext = import('./utilities/log-types.js').LogContext;
2
+ export type LogEntry = import('./utilities/log-types.js').LogEntry;
3
+ export type LogLevel = import('./utilities/log-types.js').LogLevel;
4
+ export type Formatter = import('./formatter.js').Formatter;
5
+ export type Replacer = import('./replacer.js').Replacer;
6
+ /**
7
+ * @typedef {import('./utilities/log-types.js').LogContext} LogContext
8
+ */
9
+ /**
10
+ * @typedef {import('./utilities/log-types.js').LogEntry} LogEntry
11
+ */
12
+ /**
13
+ * @typedef {import('./utilities/log-types.js').LogLevel} LogLevel
14
+ */
15
+ /**
16
+ * @typedef {import('./formatter.js').Formatter} Formatter
17
+ */
18
+ /**
19
+ * @typedef {import('./replacer.js').Replacer} Replacer
20
+ */
21
+ export { createLogger } from './create-logger.js';
22
+ export declare const logger: import("./logger.js").Logger;
23
+ export { Logger } from './logger.js';
24
+ export { BufferReplacer, ErrorReplacer, HttpClientRequestReplacer, HttpIncomingMessageReplacer, HttpServerResponseReplacer, LongStringReplacer, NetSocketReplacer, SecretRedact, SecretRemove, } from './replacers/index.js';
25
+ export { environment } from './utilities/environment.js';
26
+ export { LogLevels } from './utilities/log-types.js';