@x-oasis/log 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/src/types.ts ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Log levels in order of severity
3
+ */
4
+ export enum LogLevel {
5
+ TRACE = 0,
6
+ DEBUG = 1,
7
+ INFO = 2,
8
+ WARN = 3,
9
+ ERROR = 4,
10
+ SILENT = 5,
11
+ }
12
+
13
+ /**
14
+ * Log entry data structure
15
+ */
16
+ export interface LogEntry {
17
+ level: LogLevel;
18
+ message: string;
19
+ timestamp: number;
20
+ context?: Record<string, unknown>;
21
+ metadata?: Record<string, unknown>;
22
+ error?: Error;
23
+ prefix?: string;
24
+ }
25
+
26
+ /**
27
+ * Output handler function type
28
+ */
29
+ export type OutputHandler = (entry: LogEntry) => void;
30
+
31
+ /**
32
+ * Logger configuration options
33
+ */
34
+ export interface LoggerOptions {
35
+ /**
36
+ * Minimum log level to output (default: LogLevel.INFO)
37
+ */
38
+ level?: LogLevel | keyof typeof LogLevel;
39
+
40
+ /**
41
+ * Custom output handler (default: console)
42
+ */
43
+ handler?: OutputHandler;
44
+
45
+ /**
46
+ * Enable timestamps in log entries (default: true)
47
+ */
48
+ enableTimestamp?: boolean;
49
+
50
+ /**
51
+ * Default context that will be included in all logs
52
+ */
53
+ defaultContext?: Record<string, unknown>;
54
+
55
+ /**
56
+ * Default prefix for all log messages
57
+ */
58
+ defaultPrefix?: string;
59
+ }
60
+
61
+ /**
62
+ * Chainable logger interface
63
+ */
64
+ export interface LoggerChain {
65
+ /**
66
+ * Add context data that will be included in this log entry
67
+ */
68
+ withContext(context: Record<string, unknown>): LoggerChain;
69
+
70
+ /**
71
+ * Add metadata that will be included in this log entry
72
+ */
73
+ withMetadata(metadata: Record<string, unknown>): LoggerChain;
74
+
75
+ /**
76
+ * Add an error object to this log entry
77
+ */
78
+ withError(error: Error): LoggerChain;
79
+
80
+ /**
81
+ * Add a prefix to this log message
82
+ */
83
+ withPrefix(prefix: string): LoggerChain;
84
+
85
+ /**
86
+ * Log at trace level
87
+ */
88
+ trace(message: string): void;
89
+
90
+ /**
91
+ * Log at debug level
92
+ */
93
+ debug(message: string): void;
94
+
95
+ /**
96
+ * Log at info level
97
+ */
98
+ info(message: string): void;
99
+
100
+ /**
101
+ * Log at warn level
102
+ */
103
+ warn(message: string): void;
104
+
105
+ /**
106
+ * Log at error level
107
+ */
108
+ error(message: string): void;
109
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,63 @@
1
+ import { LogLevel, LogEntry } from './types';
2
+
3
+ /**
4
+ * Convert log level string to LogLevel enum
5
+ */
6
+ export function parseLogLevel(
7
+ level: LogLevel | keyof typeof LogLevel
8
+ ): LogLevel {
9
+ if (typeof level === 'number') {
10
+ return level;
11
+ }
12
+ return LogLevel[level] ?? LogLevel.INFO;
13
+ }
14
+
15
+ /**
16
+ * Get log level name
17
+ */
18
+ export function getLogLevelName(level: LogLevel): string {
19
+ return LogLevel[level] ?? 'UNKNOWN';
20
+ }
21
+
22
+ /**
23
+ * Default console output handler
24
+ */
25
+ export function createConsoleHandler(): (entry: LogEntry) => void {
26
+ const consoleMethods: Record<number, typeof console.log> = {
27
+ [LogLevel.TRACE]: console.trace || console.debug,
28
+ [LogLevel.DEBUG]: console.debug,
29
+ [LogLevel.INFO]: console.info,
30
+ [LogLevel.WARN]: console.warn,
31
+ [LogLevel.ERROR]: console.error,
32
+ };
33
+
34
+ return (entry) => {
35
+ const method = consoleMethods[entry.level] || console.log;
36
+ const parts: unknown[] = [];
37
+
38
+ // Add prefix if present
39
+ if (entry.prefix) {
40
+ parts.push(entry.prefix);
41
+ }
42
+
43
+ // Add message
44
+ parts.push(entry.message);
45
+
46
+ // Add context if present
47
+ if (entry.context && Object.keys(entry.context).length > 0) {
48
+ parts.push('\nContext:', entry.context);
49
+ }
50
+
51
+ // Add metadata if present
52
+ if (entry.metadata && Object.keys(entry.metadata).length > 0) {
53
+ parts.push('\nMetadata:', entry.metadata);
54
+ }
55
+
56
+ // Add error if present
57
+ if (entry.error) {
58
+ parts.push('\nError:', entry.error);
59
+ }
60
+
61
+ method(...parts);
62
+ };
63
+ }
@@ -0,0 +1,333 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { Logger, LogLevel } from '../src';
3
+ // import { log } from '../src';
4
+
5
+ describe('Logger', () => {
6
+ let mockHandler: ReturnType<typeof vi.fn>;
7
+ let logger: Logger;
8
+
9
+ beforeEach(() => {
10
+ mockHandler = vi.fn();
11
+ logger = new Logger({
12
+ handler: mockHandler,
13
+ enableTimestamp: false,
14
+ });
15
+ });
16
+
17
+ describe('Basic logging', () => {
18
+ it('should log at trace level', () => {
19
+ logger.setLevel(LogLevel.TRACE);
20
+ logger.trace('test message');
21
+
22
+ expect(mockHandler).toHaveBeenCalledTimes(1);
23
+ expect(mockHandler).toHaveBeenCalledWith(
24
+ expect.objectContaining({
25
+ level: LogLevel.TRACE,
26
+ message: 'test message',
27
+ })
28
+ );
29
+ });
30
+
31
+ it('should log at debug level', () => {
32
+ logger.setLevel(LogLevel.DEBUG);
33
+ logger.debug('test message');
34
+
35
+ expect(mockHandler).toHaveBeenCalledTimes(1);
36
+ expect(mockHandler).toHaveBeenCalledWith(
37
+ expect.objectContaining({
38
+ level: LogLevel.DEBUG,
39
+ message: 'test message',
40
+ })
41
+ );
42
+ });
43
+
44
+ it('should log at info level', () => {
45
+ logger.info('test message');
46
+
47
+ expect(mockHandler).toHaveBeenCalledTimes(1);
48
+ expect(mockHandler).toHaveBeenCalledWith(
49
+ expect.objectContaining({
50
+ level: LogLevel.INFO,
51
+ message: 'test message',
52
+ })
53
+ );
54
+ });
55
+
56
+ it('should log at warn level', () => {
57
+ logger.warn('test message');
58
+
59
+ expect(mockHandler).toHaveBeenCalledTimes(1);
60
+ expect(mockHandler).toHaveBeenCalledWith(
61
+ expect.objectContaining({
62
+ level: LogLevel.WARN,
63
+ message: 'test message',
64
+ })
65
+ );
66
+ });
67
+
68
+ it('should log at error level', () => {
69
+ logger.error('test message');
70
+
71
+ expect(mockHandler).toHaveBeenCalledTimes(1);
72
+ expect(mockHandler).toHaveBeenCalledWith(
73
+ expect.objectContaining({
74
+ level: LogLevel.ERROR,
75
+ message: 'test message',
76
+ })
77
+ );
78
+ });
79
+ });
80
+
81
+ describe('Log level filtering', () => {
82
+ it('should not log messages below the set level', () => {
83
+ logger.setLevel(LogLevel.WARN);
84
+ logger.trace('trace message');
85
+ logger.debug('debug message');
86
+ logger.info('info message');
87
+
88
+ expect(mockHandler).not.toHaveBeenCalled();
89
+
90
+ logger.warn('warn message');
91
+ logger.error('error message');
92
+
93
+ expect(mockHandler).toHaveBeenCalledTimes(2);
94
+ });
95
+
96
+ it('should log all messages when level is TRACE', () => {
97
+ logger.setLevel(LogLevel.TRACE);
98
+ logger.trace('trace');
99
+ logger.debug('debug');
100
+ logger.info('info');
101
+ logger.warn('warn');
102
+ logger.error('error');
103
+
104
+ expect(mockHandler).toHaveBeenCalledTimes(5);
105
+ });
106
+ });
107
+
108
+ describe('Context support', () => {
109
+ it('should include context in log entries', () => {
110
+ logger.info('test message', { userId: '123', action: 'login' });
111
+
112
+ expect(mockHandler).toHaveBeenCalledWith(
113
+ expect.objectContaining({
114
+ context: {
115
+ userId: '123',
116
+ action: 'login',
117
+ },
118
+ })
119
+ );
120
+ });
121
+
122
+ it('should merge default context with provided context', () => {
123
+ logger.setDefaultContext({ app: 'my-app', version: '1.0.0' });
124
+ logger.info('test message', { userId: '123' });
125
+
126
+ expect(mockHandler).toHaveBeenCalledWith(
127
+ expect.objectContaining({
128
+ context: {
129
+ app: 'my-app',
130
+ version: '1.0.0',
131
+ userId: '123',
132
+ },
133
+ })
134
+ );
135
+ });
136
+ });
137
+
138
+ describe('Chainable API', () => {
139
+ it('should support chaining with context', () => {
140
+ logger
141
+ .chain()
142
+ .withContext({ userId: '123' })
143
+ .withContext({ action: 'login' })
144
+ .info('User logged in');
145
+
146
+ expect(mockHandler).toHaveBeenCalledWith(
147
+ expect.objectContaining({
148
+ context: {
149
+ userId: '123',
150
+ action: 'login',
151
+ },
152
+ message: 'User logged in',
153
+ })
154
+ );
155
+ });
156
+
157
+ it('should support chaining with metadata', () => {
158
+ logger
159
+ .chain()
160
+ .withMetadata({ duration: 150, status: 'success' })
161
+ .info('Request completed');
162
+
163
+ expect(mockHandler).toHaveBeenCalledWith(
164
+ expect.objectContaining({
165
+ metadata: {
166
+ duration: 150,
167
+ status: 'success',
168
+ },
169
+ message: 'Request completed',
170
+ })
171
+ );
172
+ });
173
+
174
+ it('should support chaining with error', () => {
175
+ const error = new Error('Something went wrong');
176
+ logger.chain().withError(error).error('Operation failed');
177
+
178
+ expect(mockHandler).toHaveBeenCalledWith(
179
+ expect.objectContaining({
180
+ error,
181
+ message: 'Operation failed',
182
+ })
183
+ );
184
+ });
185
+
186
+ it('should support chaining with prefix', () => {
187
+ logger.chain().withPrefix('[API]').info('Request received');
188
+
189
+ expect(mockHandler).toHaveBeenCalledWith(
190
+ expect.objectContaining({
191
+ prefix: '[API]',
192
+ message: 'Request received',
193
+ })
194
+ );
195
+ });
196
+
197
+ it('should support complex chaining', () => {
198
+ const error = new Error('Validation failed');
199
+ logger
200
+ .chain()
201
+ .withContext({ userId: '123' })
202
+ .withMetadata({ field: 'email', value: 'invalid' })
203
+ .withError(error)
204
+ .withPrefix('[VALIDATION]')
205
+ .warn('Validation error');
206
+
207
+ expect(mockHandler).toHaveBeenCalledWith(
208
+ expect.objectContaining({
209
+ context: { userId: '123' },
210
+ metadata: { field: 'email', value: 'invalid' },
211
+ error,
212
+ prefix: '[VALIDATION]',
213
+ message: 'Validation error',
214
+ })
215
+ );
216
+ });
217
+ });
218
+
219
+ describe('withContext and withPrefix methods', () => {
220
+ it('should create chainable logger with context', () => {
221
+ logger.withContext({ userId: '123' }).info('User action');
222
+
223
+ expect(mockHandler).toHaveBeenCalledWith(
224
+ expect.objectContaining({
225
+ context: { userId: '123' },
226
+ })
227
+ );
228
+ });
229
+
230
+ it('should create chainable logger with prefix', () => {
231
+ logger.withPrefix('[APP]').info('Application started');
232
+
233
+ expect(mockHandler).toHaveBeenCalledWith(
234
+ expect.objectContaining({
235
+ prefix: '[APP]',
236
+ })
237
+ );
238
+ });
239
+ });
240
+
241
+ describe('Default prefix', () => {
242
+ it('should include default prefix in all logs', () => {
243
+ logger.setDefaultPrefix('[MY-APP]');
244
+ logger.info('Test message');
245
+
246
+ expect(mockHandler).toHaveBeenCalledWith(
247
+ expect.objectContaining({
248
+ prefix: '[MY-APP]',
249
+ })
250
+ );
251
+ });
252
+
253
+ it('should allow overriding default prefix with chain prefix', () => {
254
+ logger.setDefaultPrefix('[DEFAULT]');
255
+ logger.chain().withPrefix('[OVERRIDE]').info('Test message');
256
+
257
+ expect(mockHandler).toHaveBeenCalledWith(
258
+ expect.objectContaining({
259
+ prefix: '[OVERRIDE]',
260
+ })
261
+ );
262
+ });
263
+ });
264
+
265
+ describe('Timestamp', () => {
266
+ it('should include timestamp when enabled', () => {
267
+ const loggerWithTimestamp = new Logger({
268
+ handler: mockHandler,
269
+ enableTimestamp: true,
270
+ });
271
+ loggerWithTimestamp.info('test');
272
+
273
+ const call = mockHandler.mock.calls[0][0];
274
+ expect(call.timestamp).toBeGreaterThan(0);
275
+ });
276
+
277
+ it('should not include timestamp when disabled', () => {
278
+ logger.info('test');
279
+
280
+ const call = mockHandler.mock.calls[0][0];
281
+ expect(call.timestamp).toBe(0);
282
+ });
283
+ });
284
+
285
+ describe('Log level string parsing', () => {
286
+ it('should accept log level as string', () => {
287
+ logger.setLevel('DEBUG');
288
+ logger.setLevel('WARN');
289
+ logger.setLevel('ERROR');
290
+
291
+ expect(logger.getLevel()).toBe(LogLevel.ERROR);
292
+ });
293
+
294
+ it('should default to INFO for invalid string', () => {
295
+ logger.setLevel('INVALID' as any);
296
+ expect(logger.getLevel()).toBe(LogLevel.INFO);
297
+ });
298
+ });
299
+ });
300
+
301
+ // describe('Default logger (log)', () => {
302
+ // it('should provide convenience methods', () => {
303
+ // const originalConsole = console.info;
304
+ // const mockConsole = vi.fn();
305
+ // console.info = mockConsole;
306
+
307
+ // try {
308
+ // log.info('test message');
309
+ // expect(mockConsole).toHaveBeenCalled();
310
+ // } finally {
311
+ // console.info = originalConsole;
312
+ // }
313
+ // });
314
+
315
+ // it('should support setLevel', () => {
316
+ // expect(() => log.setLevel(LogLevel.DEBUG)).not.toThrow();
317
+ // });
318
+
319
+ // it('should support withContext', () => {
320
+ // const chain = log.withContext({ test: 'value' });
321
+ // expect(chain).toBeDefined();
322
+ // });
323
+
324
+ // it('should support withPrefix', () => {
325
+ // const chain = log.withPrefix('[TEST]');
326
+ // expect(chain).toBeDefined();
327
+ // });
328
+
329
+ // it('should support chain', () => {
330
+ // const chain = log.chain();
331
+ // expect(chain).toBeDefined();
332
+ // });
333
+ // });
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../../tsconfig.build.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "esModuleInterop": true
6
+ },
7
+
8
+ "include": [
9
+ "src/**/*"
10
+ ]
11
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "jsx": "react",
5
+ "esModuleInterop": true
6
+ }
7
+ }
@@ -0,0 +1,21 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ include: ['test/**/*.(spec|test).ts'],
7
+ exclude: ['node_modules/**'],
8
+ threads: false,
9
+
10
+ coverage: {
11
+ provider: 'istanbul',
12
+ },
13
+ },
14
+
15
+ resolve: {
16
+ alias: {},
17
+ },
18
+ define: {
19
+ __DEV__: false,
20
+ },
21
+ });