@arkv/logger 0.1.1
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.
- package/README.md +200 -0
- package/dist/cjs/context.js +24 -0
- package/dist/cjs/format.js +32 -0
- package/dist/cjs/index.js +11 -0
- package/dist/cjs/logger.js +220 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/sanitize.js +187 -0
- package/dist/cjs/test-utils.js +26 -0
- package/dist/cjs/types.js +30 -0
- package/dist/esm/context.js +20 -0
- package/dist/esm/format.js +29 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/logger.js +216 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/sanitize.js +183 -0
- package/dist/esm/test-utils.js +22 -0
- package/dist/esm/types.js +27 -0
- package/dist/types/context.d.ts +7 -0
- package/dist/types/format.d.ts +2 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/logger.d.ts +26 -0
- package/dist/types/sanitize.d.ts +8 -0
- package/dist/types/test-utils.d.ts +3 -0
- package/dist/types/types.d.ts +37 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# @arkv/logger
|
|
2
|
+
|
|
3
|
+
Framework-agnostic structured logger with async context, sanitization, and colored output.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @arkv/logger
|
|
9
|
+
# or
|
|
10
|
+
npm install @arkv/logger
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Basic Logging
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { Logger } from '@arkv/logger';
|
|
19
|
+
|
|
20
|
+
const logger = new Logger({
|
|
21
|
+
name: 'my-app',
|
|
22
|
+
version: '1.0.0',
|
|
23
|
+
env: 'production',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
logger.log('Server started');
|
|
27
|
+
logger.debug('Loading config', { path: '/etc/app' });
|
|
28
|
+
logger.warn('Disk usage high', { usage: 92 });
|
|
29
|
+
logger.error('Request failed', new Error('timeout'));
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Log Levels
|
|
33
|
+
|
|
34
|
+
Six levels in ascending severity:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { Logger, LogLevel } from '@arkv/logger';
|
|
38
|
+
|
|
39
|
+
const logger = new Logger({ level: LogLevel.WARN });
|
|
40
|
+
|
|
41
|
+
logger.debug('skipped'); // below WARN, not logged
|
|
42
|
+
logger.warn('logged'); // WARN and above are logged
|
|
43
|
+
logger.error('logged');
|
|
44
|
+
logger.fatal('logged');
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
| Level | Value |
|
|
48
|
+
|-------|-------|
|
|
49
|
+
| `LogLevel.VERBOSE` | `'verbose'` |
|
|
50
|
+
| `LogLevel.DEBUG` | `'debug'` |
|
|
51
|
+
| `LogLevel.LOG` | `'log'` |
|
|
52
|
+
| `LogLevel.WARN` | `'warn'` |
|
|
53
|
+
| `LogLevel.ERROR` | `'error'` |
|
|
54
|
+
| `LogLevel.FATAL` | `'fatal'` |
|
|
55
|
+
|
|
56
|
+
### Async Context
|
|
57
|
+
|
|
58
|
+
Track request-scoped data across async boundaries using `ContextStore` (backed by `AsyncLocalStorage`):
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { Logger, ContextStore } from '@arkv/logger';
|
|
62
|
+
|
|
63
|
+
const context = new ContextStore();
|
|
64
|
+
const logger = new Logger({ name: 'api' }, context);
|
|
65
|
+
|
|
66
|
+
function handleRequest(req) {
|
|
67
|
+
context.runWithContext(
|
|
68
|
+
{ requestId: req.id, userId: req.user },
|
|
69
|
+
() => {
|
|
70
|
+
// requestId and userId are automatically
|
|
71
|
+
// included in every log entry
|
|
72
|
+
logger.log('Processing request');
|
|
73
|
+
doWork();
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Sensitive Field Masking
|
|
80
|
+
|
|
81
|
+
Fields matching known sensitive names are automatically replaced with `[MASKED]`:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
logger.log('User login', {
|
|
85
|
+
username: 'alice',
|
|
86
|
+
password: 'secret', // → [MASKED]
|
|
87
|
+
token: 'jwt-abc', // → [MASKED]
|
|
88
|
+
apiKey: 'key-123', // → [MASKED]
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Default masked fields: `password`, `secret`, `token`, `authorization`, `cookie`, `apiKey`, `apiSecret`, `apiPass`. Add custom fields via config:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
const logger = new Logger({
|
|
96
|
+
maskFields: ['ssn', 'creditCard'],
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Error Handling
|
|
101
|
+
|
|
102
|
+
Errors are automatically extracted and serialized from multiple patterns:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// Direct Error object
|
|
106
|
+
logger.error('Failed', new Error('timeout'));
|
|
107
|
+
|
|
108
|
+
// Error as message
|
|
109
|
+
logger.error(new Error('crash'));
|
|
110
|
+
|
|
111
|
+
// Wrapped in object
|
|
112
|
+
logger.error('Op failed', { err: new Error('db') });
|
|
113
|
+
logger.error('Op failed', { error: new Error('db') });
|
|
114
|
+
|
|
115
|
+
// String shorthand at error/warn/fatal level
|
|
116
|
+
logger.error('Op failed', { error: 'connection refused' });
|
|
117
|
+
|
|
118
|
+
// Deeply nested errors are found automatically
|
|
119
|
+
logger.error('Op failed', {
|
|
120
|
+
metadata: { nested: { err: new Error('deep') } },
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Development vs Production Output
|
|
125
|
+
|
|
126
|
+
In development (`NODE_ENV !== 'production'`), output is colored JSON for readability. In production, output is plain JSON for log aggregators:
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// Colored dev output (default)
|
|
130
|
+
const dev = new Logger({ isDevelopment: true });
|
|
131
|
+
|
|
132
|
+
// Plain JSON for production
|
|
133
|
+
const prod = new Logger({ isDevelopment: false });
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Event Filtering
|
|
137
|
+
|
|
138
|
+
Suppress logs for specific events (e.g. health checks):
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
const logger = new Logger(
|
|
142
|
+
{ filterEvents: ['/health', '/ready'] },
|
|
143
|
+
contextStore,
|
|
144
|
+
);
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
When the context's `event` field matches a filtered event, the log is silently dropped.
|
|
148
|
+
|
|
149
|
+
## API
|
|
150
|
+
|
|
151
|
+
### `Logger`
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
new Logger(config?: LoggerConfig, context?: ContextStore)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
| Property | Type | Description |
|
|
158
|
+
|----------|------|-------------|
|
|
159
|
+
| `logLevel` | `LogLevel` | Current log level (read-only) |
|
|
160
|
+
| `appId` | `string \| undefined` | `name-version-env` or `undefined` |
|
|
161
|
+
|
|
162
|
+
| Method | Description |
|
|
163
|
+
|--------|-------------|
|
|
164
|
+
| `log(message, ...params)` | Log at `log` level |
|
|
165
|
+
| `debug(message, ...params)` | Log at `debug` level |
|
|
166
|
+
| `verbose(message, ...params)` | Log at `verbose` level |
|
|
167
|
+
| `warn(message, ...params)` | Log at `warn` level |
|
|
168
|
+
| `error(message, ...params)` | Log at `error` level |
|
|
169
|
+
| `fatal(message, ...params)` | Log at `fatal` level |
|
|
170
|
+
|
|
171
|
+
Each method accepts `string`, `Record<string, unknown>`, or `Error` as the message, plus optional extra params.
|
|
172
|
+
|
|
173
|
+
### `LoggerConfig`
|
|
174
|
+
|
|
175
|
+
| Field | Type | Default | Description |
|
|
176
|
+
|-------|------|---------|-------------|
|
|
177
|
+
| `name` | `string` | — | Application name |
|
|
178
|
+
| `version` | `string` | — | Application version |
|
|
179
|
+
| `env` | `string` | — | Environment name |
|
|
180
|
+
| `level` | `LogLevel` | `DEBUG` | Minimum log level |
|
|
181
|
+
| `isDevelopment` | `boolean` | `NODE_ENV !== 'production'` | Colored vs plain JSON output |
|
|
182
|
+
| `maskFields` | `string[]` | `[]` | Additional fields to mask (merged with defaults) |
|
|
183
|
+
| `filterEvents` | `string[]` | `[]` | Context events to suppress |
|
|
184
|
+
| `maxArrayLength` | `number` | `100` | Max array items before truncation |
|
|
185
|
+
|
|
186
|
+
### `ContextStore`
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
new ContextStore()
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
| Method | Description |
|
|
193
|
+
|--------|-------------|
|
|
194
|
+
| `getContext()` | Get current async context |
|
|
195
|
+
| `updateContext(partial)` | Merge partial update into current context |
|
|
196
|
+
| `runWithContext(ctx, callback)` | Execute callback within a context |
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
[MIT](../../LICENSE)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ContextStore = void 0;
|
|
4
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
5
|
+
class ContextStore {
|
|
6
|
+
asyncLocalStorage = new node_async_hooks_1.AsyncLocalStorage();
|
|
7
|
+
getContext() {
|
|
8
|
+
const context = this.asyncLocalStorage.getStore();
|
|
9
|
+
if (!context) {
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
return { ...context };
|
|
13
|
+
}
|
|
14
|
+
updateContext(obj) {
|
|
15
|
+
const context = this.asyncLocalStorage.getStore();
|
|
16
|
+
if (context) {
|
|
17
|
+
Object.assign(context, obj);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
runWithContext(context, callback) {
|
|
21
|
+
return this.asyncLocalStorage.run(context, callback);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.ContextStore = ContextStore;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatColoredJson = formatColoredJson;
|
|
4
|
+
const colors_1 = require("@arkv/colors");
|
|
5
|
+
const shared_1 = require("@arkv/shared");
|
|
6
|
+
function formatColoredJson(obj, level) {
|
|
7
|
+
const jsonString = (0, shared_1.safeStringify)(obj);
|
|
8
|
+
const colorMap = {
|
|
9
|
+
level: (0, colors_1.getLevelColorFn)(level),
|
|
10
|
+
message: colors_1.green,
|
|
11
|
+
timestamp: colors_1.magenta,
|
|
12
|
+
requestId: colors_1.brightGreen,
|
|
13
|
+
userId: colors_1.brightBlue,
|
|
14
|
+
context: colors_1.brightCyan,
|
|
15
|
+
duration: colors_1.yellow,
|
|
16
|
+
event: colors_1.brightMagenta,
|
|
17
|
+
error: colors_1.red,
|
|
18
|
+
exception: colors_1.red,
|
|
19
|
+
flow: colors_1.brightGreen,
|
|
20
|
+
method: colors_1.brightBlue,
|
|
21
|
+
stack: colors_1.gray,
|
|
22
|
+
status: colors_1.brightYellow,
|
|
23
|
+
elapsed: colors_1.brightYellow,
|
|
24
|
+
};
|
|
25
|
+
return jsonString.replace(/(".*?":\s*)(.*?)(?=,|\n|$)/g, (_, key, value) => {
|
|
26
|
+
const keyWithoutQuotes = key
|
|
27
|
+
.replace(/"/g, '')
|
|
28
|
+
.slice(0, -1);
|
|
29
|
+
const colorizer = colorMap[keyWithoutQuotes] || (0, colors_1.getValueColor)(value);
|
|
30
|
+
return `${(0, colors_1.cyan)(key)}${colorizer(value)}`;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LogLevel = exports.LOG_LEVELS = exports.DEFAULT_MASK_FIELDS = exports.Logger = exports.ContextStore = void 0;
|
|
4
|
+
var context_js_1 = require("./context.js");
|
|
5
|
+
Object.defineProperty(exports, "ContextStore", { enumerable: true, get: function () { return context_js_1.ContextStore; } });
|
|
6
|
+
var logger_js_1 = require("./logger.js");
|
|
7
|
+
Object.defineProperty(exports, "Logger", { enumerable: true, get: function () { return logger_js_1.Logger; } });
|
|
8
|
+
var types_js_1 = require("./types.js");
|
|
9
|
+
Object.defineProperty(exports, "DEFAULT_MASK_FIELDS", { enumerable: true, get: function () { return types_js_1.DEFAULT_MASK_FIELDS; } });
|
|
10
|
+
Object.defineProperty(exports, "LOG_LEVELS", { enumerable: true, get: function () { return types_js_1.LOG_LEVELS; } });
|
|
11
|
+
Object.defineProperty(exports, "LogLevel", { enumerable: true, get: function () { return types_js_1.LogLevel; } });
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Logger = void 0;
|
|
4
|
+
const shared_1 = require("@arkv/shared");
|
|
5
|
+
const format_js_1 = require("./format.js");
|
|
6
|
+
const sanitize_js_1 = require("./sanitize.js");
|
|
7
|
+
const types_js_1 = require("./types.js");
|
|
8
|
+
class Logger {
|
|
9
|
+
logLevel;
|
|
10
|
+
#isDevelopment;
|
|
11
|
+
#maskFields;
|
|
12
|
+
#maxArrayLength;
|
|
13
|
+
#filterEvents;
|
|
14
|
+
#context;
|
|
15
|
+
#appName;
|
|
16
|
+
#appVersion;
|
|
17
|
+
#appEnv;
|
|
18
|
+
constructor(config, context) {
|
|
19
|
+
const cfg = config ?? {};
|
|
20
|
+
this.logLevel = cfg.level ?? types_js_1.LogLevel.DEBUG;
|
|
21
|
+
this.#isDevelopment =
|
|
22
|
+
cfg.isDevelopment ??
|
|
23
|
+
process.env.NODE_ENV !== 'production';
|
|
24
|
+
this.#maskFields =
|
|
25
|
+
cfg.maskFields && cfg.maskFields.length > 0
|
|
26
|
+
? Array.from(new Set([
|
|
27
|
+
...types_js_1.DEFAULT_MASK_FIELDS,
|
|
28
|
+
...cfg.maskFields,
|
|
29
|
+
]))
|
|
30
|
+
: [...types_js_1.DEFAULT_MASK_FIELDS];
|
|
31
|
+
this.#maxArrayLength = cfg.maxArrayLength ?? 100;
|
|
32
|
+
this.#filterEvents = cfg.filterEvents ?? [];
|
|
33
|
+
this.#context = context;
|
|
34
|
+
this.#appName = cfg.name;
|
|
35
|
+
this.#appVersion = cfg.version;
|
|
36
|
+
this.#appEnv = cfg.env;
|
|
37
|
+
}
|
|
38
|
+
get appId() {
|
|
39
|
+
if (this.#appName && this.#appVersion && this.#appEnv) {
|
|
40
|
+
return `${this.#appName}-${this.#appVersion}-${this.#appEnv}`;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
log(message, ...optionalParams) {
|
|
45
|
+
this.#writeLog(types_js_1.LogLevel.LOG, message, optionalParams);
|
|
46
|
+
}
|
|
47
|
+
error(message, ...optionalParams) {
|
|
48
|
+
this.#writeLog(types_js_1.LogLevel.ERROR, message, optionalParams);
|
|
49
|
+
}
|
|
50
|
+
warn(message, ...optionalParams) {
|
|
51
|
+
this.#writeLog(types_js_1.LogLevel.WARN, message, optionalParams);
|
|
52
|
+
}
|
|
53
|
+
debug(message, ...optionalParams) {
|
|
54
|
+
this.#writeLog(types_js_1.LogLevel.DEBUG, message, optionalParams);
|
|
55
|
+
}
|
|
56
|
+
verbose(message, ...optionalParams) {
|
|
57
|
+
this.#writeLog(types_js_1.LogLevel.VERBOSE, message, optionalParams);
|
|
58
|
+
}
|
|
59
|
+
fatal(message, ...optionalParams) {
|
|
60
|
+
this.#writeLog(types_js_1.LogLevel.FATAL, message, optionalParams);
|
|
61
|
+
}
|
|
62
|
+
#writeLog(level, message, optionalParams) {
|
|
63
|
+
if (!this.#shouldLog(level)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const { preparedMessage, invalidMessageInfo, messageError, messageExtra, } = this.#prepareMessage(message);
|
|
67
|
+
const { error, extra } = this.#extractErrorAndExtra(optionalParams, level);
|
|
68
|
+
const finalError = messageError || error;
|
|
69
|
+
const finalExtra = {
|
|
70
|
+
...messageExtra,
|
|
71
|
+
...extra,
|
|
72
|
+
};
|
|
73
|
+
const logEntry = this.#createLogEntry(level, preparedMessage, finalExtra, finalError, invalidMessageInfo);
|
|
74
|
+
const sanitizedLogEntry = (0, sanitize_js_1.sanitizeLogEntry)(logEntry, {
|
|
75
|
+
maskFields: this.#maskFields,
|
|
76
|
+
maxArrayLength: this.#maxArrayLength,
|
|
77
|
+
});
|
|
78
|
+
const output = this.#isDevelopment
|
|
79
|
+
? (0, format_js_1.formatColoredJson)(sanitizedLogEntry, level)
|
|
80
|
+
: (0, shared_1.safeStringify)(sanitizedLogEntry);
|
|
81
|
+
console.log(output);
|
|
82
|
+
}
|
|
83
|
+
#prepareMessage(message) {
|
|
84
|
+
if (typeof message === 'string') {
|
|
85
|
+
return { preparedMessage: message };
|
|
86
|
+
}
|
|
87
|
+
if (message instanceof Error) {
|
|
88
|
+
return {
|
|
89
|
+
preparedMessage: message.message,
|
|
90
|
+
messageError: message,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if ((0, shared_1.isPlainObject)(message)) {
|
|
94
|
+
const foundError = (0, sanitize_js_1.findNestedError)(message);
|
|
95
|
+
if (foundError) {
|
|
96
|
+
return {
|
|
97
|
+
preparedMessage: foundError.message,
|
|
98
|
+
messageError: foundError,
|
|
99
|
+
messageExtra: message,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
preparedMessage: 'Object logged',
|
|
104
|
+
messageExtra: message,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const stack = new Error().stack
|
|
108
|
+
?.split('\n')
|
|
109
|
+
.slice(2, 7)
|
|
110
|
+
.join('\n');
|
|
111
|
+
const preparedMessage = message === null || message === undefined
|
|
112
|
+
? `[${String(message)}]`
|
|
113
|
+
: `[OBJECT]: ${(0, shared_1.safeStringify)(message)}`;
|
|
114
|
+
const invalidMessageInfo = {
|
|
115
|
+
invalidMessageWarning: 'Logger called with non-string message parameter',
|
|
116
|
+
invalidMessageCallstack: stack,
|
|
117
|
+
originalMessageType: typeof message,
|
|
118
|
+
originalMessage: (0, shared_1.safeStringify)(message),
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
preparedMessage,
|
|
122
|
+
invalidMessageInfo,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: handles multiple error extraction patterns
|
|
126
|
+
#extractErrorAndExtra(params, level) {
|
|
127
|
+
let error = null;
|
|
128
|
+
const extra = {};
|
|
129
|
+
for (const param of params) {
|
|
130
|
+
if (param instanceof Error) {
|
|
131
|
+
error = param;
|
|
132
|
+
}
|
|
133
|
+
else if (typeof param === 'string') {
|
|
134
|
+
const isErrorLevel = level === types_js_1.LogLevel.WARN ||
|
|
135
|
+
level === types_js_1.LogLevel.ERROR ||
|
|
136
|
+
level === types_js_1.LogLevel.FATAL;
|
|
137
|
+
if (isErrorLevel) {
|
|
138
|
+
error = new Error(param);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
extra.context = param;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else if ((0, shared_1.isPlainObject)(param)) {
|
|
145
|
+
const isErrorLevel = level === types_js_1.LogLevel.WARN ||
|
|
146
|
+
level === types_js_1.LogLevel.ERROR ||
|
|
147
|
+
level === types_js_1.LogLevel.FATAL;
|
|
148
|
+
if (param.err instanceof Error) {
|
|
149
|
+
error = param.err;
|
|
150
|
+
const { err: _, ...rest } = param;
|
|
151
|
+
Object.assign(extra, rest);
|
|
152
|
+
}
|
|
153
|
+
else if (param.error instanceof Error) {
|
|
154
|
+
error = param.error;
|
|
155
|
+
const { error: _, ...rest } = param;
|
|
156
|
+
Object.assign(extra, rest);
|
|
157
|
+
}
|
|
158
|
+
else if (isErrorLevel &&
|
|
159
|
+
typeof param.err === 'string') {
|
|
160
|
+
error = new Error(param.err);
|
|
161
|
+
const { err: _, ...rest } = param;
|
|
162
|
+
Object.assign(extra, rest);
|
|
163
|
+
}
|
|
164
|
+
else if (isErrorLevel &&
|
|
165
|
+
typeof param.error === 'string') {
|
|
166
|
+
error = new Error(param.error);
|
|
167
|
+
const { error: _, ...rest } = param;
|
|
168
|
+
Object.assign(extra, rest);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const foundError = (0, sanitize_js_1.findNestedError)(param);
|
|
172
|
+
if (foundError) {
|
|
173
|
+
error = foundError;
|
|
174
|
+
}
|
|
175
|
+
Object.assign(extra, param);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return { error, extra };
|
|
180
|
+
}
|
|
181
|
+
#createLogEntry(level, message, extra, error, invalidMessageInfo) {
|
|
182
|
+
const ctx = this.#context
|
|
183
|
+
? this.#context.getContext()
|
|
184
|
+
: {};
|
|
185
|
+
const logEntry = {
|
|
186
|
+
level,
|
|
187
|
+
timestamp: new Date().toISOString(),
|
|
188
|
+
pid: process.pid,
|
|
189
|
+
message,
|
|
190
|
+
...(this.appId ? { appId: this.appId } : {}),
|
|
191
|
+
...ctx,
|
|
192
|
+
...extra,
|
|
193
|
+
...(invalidMessageInfo || {}),
|
|
194
|
+
};
|
|
195
|
+
if (error) {
|
|
196
|
+
logEntry.error = {
|
|
197
|
+
name: error.name,
|
|
198
|
+
message: error.message,
|
|
199
|
+
stack: error.stack?.replace(/\n(\s+)?/g, ','),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return logEntry;
|
|
203
|
+
}
|
|
204
|
+
#shouldLog(level) {
|
|
205
|
+
const configuredIdx = types_js_1.LOG_LEVELS.indexOf(this.logLevel);
|
|
206
|
+
const messageIdx = types_js_1.LOG_LEVELS.indexOf(level);
|
|
207
|
+
if (messageIdx < configuredIdx) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
if (this.#context) {
|
|
211
|
+
const ctx = this.#context.getContext();
|
|
212
|
+
if (ctx.event &&
|
|
213
|
+
this.#filterEvents.includes(ctx.event)) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
exports.Logger = Logger;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sanitizeLogEntry = sanitizeLogEntry;
|
|
4
|
+
exports.findNestedError = findNestedError;
|
|
5
|
+
const shared_1 = require("@arkv/shared");
|
|
6
|
+
function sanitizeLogEntry(obj, options, visited = new WeakSet()) {
|
|
7
|
+
if (visited.has(obj)) {
|
|
8
|
+
return {
|
|
9
|
+
'[Circular]': 'circular reference detected',
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
visited.add(obj);
|
|
13
|
+
const cleaned = {};
|
|
14
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
15
|
+
if (value === undefined || value === null) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const shouldMask = options.maskFields.some(field => key.toLowerCase().includes(field.toLowerCase()));
|
|
19
|
+
if (shouldMask) {
|
|
20
|
+
cleaned[key] = '[MASKED]';
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
const safeValue = makeSafeForJson(value, options);
|
|
24
|
+
if (safeValue !== undefined) {
|
|
25
|
+
if (Array.isArray(safeValue)) {
|
|
26
|
+
cleaned[key] = sanitizeArray(safeValue, options, visited);
|
|
27
|
+
}
|
|
28
|
+
else if ((0, shared_1.isPlainObject)(safeValue)) {
|
|
29
|
+
cleaned[key] = sanitizeLogEntry(safeValue, options, visited);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
cleaned[key] = safeValue;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return cleaned;
|
|
38
|
+
}
|
|
39
|
+
function sanitizeArray(array, options, visited) {
|
|
40
|
+
return array.map(item => {
|
|
41
|
+
if ((0, shared_1.isPlainObject)(item)) {
|
|
42
|
+
return sanitizeLogEntry(item, options, visited);
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(item)) {
|
|
45
|
+
return sanitizeArray(item, options, visited);
|
|
46
|
+
}
|
|
47
|
+
return makeSafeForJson(item, options);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: recursive error search
|
|
51
|
+
function findNestedError(obj, visited = new WeakSet()) {
|
|
52
|
+
if (!obj || typeof obj !== 'object') {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (visited.has(obj)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
visited.add(obj);
|
|
59
|
+
for (const value of Object.values(obj)) {
|
|
60
|
+
if (value instanceof Error) {
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
if ((0, shared_1.isPlainObject)(value)) {
|
|
64
|
+
const nestedError = findNestedError(value, visited);
|
|
65
|
+
if (nestedError) {
|
|
66
|
+
return nestedError;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (Array.isArray(value)) {
|
|
70
|
+
for (const item of value) {
|
|
71
|
+
if (item instanceof Error) {
|
|
72
|
+
return item;
|
|
73
|
+
}
|
|
74
|
+
if ((0, shared_1.isPlainObject)(item)) {
|
|
75
|
+
const nestedError = findNestedError(item, visited);
|
|
76
|
+
if (nestedError) {
|
|
77
|
+
return nestedError;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: handles many type serialization cases
|
|
86
|
+
function makeSafeForJson(value, options) {
|
|
87
|
+
if (value === null || value === undefined) {
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
const valueType = typeof value;
|
|
91
|
+
if (valueType === 'function') {
|
|
92
|
+
return `[Function: ${value.name || 'anonymous'}]`;
|
|
93
|
+
}
|
|
94
|
+
if (valueType === 'symbol') {
|
|
95
|
+
return `[Symbol: ${value.toString()}]`;
|
|
96
|
+
}
|
|
97
|
+
if (valueType === 'bigint') {
|
|
98
|
+
return `[BigInt: ${value.toString()}]`;
|
|
99
|
+
}
|
|
100
|
+
if (valueType !== 'object') {
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
if (value instanceof Date) {
|
|
104
|
+
return value.toISOString();
|
|
105
|
+
}
|
|
106
|
+
if (value instanceof RegExp) {
|
|
107
|
+
return `[RegExp: ${value.toString()}]`;
|
|
108
|
+
}
|
|
109
|
+
if (value instanceof Error) {
|
|
110
|
+
return {
|
|
111
|
+
name: value.name,
|
|
112
|
+
message: value.message,
|
|
113
|
+
stack: value.stack?.replace(/\n(\s+)?/g, ','),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (typeof FormData !== 'undefined' &&
|
|
117
|
+
value instanceof FormData) {
|
|
118
|
+
const entries = {};
|
|
119
|
+
try {
|
|
120
|
+
for (const [key, val] of value.entries()) {
|
|
121
|
+
if (val &&
|
|
122
|
+
typeof val === 'object' &&
|
|
123
|
+
'name' in val &&
|
|
124
|
+
'size' in val &&
|
|
125
|
+
'type' in val) {
|
|
126
|
+
const file = val;
|
|
127
|
+
entries[key] =
|
|
128
|
+
`[File: ${file.name} (${file.size} bytes, ${file.type})]`;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
entries[key] = val;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { '[FormData]': entries };
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return '[FormData: unable to read entries]';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (value &&
|
|
141
|
+
typeof value === 'object' &&
|
|
142
|
+
'name' in value &&
|
|
143
|
+
'size' in value &&
|
|
144
|
+
'type' in value &&
|
|
145
|
+
typeof value
|
|
146
|
+
.arrayBuffer === 'function') {
|
|
147
|
+
const file = value;
|
|
148
|
+
return `[File: ${file.name} (${file.size} bytes, ${file.type})]`;
|
|
149
|
+
}
|
|
150
|
+
if (typeof Blob !== 'undefined' &&
|
|
151
|
+
value instanceof Blob) {
|
|
152
|
+
return `[Blob: ${value.size} bytes, ${value.type}]`;
|
|
153
|
+
}
|
|
154
|
+
if (typeof ArrayBuffer !== 'undefined' &&
|
|
155
|
+
value instanceof ArrayBuffer) {
|
|
156
|
+
return `[ArrayBuffer: ${value.byteLength} bytes]`;
|
|
157
|
+
}
|
|
158
|
+
if (Array.isArray(value)) {
|
|
159
|
+
return sliceArray(value, options);
|
|
160
|
+
}
|
|
161
|
+
if ((0, shared_1.isPlainObject)(value)) {
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
JSON.stringify(value);
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
if (value
|
|
170
|
+
.constructor?.name) {
|
|
171
|
+
return `[${value.constructor.name}: object not serializable]`;
|
|
172
|
+
}
|
|
173
|
+
return '[Object: not serializable]';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function sliceArray(array, options) {
|
|
177
|
+
if (array.length <= options.maxArrayLength) {
|
|
178
|
+
return array.map(item => makeSafeForJson(item, options));
|
|
179
|
+
}
|
|
180
|
+
const slicedArray = array
|
|
181
|
+
.slice(0, options.maxArrayLength)
|
|
182
|
+
.map(item => makeSafeForJson(item, options));
|
|
183
|
+
return [
|
|
184
|
+
...slicedArray,
|
|
185
|
+
`[TRUNCATED: ${array.length - options.maxArrayLength} more items]`,
|
|
186
|
+
];
|
|
187
|
+
}
|