@darksheep/logger 1.0.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 (61) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +191 -0
  3. package/package.json +44 -0
  4. package/src/create-logger.js +36 -0
  5. package/src/formatter.js +14 -0
  6. package/src/formatters/formatter-console.js +243 -0
  7. package/src/formatters/formatter-json.js +7 -0
  8. package/src/index.js +25 -0
  9. package/src/logger.js +369 -0
  10. package/src/replacer.js +68 -0
  11. package/src/replacers/buffers.js +18 -0
  12. package/src/replacers/error.js +40 -0
  13. package/src/replacers/http-client-request.js +18 -0
  14. package/src/replacers/http-incoming-message.js +26 -0
  15. package/src/replacers/http-server-response.js +16 -0
  16. package/src/replacers/index.js +8 -0
  17. package/src/replacers/long-strings.js +11 -0
  18. package/src/replacers/net-socket.js +21 -0
  19. package/src/replacers/secrets.js +50 -0
  20. package/src/stdout-write.js +7 -0
  21. package/src/utilities/colour.js +152 -0
  22. package/src/utilities/environment.js +61 -0
  23. package/src/utilities/json-path.js +17 -0
  24. package/src/utilities/last-callsite.js +11 -0
  25. package/src/utilities/log-filters.js +22 -0
  26. package/src/utilities/log-types.js +46 -0
  27. package/src/utilities/parse-filters.js +47 -0
  28. package/src/utilities/parse-log-level.js +32 -0
  29. package/src/utilities/stacktrace.js +57 -0
  30. package/types/assert.zod.d.ts +18 -0
  31. package/types/create-logger.d.ts +5 -0
  32. package/types/formatter.d.ts +6 -0
  33. package/types/formatters/formatter-console.d.ts +9 -0
  34. package/types/formatters/formatter-json.d.ts +5 -0
  35. package/types/index.d.ts +11 -0
  36. package/types/logger.d.ts +184 -0
  37. package/types/logger.test.d.ts +1 -0
  38. package/types/replacer.d.ts +45 -0
  39. package/types/replacer.test.d.ts +1 -0
  40. package/types/replacers/buffers.d.ts +2 -0
  41. package/types/replacers/buffers.test.d.ts +1 -0
  42. package/types/replacers/error.d.ts +37 -0
  43. package/types/replacers/error.test.d.ts +1 -0
  44. package/types/replacers/http-client-request.d.ts +3 -0
  45. package/types/replacers/http-client-request.test.d.ts +1 -0
  46. package/types/replacers/http-incoming-message.d.ts +15 -0
  47. package/types/replacers/http-server-response.d.ts +3 -0
  48. package/types/replacers/index.d.ts +8 -0
  49. package/types/replacers/long-strings.d.ts +2 -0
  50. package/types/replacers/net-socket.d.ts +3 -0
  51. package/types/replacers/secrets.d.ts +4 -0
  52. package/types/stdout-write.d.ts +5 -0
  53. package/types/utilities/colour.d.ts +76 -0
  54. package/types/utilities/environment.d.ts +25 -0
  55. package/types/utilities/json-path.d.ts +5 -0
  56. package/types/utilities/last-callsite.d.ts +9 -0
  57. package/types/utilities/log-filters.d.ts +12 -0
  58. package/types/utilities/log-types.d.ts +126 -0
  59. package/types/utilities/parse-filters.d.ts +9 -0
  60. package/types/utilities/parse-log-level.d.ts +7 -0
  61. package/types/utilities/stacktrace.d.ts +59 -0
@@ -0,0 +1,152 @@
1
+ const colours = Object.freeze({
2
+ black: '0',
3
+ red: '1',
4
+ green: '2',
5
+ yellow: '3',
6
+ blue: '4',
7
+ magenta: '5',
8
+ cyan: '6',
9
+ white: '7',
10
+ });
11
+
12
+ const effects = Object.freeze({
13
+ bold: [ 1, 2 ],
14
+ faint: [ 2, 2 ],
15
+ italic: [ 3, 3 ],
16
+ underline: [ 4, 4 ],
17
+ // blink: [ 5, 5 ],
18
+ });
19
+
20
+ const FOREGROUND = 3;
21
+ const BACKGROUND = 4;
22
+
23
+ /**
24
+ * @typedef {keyof typeof colours} ColourNames
25
+ * @typedef {'bright' | 'dim'} ColourMode
26
+ * @typedef {keyof typeof effects} ColourEffects
27
+ */
28
+
29
+ /**
30
+ * @typedef {Object} ColourOptions
31
+ * @property {ColourNames} [foreground] The foreground colour name
32
+ * @property {ColourMode} [foregroundMode] The foreground colour mode
33
+ * @property {ColourNames} [background] The background colour name
34
+ * @property {ColourMode} [backgroundMode] The background colour mode
35
+ * @property {ColourEffects[]} [effects] The string effents
36
+ * @property {boolean | Omit<ColourOptions, 'reset'>} [reset] How to handle the string termination
37
+ */
38
+
39
+ /**
40
+ * @param {ColourNames} colour [description]
41
+ * @param {typeof FOREGROUND | typeof BACKGROUND} base [description]
42
+ * @param {ColourMode} [mode] [description]
43
+ * @returns {{ colour: typeof colours[ColourNames], mode: number }}
44
+ */
45
+ function getColour(colour, base, mode) {
46
+ const colourCode = colours[colour];
47
+
48
+ if (mode === 'bright') {
49
+ return {
50
+ colour: colourCode,
51
+ mode: base + 6,
52
+ };
53
+ }
54
+
55
+ return {
56
+ colour: colourCode,
57
+ mode: base,
58
+ };
59
+ }
60
+
61
+ /**
62
+ * @param {ColourOptions} options How to colour the string
63
+ * @returns {{ prefix: string[], suffix: string[] }}
64
+ */
65
+ function computeColours(options) {
66
+ const prefix = new Set();
67
+ const suffix = new Set();
68
+
69
+ if (options?.foreground != null) {
70
+ const { colour, mode } = getColour(
71
+ options.foreground,
72
+ FOREGROUND,
73
+ options.foregroundMode,
74
+ );
75
+
76
+ if (colour != null) {
77
+ prefix.add(`\x1B[${mode}${colour}m`);
78
+ suffix.add(`\x1B[${FOREGROUND}9m`);
79
+ }
80
+ }
81
+
82
+ if (options?.background != null) {
83
+ const { colour, mode } = getColour(
84
+ options.background,
85
+ BACKGROUND,
86
+ options.backgroundMode,
87
+ );
88
+
89
+ if (colour != null) {
90
+ prefix.add(`\x1B[${mode}${colour}m`);
91
+ suffix.add(`\x1B[${BACKGROUND}9m`);
92
+ }
93
+ }
94
+
95
+ if (options?.effects instanceof Array) {
96
+ for (const effect of options.effects) {
97
+ if (Object.hasOwn(effects, effect)) {
98
+ const [ start, end ] = effects[effect];
99
+ prefix.add(`\x1B[${start}m`);
100
+ suffix.add(`\x1B[2${end}m`);
101
+ }
102
+ }
103
+ }
104
+
105
+ return {
106
+ prefix: [ ...prefix ],
107
+ suffix: [ ...suffix ],
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Should colours be used in the output
113
+ * @returns {boolean}
114
+ */
115
+ export function shouldColour() {
116
+ if (
117
+ process?.stdout?.isTTY === false ||
118
+ typeof process?.stdout?.hasColors !== 'function'
119
+ ) {
120
+ return false;
121
+ }
122
+
123
+ return process.stdout.hasColors();
124
+ }
125
+
126
+ /**
127
+ * Colour a string
128
+ * @param {string} string The string to colour
129
+ * @param {ColourOptions} options The options to use to colour the string
130
+ * @returns {string}
131
+ */
132
+ export function colourString(string, options) {
133
+ if (shouldColour() === false) {
134
+ return string;
135
+ }
136
+
137
+ const { prefix, suffix } = computeColours(options);
138
+
139
+ if (options?.reset === false) {
140
+ return `${prefix.join('')}${string}`;
141
+ }
142
+
143
+ if (
144
+ typeof options?.reset === 'string' ||
145
+ options?.reset instanceof Object
146
+ ) {
147
+ const { prefix: reset } = computeColours(options.reset);
148
+ return [ ...prefix, string, ...suffix, ...reset ].join('');
149
+ }
150
+
151
+ return [ ...prefix, string, ...suffix ].join('');
152
+ }
@@ -0,0 +1,61 @@
1
+ import { parseLogLevel } from './parse-log-level.js';
2
+ import { parseFilters } from './parse-filters.js';
3
+
4
+ /**
5
+ * @param {undefined | string} input The env var to check for booleaness
6
+ * @param {boolean} fallback The fallback bool
7
+ * @returns {boolean}
8
+ */
9
+ function parseBoolean(input, fallback) {
10
+ if (input === 'true' || input === '1') {
11
+ return true;
12
+ }
13
+
14
+ if (input === 'false' || input === '0') {
15
+ return false;
16
+ }
17
+
18
+ return fallback;
19
+ }
20
+
21
+ /**
22
+ * @param {undefined | string} input The env var to check to see if its an int
23
+ * @param {number} fallback The fallback number
24
+ * @returns {number}
25
+ */
26
+ function parseInt(input, fallback) {
27
+ if (typeof input === 'string') {
28
+ const number = Number.parseInt(input, 10);
29
+
30
+ if (Number.isNaN(number) === false) {
31
+ return number;
32
+ }
33
+ }
34
+
35
+ return fallback;
36
+ }
37
+
38
+ const isDevelopment = process.env.NODE_ENV === 'development';
39
+ const isTesting = process.env.NODE_ENV === 'test';
40
+ const isProduction = process.env.NODE_ENV === 'production';
41
+
42
+ const logLevel = parseLogLevel(
43
+ process.env.LOG_LEVEL,
44
+ process.env.NODE_ENV,
45
+ );
46
+ const logFilters = parseFilters(process.env.LOG_FILTERS, [ /.*/ ]);
47
+ const secretFilters = parseFilters(process.env.LOG_SECRETS);
48
+ const includeCallsite = parseBoolean(process.env.LOG_CALLSITES, false);
49
+ const stringMaxLength = parseInt(process.env.LOG_MAX_LENGTH, 1024);
50
+
51
+ export const environment = {
52
+ isDevelopment,
53
+ isTesting,
54
+ isProduction,
55
+
56
+ logLevel,
57
+ logFilters,
58
+ secretFilters,
59
+ includeCallsite,
60
+ stringMaxLength,
61
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @param {string} path The json path component to escape
3
+ * @returns {string}
4
+ */
5
+ function jsonPathEscape(path) {
6
+ return path
7
+ .replace(/\b~/g, '~0')
8
+ .replace(/\//g, '~1');
9
+ }
10
+
11
+ /**
12
+ * @param {string[]} nodes The nodes to the element
13
+ * @returns {string}
14
+ */
15
+ export function nodesToPath(nodes) {
16
+ return `#/${nodes.map(jsonPathEscape).join('/')}`;
17
+ }
@@ -0,0 +1,11 @@
1
+ import { parseStack } from './stacktrace.js';
2
+
3
+ /**
4
+ * Get the callsite that we think is outside the package
5
+ * @returns {{ file: string, line: number, column: number } | void}
6
+ */
7
+ export function getLastCallsite() {
8
+ const { stack } = new Error('stacktrace');
9
+
10
+ return parseStack(stack).at(3);
11
+ }
@@ -0,0 +1,22 @@
1
+ import { environment } from './environment.js';
2
+
3
+ /**
4
+ * Check to see if {loggerLevel} should get logged
5
+ * @param {import('./log-types.js').LogLevel} level The log level to check
6
+ * @returns {boolean}
7
+ */
8
+ export function checkLogLevel(level) {
9
+ return level <= environment.logLevel;
10
+ }
11
+
12
+ /**
13
+ * Check to see if a message with {channel} should get logged
14
+ * @param {string} [channel] The channel to check
15
+ * @returns {boolean}
16
+ */
17
+ export function checkLogFilters(channel = '') {
18
+ return (
19
+ environment.logFilters.allowed.some((filter) => filter.test(channel)) === true &&
20
+ environment.logFilters.blocked.some((filter) => filter.test(channel)) === false
21
+ );
22
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @typedef {Object} LogLevels
3
+ * @property {0} critical A crucial part of the application is not working
4
+ * @property {1} error A non critical operation fails
5
+ * @property {2} warning An operation might fail in the future
6
+ * @property {3} notice Information about events that may be unusual
7
+ * @property {4} info Information about successful operations
8
+ * @property {5} debug Information that is unlikely to help in production
9
+ * @property {6} silly Information to help resolve complex logic issues
10
+ */
11
+ /** @type {LogLevels} */
12
+ export const LogLevels = {
13
+ critical: 0,
14
+ error: 1,
15
+ warning: 2,
16
+ notice: 3,
17
+ info: 4,
18
+ debug: 5,
19
+ silly: 6,
20
+ };
21
+
22
+ export const LogNames = Object.fromEntries(
23
+ Object.entries(LogLevels).map(([ k, v ]) => [ v, k ]),
24
+ );
25
+
26
+ /** @typedef {keyof LogLevels} LogLevelNames */
27
+ /** @typedef {LogLevels[LogLevelNames]} LogLevel */
28
+
29
+ /** @typedef {import('./stacktrace.js').Callsite} Callsite */
30
+ /** @typedef {import('../replacers/error.js').NormalisedError} NormalisedError */
31
+
32
+ /**
33
+ * @typedef {Object} LogInternal
34
+ * @property {Date} [timestamp] A timestamp for the log message
35
+ * @property {string} [channel] A channel to bind to the log
36
+ * @property {LogLevel} [level] The log level
37
+ * @property {string} [message] A message to use
38
+ * @property {string | Callsite[]} [stack] The stack trace bound to the log
39
+ * @property {NormalisedError | Error} [cause] A child error for the core error
40
+ * @property {string} [trail] The trail id bound to the callsite
41
+ */
42
+
43
+ /**
44
+ * @typedef {LogInternal & Record<string, unknown>} LogContext
45
+ * @typedef {{ level: LogLevel, message: string } & LogInternal & Record<string, unknown>} LogEntry
46
+ */
@@ -0,0 +1,47 @@
1
+ const matcher = /^(-?)(?!-)(.+)$/;
2
+ const oneLevelWildcard = /(?:(?!\.).)*/;
3
+ const multiLevelWildcard = /.*/;
4
+
5
+ /**
6
+ * @param {string} [filter] The filter string
7
+ * @param {RegExp[]} [fallback] Fallback to the following allowed regexp
8
+ * @returns {{ allowed: RegExp[], blocked: RegExp[] }}
9
+ */
10
+ export function parseFilters(filter = '', fallback = []) {
11
+ const filters = filter.split(',');
12
+
13
+ const allowed = [];
14
+ const blocked = [];
15
+
16
+ for (const filter of filters) {
17
+ const [ , type, channel ] = filter.match(matcher) ?? [];
18
+
19
+ if (channel == null || channel === '') {
20
+ continue;
21
+ }
22
+
23
+ let regex = channel
24
+ .replace(/[\s#$()*+,.?[\\\]^{|}-]/g, '\\$&')
25
+ .replace(/\\\*/g, multiLevelWildcard.source)
26
+ .replace(/\\\+/g, oneLevelWildcard.source);
27
+
28
+ // Allow for multi level wildcards at the start of a filter
29
+ if (regex.startsWith('.*\\.')) {
30
+ regex = `(?:.*\\.)?${regex.slice(4)}`;
31
+ }
32
+
33
+ const pattern = new RegExp(`^${regex}$`);
34
+
35
+ if (type === '-') {
36
+ blocked.push(pattern);
37
+ } else {
38
+ allowed.push(pattern);
39
+ }
40
+ }
41
+
42
+ if (allowed.length === 0) {
43
+ allowed.push(...fallback);
44
+ }
45
+
46
+ return { allowed, blocked };
47
+ }
@@ -0,0 +1,32 @@
1
+ import { LogLevels } from './log-types.js';
2
+
3
+ /**
4
+ * Convert a given string to a LogLevel
5
+ * @param {string} [input] The string to convert
6
+ * @param {string} [node] The current NODE_ENV
7
+ * @returns {import('./log-types.js').LogLevel}
8
+ */
9
+ export function parseLogLevel(input, node) {
10
+ switch (input) {
11
+ case 'crit': return LogLevels.critical;
12
+ case 'critical': return LogLevels.critical;
13
+ case 'error': return LogLevels.error;
14
+ case 'warn': return LogLevels.warning;
15
+ case 'warning': return LogLevels.warning;
16
+ case 'notice': return LogLevels.notice;
17
+ case 'info': return LogLevels.info;
18
+ case 'debug': return LogLevels.debug;
19
+ case 'verbose': return LogLevels.debug;
20
+ case 'silly': return LogLevels.silly;
21
+ }
22
+
23
+ if (node === 'development') {
24
+ return LogLevels.debug;
25
+ }
26
+
27
+ if (node === 'test') {
28
+ return LogLevels.warning;
29
+ }
30
+
31
+ return LogLevels.info;
32
+ }
@@ -0,0 +1,57 @@
1
+ import { resolve, relative } from 'node:path';
2
+ import { parse } from 'stacktrace-parser';
3
+
4
+ /**
5
+ * @typedef {Object} Callsite
6
+ * @property {string} file The file name for the callsite
7
+ * @property {string} [relativePath] The relative path for the callsite.file
8
+ * @property {string} [absolutePath] The absolute path for the callsite.file
9
+ * @property {string} methodName The methodName from the callsite
10
+ * @property {number} line The line number in the file
11
+ * @property {number} column The column number in the file
12
+ */
13
+
14
+ /**
15
+ * Get the callsite that we think is outside the package
16
+ * @param {string} [stack] The stacktrace to parse
17
+ * @returns {Callsite[]}
18
+ */
19
+ export function parseStack(stack) {
20
+ /** @type {Callsite[]} */
21
+ const callsites = [];
22
+
23
+ if (stack == null) {
24
+ return callsites;
25
+ }
26
+
27
+ for (const callsite of parse(stack)) {
28
+ if (callsite.file == null) {
29
+ continue;
30
+ }
31
+
32
+ /** @type {Callsite} */
33
+ const site = {
34
+ file: callsite.file,
35
+ methodName: callsite.methodName,
36
+ line: callsite.lineNumber ?? 0,
37
+ column: (callsite.column ?? 1) - 1,
38
+ };
39
+
40
+ if (site.file.startsWith('file:')) {
41
+ site.file = site.file.slice(5);
42
+ }
43
+
44
+ if (site.file.startsWith('/')) {
45
+ site.relativePath = relative(process.cwd(), site.file);
46
+ site.absolutePath = resolve(site.file);
47
+
48
+ site.file = site.relativePath[0] === '.'
49
+ ? site.absolutePath
50
+ : site.relativePath;
51
+ }
52
+
53
+ callsites.push(site);
54
+ }
55
+
56
+ return callsites;
57
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @param {unknown} input The input to convert to a zod schema
3
+ * @returns {ZodType}
4
+ */
5
+ export function convertToZod(input: unknown): ZodType;
6
+ /**
7
+ * @param {unknown} input The input object test with
8
+ * @returns {ZodType}
9
+ */
10
+ export function objectContaining(input: unknown): ZodType;
11
+ /**
12
+ * @param {unknown} actual The value we're testing
13
+ * @param {unknown} expected The schema we're testing against
14
+ * @returns {void}
15
+ */
16
+ export function assertSchema(actual: unknown, expected: unknown): void;
17
+ export * from "zod";
18
+ import { ZodType } from 'zod';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @param {string} [channel] The logging channel
3
+ * @returns {import('./logger.js').Logger}
4
+ */
5
+ export function createLogger(channel?: string | undefined): import("./logger.js").Logger;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @typedef {(logEntry: import('./utilities/log-types.js').LogEntry) => string} Formatter
3
+ */
4
+ /** @type {Formatter} */
5
+ export const formatter: Formatter;
6
+ export type Formatter = (logEntry: import("./utilities/log-types.js").LogEntry) => string;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @param {LogEntry} logEntry the Log entry which we're going to convert to a splatted string
3
+ * @returns {string}
4
+ */
5
+ export function formatterConsole(logEntry: LogEntry): string;
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 Callsite = import("../utilities/log-types.js").Callsite;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @param {import('../utilities/log-types.js').LogEntry} logEntry the Log entry which we're going to convert to JSON
3
+ * @returns {string}
4
+ */
5
+ export function formatterJson(logEntry: import("../utilities/log-types.js").LogEntry): string;
@@ -0,0 +1,11 @@
1
+ export { Logger } from "./logger.js";
2
+ export const logger: import("./logger.js").Logger;
3
+ export { environment } from "./utilities/environment.js";
4
+ export { createLogger } from "./create-logger.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 { SecretDelete, SecretObscure, BufferReplacer, ErrorReplacer, HttpClientRequestReplacer, HttpIncomingMessageReplacer, HttpServerResponseReplacer, LongStringReplacer, NetSocketReplacer } from "./replacers/index.js";