@onebun/logger 0.1.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.
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@onebun/logger",
3
+ "version": "0.1.0",
4
+ "description": "Logger package for OneBun framework",
5
+ "license": "LGPL-3.0",
6
+ "author": "RemRyahirev",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/RemRyahirev/onebun.git",
10
+ "directory": "packages/logger"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/RemRyahirev/onebun/issues"
14
+ },
15
+ "homepage": "https://github.com/RemRyahirev/onebun/tree/master/packages/logger#readme",
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "registry": "https://registry.npmjs.org/"
19
+ },
20
+ "files": [
21
+ "src",
22
+ "README.md"
23
+ ],
24
+ "main": "src/index.ts",
25
+ "module": "src/index.ts",
26
+ "types": "src/index.ts",
27
+ "scripts": {
28
+ "test": "bun test"
29
+ },
30
+ "dependencies": {
31
+ "effect": "^3.13.10"
32
+ },
33
+ "devDependencies": {
34
+ "bun-types": "1.2.2"
35
+ },
36
+ "engines": {
37
+ "bun": "1.2.2"
38
+ }
39
+ }
@@ -0,0 +1,257 @@
1
+ import {
2
+ type LogEntry,
3
+ type LogFormatter,
4
+ LogLevel,
5
+ } from './types';
6
+
7
+ /**
8
+ * Maximum recursion depth for object formatting
9
+ */
10
+ const MAX_OBJECT_DEPTH = 3;
11
+
12
+ /**
13
+ * Standard width for level names alignment in console output
14
+ */
15
+ const LEVEL_NAME_WIDTH = 7;
16
+
17
+ /**
18
+ * Number of characters to display from trace/span IDs for brevity
19
+ */
20
+ const TRACE_ID_DISPLAY_LENGTH = 8;
21
+
22
+ /**
23
+ * Colors for console output
24
+ */
25
+ const COLORS: Record<string, string> = {
26
+ [LogLevel.Trace]: '\x1b[90m', // Gray
27
+ [LogLevel.Debug]: '\x1b[36m', // Cyan
28
+ [LogLevel.Info]: '\x1b[32m', // Green
29
+ [LogLevel.Warning]: '\x1b[33m', // Yellow
30
+ [LogLevel.Error]: '\x1b[31m', // Red
31
+ [LogLevel.Fatal]: '\x1b[35m', // Magenta
32
+ RESET: '\x1b[0m',
33
+ DIM: '\x1b[2m',
34
+ BRIGHT: '\x1b[1m',
35
+ };
36
+
37
+ /**
38
+ * Format a value for pretty output
39
+ */
40
+ function formatValue(value: unknown, depth = 0): string {
41
+ if (value === null) {
42
+ return '\x1b[90mnull\x1b[0m';
43
+ }
44
+ if (value === undefined) {
45
+ return '\x1b[90mundefined\x1b[0m';
46
+ }
47
+
48
+ if (typeof value === 'string') {
49
+ return `\x1b[32m"${value}"\x1b[0m`;
50
+ }
51
+
52
+ if (typeof value === 'number') {
53
+ return `\x1b[33m${value}\x1b[0m`;
54
+ }
55
+
56
+ if (typeof value === 'boolean') {
57
+ return `\x1b[35m${value}\x1b[0m`;
58
+ }
59
+
60
+ if (value instanceof Date) {
61
+ return `\x1b[36m${value.toISOString()}\x1b[0m`;
62
+ }
63
+
64
+ if (value instanceof Error) {
65
+ return `\x1b[31m${value.name}: ${value.message}\x1b[0m`;
66
+ }
67
+
68
+ if (Array.isArray(value)) {
69
+ if (depth > MAX_OBJECT_DEPTH) {
70
+ return '\x1b[90m[Array]\x1b[0m';
71
+ }
72
+
73
+ const items = value.map((item) => formatValue(item, depth + 1));
74
+ if (items.length === 0) {
75
+ return '\x1b[90m[]\x1b[0m';
76
+ }
77
+
78
+ const indent = ' '.repeat(depth + 1);
79
+ const closeIndent = ' '.repeat(depth);
80
+
81
+ return `\x1b[90m[\x1b[0m\n${indent}${items.join(`,\n${indent}`)}\n${closeIndent}\x1b[90m]\x1b[0m`;
82
+ }
83
+
84
+ if (typeof value === 'object') {
85
+ if (depth > MAX_OBJECT_DEPTH) {
86
+ return '\x1b[90m[Object]\x1b[0m';
87
+ }
88
+
89
+ const entries = Object.entries(value as Record<string, unknown>);
90
+ if (entries.length === 0) {
91
+ return '\x1b[90m{}\x1b[0m';
92
+ }
93
+
94
+ const indent = ' '.repeat(depth + 1);
95
+ const closeIndent = ' '.repeat(depth);
96
+
97
+ const formattedEntries = entries.map(([key, val]) => {
98
+ const formattedKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)
99
+ ? `\x1b[34m${key}\x1b[0m`
100
+ : `\x1b[32m"${key}"\x1b[0m`;
101
+
102
+ return `${formattedKey}: ${formatValue(val, depth + 1)}`;
103
+ });
104
+
105
+ return `\x1b[90m{\x1b[0m\n${indent}${formattedEntries.join(`,\n${indent}`)}\n${closeIndent}\x1b[90m}\x1b[0m`;
106
+ }
107
+
108
+ if (typeof value === 'function') {
109
+ return `\x1b[36m[Function: ${value.name || 'anonymous'}]\x1b[0m`;
110
+ }
111
+
112
+ return String(value);
113
+ }
114
+
115
+ /**
116
+ * Pretty console formatter with colors
117
+ */
118
+ export class PrettyFormatter implements LogFormatter {
119
+ format(entry: LogEntry): string {
120
+ const time = entry.timestamp.toISOString();
121
+
122
+ const level = this.getLevelName(entry.level).padEnd(LEVEL_NAME_WIDTH);
123
+ const color = COLORS[entry.level.toString()] || '';
124
+ const reset = COLORS.RESET;
125
+
126
+ // Add trace information if available
127
+ const traceInfo = entry.trace
128
+ ? ` [trace:${entry.trace.traceId.slice(-TRACE_ID_DISPLAY_LENGTH)} span:${entry.trace.spanId.slice(-TRACE_ID_DISPLAY_LENGTH)}]`
129
+ : '';
130
+
131
+ // Extract className from context for main log line
132
+ let className = '';
133
+ let contextWithoutClassName: Record<string, unknown> = {};
134
+
135
+ if (entry.context) {
136
+ const { className: extractedClassName, ...restContext } = entry.context;
137
+ if (extractedClassName && typeof extractedClassName === 'string') {
138
+ className = ` [${extractedClassName}]`;
139
+ }
140
+ contextWithoutClassName = restContext;
141
+ }
142
+
143
+ let message = `${color}${time} [${level}]${reset}${traceInfo}${className} ${entry.message}`;
144
+
145
+ // Handle additional data first (this is the main new functionality)
146
+ if (contextWithoutClassName.__additionalData) {
147
+ const additionalData = contextWithoutClassName.__additionalData as unknown[];
148
+ if (additionalData.length > 0) {
149
+ const formattedData = additionalData.map((data) => formatValue(data)).join(' ');
150
+ message += ` ${formattedData}`;
151
+ }
152
+ // Remove __additionalData from context since we've processed it
153
+ delete contextWithoutClassName.__additionalData;
154
+ }
155
+
156
+ // Handle regular context (excluding special fields and className)
157
+ if (Object.keys(contextWithoutClassName).length > 0) {
158
+ const contextWithoutSpecialFields = { ...contextWithoutClassName };
159
+ delete contextWithoutSpecialFields.SHOW_CONTEXT;
160
+
161
+ if (Object.keys(contextWithoutSpecialFields).length > 0) {
162
+ message += `\n${COLORS.DIM}Context:${COLORS.RESET} ${formatValue(contextWithoutSpecialFields)}`;
163
+ }
164
+ }
165
+
166
+ if (entry.error) {
167
+ message += `\n${COLORS[LogLevel.Error]}Error:${COLORS.RESET} ${entry.error.stack || entry.error.message}`;
168
+ }
169
+
170
+ return message;
171
+ }
172
+
173
+ private getLevelName(level: LogLevel): string {
174
+ switch (level) {
175
+ case LogLevel.Trace:
176
+ return 'TRACE';
177
+ case LogLevel.Debug:
178
+ return 'DEBUG';
179
+ case LogLevel.Info:
180
+ return 'INFO';
181
+ case LogLevel.Warning:
182
+ return 'WARN';
183
+ case LogLevel.Error:
184
+ return 'ERROR';
185
+ case LogLevel.Fatal:
186
+ return 'FATAL';
187
+ default:
188
+ return 'UNKNOWN';
189
+ }
190
+ }
191
+ }
192
+
193
+ /**
194
+ * JSON formatter for structured logging
195
+ */
196
+ export class JsonFormatter implements LogFormatter {
197
+ format(entry: LogEntry): string {
198
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
199
+ const logData: any = {
200
+ timestamp: entry.timestamp.toISOString(),
201
+ level: this.getLevelName(entry.level),
202
+ message: entry.message,
203
+ };
204
+
205
+ if (entry.trace) {
206
+ logData.trace = {
207
+ traceId: entry.trace.traceId,
208
+ spanId: entry.trace.spanId,
209
+ ...(entry.trace.parentSpanId ? { parentSpanId: entry.trace.parentSpanId } : {}),
210
+ };
211
+ }
212
+
213
+ if (entry.context) {
214
+ const contextData = { ...entry.context };
215
+
216
+ // Extract additional data if present
217
+ if (contextData.__additionalData) {
218
+ logData.additionalData = contextData.__additionalData;
219
+ delete contextData.__additionalData;
220
+ }
221
+
222
+ // Add remaining context
223
+ if (Object.keys(contextData).length > 0) {
224
+ logData.context = contextData;
225
+ }
226
+ }
227
+
228
+ if (entry.error) {
229
+ logData.error = {
230
+ message: entry.error.message,
231
+ stack: entry.error.stack,
232
+ name: entry.error.name,
233
+ };
234
+ }
235
+
236
+ return JSON.stringify(logData);
237
+ }
238
+
239
+ private getLevelName(level: LogLevel): string {
240
+ switch (level) {
241
+ case LogLevel.Trace:
242
+ return 'trace';
243
+ case LogLevel.Debug:
244
+ return 'debug';
245
+ case LogLevel.Info:
246
+ return 'info';
247
+ case LogLevel.Warning:
248
+ return 'warn';
249
+ case LogLevel.Error:
250
+ return 'error';
251
+ case LogLevel.Fatal:
252
+ return 'fatal';
253
+ default:
254
+ return 'unknown';
255
+ }
256
+ }
257
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export {
2
+ createSyncLogger,
3
+ type Logger,
4
+ LoggerService,
5
+ makeLogger,
6
+ type SyncLogger,
7
+ } from './logger';
@@ -0,0 +1,588 @@
1
+ import {
2
+ describe,
3
+ it,
4
+ expect,
5
+ spyOn,
6
+ beforeEach,
7
+ afterEach,
8
+ } from 'bun:test';
9
+ import { Effect } from 'effect';
10
+
11
+ import { JsonFormatter, PrettyFormatter } from './formatter';
12
+ import {
13
+ LoggerService,
14
+ createSyncLogger,
15
+ makeDevLogger,
16
+ } from './logger';
17
+ import { makeLogger } from './logger';
18
+ import { ConsoleTransport } from './transport';
19
+ import { LogLevel, type LogEntry } from './types';
20
+
21
+
22
+ describe('PrettyFormatter', () => {
23
+ it('formats basic info with color, level and message', () => {
24
+ const formatter = new PrettyFormatter();
25
+ const entry: LogEntry = {
26
+ level: LogLevel.Info,
27
+ message: 'Hello',
28
+ timestamp: new Date('2025-01-01T00:00:00.000Z'),
29
+ };
30
+ const out = formatter.format(entry);
31
+ expect(out).toContain('[INFO ]');
32
+ expect(out).toContain('Hello');
33
+ });
34
+
35
+ it('includes className in brackets and additionalData inline', () => {
36
+ const formatter = new PrettyFormatter();
37
+ const entry: LogEntry = {
38
+ level: LogLevel.Debug,
39
+ message: 'Data',
40
+ timestamp: new Date('2025-01-01T00:00:00.000Z'),
41
+ // eslint-disable-next-line @typescript-eslint/naming-convention
42
+ context: { className: 'Test', __additionalData: ['a', 1, true] },
43
+ };
44
+ const out = formatter.format(entry);
45
+ expect(out).toContain('[Test]');
46
+ expect(out).toContain('"a"');
47
+ expect(out).toContain('1');
48
+ expect(out).toContain('true');
49
+ });
50
+
51
+ it('prints context object pretty when present (excluding special fields)', () => {
52
+ const formatter = new PrettyFormatter();
53
+ const entry: LogEntry = {
54
+ level: LogLevel.Warning,
55
+ message: 'Warn',
56
+ timestamp: new Date('2025-01-01T00:00:00.000Z'),
57
+ context: { a: 1, SHOW_CONTEXT: true },
58
+ };
59
+ const out = formatter.format(entry);
60
+ expect(out).toContain('Context:');
61
+ expect(out).toContain('a');
62
+ expect(out).not.toContain('SHOW_CONTEXT');
63
+ });
64
+
65
+ it('includes error stack if error present', () => {
66
+ const formatter = new PrettyFormatter();
67
+ const error = new Error('Boom');
68
+ const entry: LogEntry = {
69
+ level: LogLevel.Error,
70
+ message: 'Err',
71
+ timestamp: new Date('2025-01-01T00:00:00.000Z'),
72
+ error,
73
+ };
74
+ const out = formatter.format(entry);
75
+ expect(out).toContain('Error:');
76
+ expect(out).toContain('Boom');
77
+ });
78
+
79
+ it('adds trace info with last 8 chars', () => {
80
+ const formatter = new PrettyFormatter();
81
+ const entry: LogEntry = {
82
+ level: LogLevel.Info,
83
+ message: 'Traced',
84
+ timestamp: new Date('2025-01-01T00:00:00.000Z'),
85
+ trace: { traceId: '1234567890abcdef', spanId: 'abcdef1234567890' },
86
+ } as LogEntry;
87
+ const out = formatter.format(entry);
88
+ expect(out).toContain('trace:90abcdef');
89
+ expect(out).toContain('span:34567890');
90
+ });
91
+ });
92
+
93
+ describe('JsonFormatter', () => {
94
+ it('produces structured json', () => {
95
+ const formatter = new JsonFormatter();
96
+ const entry: LogEntry = {
97
+ level: LogLevel.Info,
98
+ message: 'Hi',
99
+ timestamp: new Date('2025-01-01T00:00:00.000Z'),
100
+ // eslint-disable-next-line @typescript-eslint/naming-convention
101
+ context: { x: 1, __additionalData: [2, 'b'] },
102
+ error: new Error('E'),
103
+ trace: { traceId: 't', spanId: 's', parentSpanId: 'p' },
104
+ };
105
+ const obj = JSON.parse(formatter.format(entry));
106
+ expect(obj.level).toBe('info');
107
+ expect(obj.message).toBe('Hi');
108
+ expect(obj.context).toEqual({ x: 1 });
109
+ expect(obj.additionalData).toEqual([2, 'b']);
110
+ expect(obj.error.message).toBe('E');
111
+ expect(obj.trace.traceId).toBe('t');
112
+ expect(obj.trace.parentSpanId).toBe('p');
113
+ });
114
+ });
115
+
116
+ describe('ConsoleTransport', () => {
117
+ const transport = new ConsoleTransport();
118
+ let logSpy: ReturnType<typeof spyOn>;
119
+ let infoSpy: ReturnType<typeof spyOn>;
120
+ let warnSpy: ReturnType<typeof spyOn>;
121
+ let errorSpy: ReturnType<typeof spyOn>;
122
+
123
+ beforeEach(() => {
124
+ logSpy = spyOn(console, 'log').mockImplementation(() => undefined);
125
+ infoSpy = spyOn(console, 'info').mockImplementation(() => undefined);
126
+ warnSpy = spyOn(console, 'warn').mockImplementation(() => undefined);
127
+ errorSpy = spyOn(console, 'error').mockImplementation(() => undefined);
128
+ });
129
+
130
+ afterEach(() => {
131
+ logSpy.mockRestore();
132
+ infoSpy.mockRestore();
133
+ warnSpy.mockRestore();
134
+ errorSpy.mockRestore();
135
+ });
136
+
137
+ const ts = new Date();
138
+ const base: Omit<LogEntry, 'level'|'message'> = { timestamp: ts };
139
+
140
+ it('routes info to console.info', () => {
141
+ Effect.runSync(transport.log('x', { ...base, level: LogLevel.Info, message: 'm' }));
142
+ expect(infoSpy).toHaveBeenCalled();
143
+ });
144
+
145
+ it('routes warn to console.warn', () => {
146
+ Effect.runSync(transport.log('x', { ...base, level: LogLevel.Warning, message: 'm' }));
147
+ expect(warnSpy).toHaveBeenCalled();
148
+ });
149
+
150
+ it('routes error/fatal to console.error and others to console.log', () => {
151
+ Effect.runSync(transport.log('x', { ...base, level: LogLevel.Error, message: 'm' }));
152
+ expect(errorSpy).toHaveBeenCalled();
153
+ Effect.runSync(transport.log('x', { ...base, level: LogLevel.Fatal, message: 'm' }));
154
+ expect(errorSpy).toHaveBeenCalledTimes(2);
155
+ Effect.runSync(transport.log('x', { ...base, level: LogLevel.Debug, message: 'm' }));
156
+ expect(logSpy).toHaveBeenCalled();
157
+ });
158
+
159
+ it('routes trace to console.log', () => {
160
+ Effect.runSync(transport.log('trace message', { ...base, level: LogLevel.Trace, message: 'trace' }));
161
+ expect(logSpy).toHaveBeenCalledWith('trace message');
162
+ });
163
+
164
+ it('should create ConsoleTransport instance', () => {
165
+ const newTransport = new ConsoleTransport();
166
+ expect(newTransport).toBeInstanceOf(ConsoleTransport);
167
+ expect(typeof newTransport.log).toBe('function');
168
+ });
169
+
170
+ it('should create ConsoleTransport with implicit constructor', () => {
171
+ // Test explicit constructor call to improve coverage
172
+ const transport1 = new ConsoleTransport();
173
+ const transport2 = new ConsoleTransport();
174
+
175
+ expect(transport1).toBeInstanceOf(ConsoleTransport);
176
+ expect(transport2).toBeInstanceOf(ConsoleTransport);
177
+ expect(transport1).not.toBe(transport2); // Different instances
178
+ });
179
+
180
+ it('should handle unknown log level', () => {
181
+ // Test the default case in switch statement
182
+ const unknownLevel = 999 as LogLevel;
183
+ Effect.runSync(transport.log('unknown level', { ...base, level: unknownLevel, message: 'test' }));
184
+ expect(logSpy).toHaveBeenCalledWith('unknown level');
185
+ });
186
+ });
187
+
188
+ describe('Logger + SyncLogger basic flow', () => {
189
+ it('filters by minLevel and merges contexts and additional data', () => {
190
+ // Create a minimal logger by constructing our own transport that captures output
191
+ const outputs: string[] = [];
192
+ const transport = new (class extends ConsoleTransport {
193
+ override log(formattedEntry: string, entry: LogEntry) {
194
+ return Effect.sync(() => {
195
+ outputs.push(formattedEntry + '|' + entry.level);
196
+ });
197
+ }
198
+ })();
199
+
200
+ const loggerLayer = makeDevLogger({ minLevel: LogLevel.Info, transport });
201
+
202
+ // Extract the underlying logger by creating a small helper service instance
203
+ // We cannot easily read from Layer in tests, so we re-create SyncLogger through
204
+ // the public API using LoggerService tag and Effect runtime.
205
+
206
+ // Build a ad-hoc logger effect from the layer and run logging through Effect directly
207
+ const loggerEffect = Effect.flatMap(LoggerService, (logger) => Effect.succeed(logger));
208
+ const provided = Effect.provide(loggerEffect, loggerLayer);
209
+ const logger = Effect.runSync(provided);
210
+
211
+ // Create sync wrapper for easier calls
212
+ const sync = createSyncLogger(logger);
213
+
214
+ sync.debug('dropped'); // below Info, should be filtered
215
+ sync.info('hello', { a: 1 }, 'xtra');
216
+
217
+ expect(outputs.length).toBe(1);
218
+ expect(outputs[0]).toContain('hello');
219
+ expect(outputs[0]).toContain('Context');
220
+ });
221
+
222
+ it('child adds context and uses global trace context in SyncLogger', () => {
223
+ const outputs: LogEntry[] = [];
224
+ const transport = new (class extends ConsoleTransport {
225
+ override log(_formattedEntry: string, entry: LogEntry) {
226
+ return Effect.sync(() => {
227
+ outputs.push(entry);
228
+ });
229
+ }
230
+ })();
231
+
232
+ const layer = makeDevLogger({ transport });
233
+ const logger = Effect.runSync(Effect.provide(Effect.flatMap(LoggerService, (l) => Effect.succeed(l)), layer));
234
+ const sync = createSyncLogger(logger);
235
+
236
+ // set global trace for SyncLogger path
237
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
238
+ (globalThis as any).__onebunCurrentTraceContext = { traceId: 't123456789', spanId: 's123456789' };
239
+
240
+ const child = sync.child({ className: 'Child' });
241
+ child.info('hi');
242
+
243
+ expect(outputs[0].context?.className).toBe('Child');
244
+ expect(outputs[0].trace?.traceId).toBe('t123456789');
245
+ });
246
+ });
247
+
248
+
249
+ // Additional tests to improve coverage near 100%
250
+
251
+ describe('PrettyFormatter - additional value shapes', () => {
252
+ it('formats various value types in additionalData', () => {
253
+ const f = new PrettyFormatter();
254
+ const err = new Error('X');
255
+ const fn = function namedFn() {
256
+ return 1;
257
+ };
258
+ const entry: LogEntry = {
259
+ level: LogLevel.Debug,
260
+ message: 'v',
261
+ timestamp: new Date('2025-01-01T00:00:00.000Z'),
262
+ // eslint-disable-next-line @typescript-eslint/naming-convention
263
+ context: { __additionalData: [null, undefined, 123, 's', false, new Date('2025-01-01T00:00:00.000Z'), err, fn] },
264
+ };
265
+ const out = f.format(entry);
266
+ expect(out).toContain('null');
267
+ expect(out).toContain('undefined');
268
+ expect(out).toContain('123');
269
+ expect(out).toContain('"s"');
270
+ expect(out).toContain('false');
271
+ expect(out).toContain('2025-01-01T00:00:00.000Z');
272
+ expect(out).toContain('Error: X');
273
+ expect(out).toContain('[Function: namedFn]');
274
+ });
275
+
276
+ it('pretty prints empty and deep structures with truncation', () => {
277
+ const f = new PrettyFormatter();
278
+ const deep = { a: { b: { c: { d: 1 } } } };
279
+ const entry: LogEntry = {
280
+ level: LogLevel.Info,
281
+ message: 'ctx',
282
+ timestamp: new Date('2025-01-01T00:00:00.000Z'),
283
+ context: { emptyObj: {}, emptyArr: [], deep },
284
+ };
285
+ const out = f.format(entry);
286
+ expect(out).toContain('emptyObj');
287
+ expect(out).toContain('{}');
288
+ expect(out).toContain('emptyArr');
289
+ expect(out).toContain('[]');
290
+ // при глубокой структуре встречается маркер [Object]
291
+ expect(out).toContain('[Object]');
292
+ });
293
+ });
294
+
295
+ describe('JsonFormatter - variants', () => {
296
+ it('omits context and additionalData when absent; trace without parent', () => {
297
+ const f = new JsonFormatter();
298
+ const entry: LogEntry = {
299
+ level: LogLevel.Trace,
300
+ message: 'j',
301
+ timestamp: new Date('2025-01-01T00:00:00.000Z'),
302
+ trace: { traceId: 'tt', spanId: 'ss' },
303
+ };
304
+ const o = JSON.parse(f.format(entry));
305
+ expect(o.level).toBe('trace');
306
+ expect(o.context).toBeUndefined();
307
+ expect(o.additionalData).toBeUndefined();
308
+ expect(o.trace.traceId).toBe('tt');
309
+ expect(o.trace.parentSpanId).toBeUndefined();
310
+ });
311
+ });
312
+
313
+ describe('LoggerImpl methods and makeLogger env selection', () => {
314
+ it('calls all level methods at or above minLevel', () => {
315
+ const seen: LogEntry[] = [];
316
+ const transport = new (class extends ConsoleTransport {
317
+ override log(_formatted: string, e: LogEntry) {
318
+ return Effect.sync(() => {
319
+ seen.push(e);
320
+ });
321
+ }
322
+ })();
323
+ const layer = makeDevLogger({ minLevel: LogLevel.Trace, transport });
324
+ const logger = Effect.runSync(Effect.provide(Effect.flatMap(LoggerService, (l) => Effect.succeed(l)), layer));
325
+ const sync = createSyncLogger(logger);
326
+ sync.trace('t');
327
+ sync.debug('d');
328
+ sync.info('i');
329
+ sync.warn('w');
330
+ sync.error('e', new Error('E1'));
331
+ sync.fatal('f', new Error('E2'));
332
+ expect(seen.map((e) => e.level)).toEqual([
333
+ LogLevel.Trace, LogLevel.Debug, LogLevel.Info, LogLevel.Warning, LogLevel.Error, LogLevel.Fatal,
334
+ ]);
335
+ });
336
+
337
+ it('makeLogger picks prod logger when NODE_ENV=production', () => {
338
+ const prev = process.env.NODE_ENV;
339
+ process.env.NODE_ENV = 'production';
340
+ const seen: LogEntry[] = [];
341
+ const transport = new (class extends ConsoleTransport {
342
+ override log(_formatted: string, e: LogEntry) {
343
+ return Effect.sync(() => {
344
+ seen.push(e);
345
+ });
346
+ }
347
+ })();
348
+ const layer = makeLogger({ transport });
349
+ const logger = Effect.runSync(Effect.provide(Effect.flatMap(LoggerService, (l) => Effect.succeed(l)), layer));
350
+ const sync = createSyncLogger(logger);
351
+ // Debug должен быть отфильтрован в prod (minLevel Info)
352
+ sync.debug('dbg');
353
+ sync.info('ok');
354
+ expect(seen.length).toBe(1);
355
+ expect(seen[0].level).toBe(LogLevel.Info);
356
+ process.env.NODE_ENV = prev;
357
+ });
358
+ });
359
+
360
+ describe('LoggerImpl trace context fallback to __onebunTraceService', () => {
361
+ it('uses global trace service when current trace context is absent', () => {
362
+ // очистить direct context
363
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
364
+ (globalThis as any).__onebunCurrentTraceContext = undefined;
365
+ const outputs: LogEntry[] = [];
366
+ const transport = new (class extends ConsoleTransport {
367
+ override log(_f: string, e: LogEntry) {
368
+ return Effect.sync(() => {
369
+ outputs.push(e);
370
+ });
371
+ }
372
+ })();
373
+ // подготовить псевдо trace service
374
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
375
+ (globalThis as any).__onebunTraceService = {
376
+ getCurrentTraceContext: () => Effect.succeed({ traceId: 'svc-trace', spanId: 'svc-span' }),
377
+ };
378
+
379
+ const layer = makeDevLogger({ transport });
380
+ const logger = Effect.runSync(Effect.provide(Effect.flatMap(LoggerService, (l) => Effect.succeed(l)), layer));
381
+ const sync = createSyncLogger(logger);
382
+ sync.info('ping');
383
+
384
+ expect(outputs[0].trace?.traceId).toBe('svc-trace');
385
+ });
386
+ });
387
+
388
+
389
+ // Added tests to raise function coverage for formatter.ts
390
+
391
+ describe('Formatter getLevelName coverage', () => {
392
+ it('JsonFormatter maps all levels to lowercase names', () => {
393
+ const f = new JsonFormatter();
394
+ const ts = new Date('2025-01-01T00:00:00.000Z');
395
+ const mk = (level: LogLevel) => JSON.parse(f.format({ level, message: 'm', timestamp: ts })).level as string;
396
+ expect(mk(LogLevel.Trace)).toBe('trace');
397
+ expect(mk(LogLevel.Debug)).toBe('debug');
398
+ expect(mk(LogLevel.Info)).toBe('info');
399
+ expect(mk(LogLevel.Warning)).toBe('warn');
400
+ expect(mk(LogLevel.Error)).toBe('error');
401
+ expect(mk(LogLevel.Fatal)).toBe('fatal');
402
+ });
403
+
404
+ it('PrettyFormatter maps TRACE and FATAL to bracketed level names', () => {
405
+ const f = new PrettyFormatter();
406
+ const ts = new Date('2025-01-01T00:00:00.000Z');
407
+ const outTrace = f.format({ level: LogLevel.Trace, message: 't', timestamp: ts });
408
+ const outFatal = f.format({ level: LogLevel.Fatal, message: 'f', timestamp: ts });
409
+ expect(outTrace).toContain('[TRACE ]');
410
+ expect(outFatal).toContain('[FATAL ]');
411
+ });
412
+ });
413
+
414
+ describe('formatValue rare branches via PrettyFormatter additionalData', () => {
415
+ it('handles empty array and multi-element array with pretty multi-line', () => {
416
+ const f = new PrettyFormatter();
417
+ const ts = new Date('2025-01-01T00:00:00.000Z');
418
+ const outEmpty = f.format({
419
+ level: LogLevel.Debug,
420
+ message: 'empty',
421
+ timestamp: ts,
422
+ // eslint-disable-next-line @typescript-eslint/naming-convention
423
+ context: { __additionalData: [[]] },
424
+ });
425
+ expect(outEmpty).toContain('[]');
426
+
427
+ const outNonEmpty = f.format({
428
+ level: LogLevel.Debug,
429
+ message: 'nonempty',
430
+ timestamp: ts,
431
+ // eslint-disable-next-line @typescript-eslint/naming-convention
432
+ context: { __additionalData: [[1, 2, 3]] },
433
+ });
434
+ // Ожидаем многострочное форматирование массива (наличие символов [ и ] с переносами строк)
435
+ expect(outNonEmpty).toMatch(/\[\x1b\[0m\n/); // присутствует перевод строки после [
436
+ });
437
+
438
+ it('handles deep nested array producing [Array] marker when depth exceeded', () => {
439
+ const f = new PrettyFormatter();
440
+ const deepArr = [[[[[1]]]]]; // глубина 5 (> MAX_OBJECT_DEPTH)
441
+ const out = f.format({
442
+ level: LogLevel.Debug,
443
+ message: 'deep',
444
+ timestamp: new Date('2025-01-01T00:00:00.000Z'),
445
+ // eslint-disable-next-line @typescript-eslint/naming-convention
446
+ context: { __additionalData: [deepArr] },
447
+ });
448
+ expect(out).toContain('[Array]');
449
+ });
450
+
451
+ it('formats object keys that require quotes', () => {
452
+ const f = new PrettyFormatter();
453
+ const out = f.format({
454
+ level: LogLevel.Info,
455
+ message: 'obj',
456
+ timestamp: new Date('2025-01-01T00:00:00.000Z'),
457
+ // eslint-disable-next-line @typescript-eslint/naming-convention
458
+ context: { 'a-b': 1 },
459
+ });
460
+ // Ключ с дефисом должен печататься в кавычках
461
+ expect(out).toContain('"a-b"');
462
+ });
463
+
464
+ it('prints anonymous function with fallback name and covers String(value) for symbol and bigint', () => {
465
+ const f = new PrettyFormatter();
466
+ // анонимная функция
467
+
468
+ const anon = function () {
469
+ return 0;
470
+ };
471
+ const sym = Symbol('s');
472
+ const big = BigInt(42);
473
+ const out = f.format({
474
+ level: LogLevel.Debug,
475
+ message: 'misc',
476
+ timestamp: new Date('2025-01-01T00:00:00.000Z'),
477
+ // eslint-disable-next-line @typescript-eslint/naming-convention
478
+ context: { __additionalData: [anon, sym, big] },
479
+ });
480
+ expect(out).toContain('[Function:');
481
+ // символы и бигинты проходят в String(value)
482
+ expect(out).toContain('Symbol(s)');
483
+ expect(out).toContain('42');
484
+ });
485
+ });
486
+
487
+ describe('Formatter edge cases and additional coverage', () => {
488
+ it('PrettyFormatter should handle Set and Map objects', () => {
489
+ const formatter = new PrettyFormatter();
490
+ const set = new Set([1, 2, 3]);
491
+ const map = new Map([['key', 'value']]);
492
+
493
+ const out = formatter.format({
494
+ level: LogLevel.Info,
495
+ message: 'collections',
496
+ timestamp: new Date('2025-01-01T00:00:00.000Z'),
497
+ // eslint-disable-next-line @typescript-eslint/naming-convention
498
+ context: { __additionalData: [set, map] },
499
+ });
500
+
501
+ // Sets and Maps are formatted as empty objects {} in the formatter
502
+ expect(out).toContain('{}');
503
+ expect(out).toContain('collections');
504
+ });
505
+
506
+ it('PrettyFormatter should handle Date objects', () => {
507
+ const formatter = new PrettyFormatter();
508
+ const date = new Date('2025-01-01T12:00:00.000Z');
509
+
510
+ const out = formatter.format({
511
+ level: LogLevel.Info,
512
+ message: 'dates',
513
+ timestamp: new Date('2025-01-01T00:00:00.000Z'),
514
+ // eslint-disable-next-line @typescript-eslint/naming-convention
515
+ context: { __additionalData: [date] },
516
+ });
517
+
518
+ expect(out).toContain('2025');
519
+ });
520
+
521
+ it('PrettyFormatter should handle class instances', () => {
522
+ const formatter = new PrettyFormatter();
523
+
524
+ class TestClass {
525
+ prop = 'value';
526
+ }
527
+
528
+ const instance = new TestClass();
529
+
530
+ const out = formatter.format({
531
+ level: LogLevel.Info,
532
+ message: 'instance',
533
+ timestamp: new Date('2025-01-01T00:00:00.000Z'),
534
+ // eslint-disable-next-line @typescript-eslint/naming-convention
535
+ context: { __additionalData: [instance] },
536
+ });
537
+
538
+ // Class instances show their properties, not class name
539
+ expect(out).toContain('prop');
540
+ expect(out).toContain('value');
541
+ });
542
+
543
+ it('JsonFormatter should handle all log levels correctly', () => {
544
+ const formatter = new JsonFormatter();
545
+ const timestamp = new Date('2025-01-01T00:00:00.000Z');
546
+
547
+ // Test all log levels
548
+ const levels = [LogLevel.Trace, LogLevel.Debug, LogLevel.Info, LogLevel.Warning, LogLevel.Error, LogLevel.Fatal];
549
+
550
+ levels.forEach(level => {
551
+ const out = formatter.format({
552
+ level,
553
+ message: 'test',
554
+ timestamp,
555
+ });
556
+
557
+ const parsed = JSON.parse(out);
558
+ expect(parsed.level).toBeDefined();
559
+ expect(parsed.message).toBe('test');
560
+ });
561
+ });
562
+
563
+ it('PrettyFormatter should handle complex nested structures with formatting', () => {
564
+ const formatter = new PrettyFormatter();
565
+
566
+ const complexData = {
567
+ level1: {
568
+ level2: {
569
+ level3: {
570
+ deep: 'value',
571
+ array: [1, 2, { nested: true }],
572
+ },
573
+ },
574
+ },
575
+ };
576
+
577
+ const out = formatter.format({
578
+ level: LogLevel.Info,
579
+ message: 'complex',
580
+ timestamp: new Date('2025-01-01T00:00:00.000Z'),
581
+ context: complexData,
582
+ });
583
+
584
+ expect(out).toContain('level1');
585
+ expect(out).toContain('level2');
586
+ expect(out).toContain('level3');
587
+ });
588
+ });
package/src/logger.ts ADDED
@@ -0,0 +1,308 @@
1
+ import {
2
+ Context,
3
+ Effect,
4
+ FiberRef,
5
+ Layer,
6
+ } from 'effect';
7
+
8
+ import { JsonFormatter, PrettyFormatter } from './formatter';
9
+ import { ConsoleTransport } from './transport';
10
+ import {
11
+ type LoggerConfig,
12
+ LogLevel,
13
+ type TraceInfo,
14
+ } from './types';
15
+
16
+ /**
17
+ * Custom logger service interface
18
+ */
19
+ export type Logger = {
20
+ trace(message: string, ...args: unknown[]): Effect.Effect<void>;
21
+ debug(message: string, ...args: unknown[]): Effect.Effect<void>;
22
+ info(message: string, ...args: unknown[]): Effect.Effect<void>;
23
+ warn(message: string, ...args: unknown[]): Effect.Effect<void>;
24
+ error(message: string, ...args: unknown[]): Effect.Effect<void>;
25
+ fatal(message: string, ...args: unknown[]): Effect.Effect<void>;
26
+ child(context: Record<string, unknown>): Logger;
27
+ };
28
+
29
+ /**
30
+ * Synchronous logger interface for convenience
31
+ */
32
+ export type SyncLogger = {
33
+ trace(message: string, ...args: unknown[]): void;
34
+ debug(message: string, ...args: unknown[]): void;
35
+ info(message: string, ...args: unknown[]): void;
36
+ warn(message: string, ...args: unknown[]): void;
37
+ error(message: string, ...args: unknown[]): void;
38
+ fatal(message: string, ...args: unknown[]): void;
39
+ child(context: Record<string, unknown>): SyncLogger;
40
+ };
41
+
42
+ /**
43
+ * Context tag for the Logger service
44
+ */
45
+ // eslint-disable-next-line @typescript-eslint/naming-convention
46
+ export const LoggerService = Context.GenericTag<Logger>('LoggerService');
47
+
48
+ /**
49
+ * Current trace context stored in fiber for automatic trace inclusion in logs
50
+ */
51
+ // eslint-disable-next-line @typescript-eslint/naming-convention
52
+ export const CurrentLoggerTraceContext = FiberRef.unsafeMake<TraceInfo | null>(null);
53
+
54
+ /**
55
+ * Parse arguments to extract error, context and additional data
56
+ */
57
+ function parseLogArgs(args: unknown[]): {
58
+ error?: Error;
59
+ context?: Record<string, unknown>;
60
+ additionalData?: unknown[];
61
+ } {
62
+ if (args.length === 0) {
63
+ return {};
64
+ }
65
+
66
+ let error: Error | undefined;
67
+ let context: Record<string, unknown> | undefined;
68
+ const additionalData: unknown[] = [];
69
+
70
+ for (const arg of args) {
71
+ if (arg instanceof Error) {
72
+ // First error found becomes the main error
73
+ if (!error) {
74
+ error = arg;
75
+ } else {
76
+ additionalData.push(arg);
77
+ }
78
+ } else if (
79
+ arg &&
80
+ typeof arg === 'object' &&
81
+ !Array.isArray(arg) &&
82
+ arg.constructor === Object
83
+ ) {
84
+ // Plain objects are merged into context
85
+ context = { ...context, ...(arg as Record<string, unknown>) };
86
+ } else {
87
+ // Everything else goes to additional data
88
+ additionalData.push(arg);
89
+ }
90
+ }
91
+
92
+ return {
93
+ error,
94
+ context: Object.keys(context || {}).length > 0 ? context : undefined,
95
+ additionalData: additionalData.length > 0 ? additionalData : undefined,
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Simple logger implementation that uses our formatters and transports
101
+ */
102
+ class LoggerImpl implements Logger {
103
+ constructor(
104
+ private config: LoggerConfig,
105
+ private context: Record<string, unknown> = {},
106
+ ) {}
107
+
108
+ private log(level: LogLevel, message: string, ...args: unknown[]): Effect.Effect<void> {
109
+ // Check minimum logging level
110
+ if (level < this.config.minLevel) {
111
+ return Effect.succeed(undefined);
112
+ }
113
+
114
+ return Effect.flatMap(FiberRef.get(CurrentLoggerTraceContext), (traceInfo) => {
115
+ // Try to get trace context from global context or trace service
116
+ let currentTraceInfo = traceInfo;
117
+ if (!currentTraceInfo && typeof globalThis !== 'undefined') {
118
+ // Try global trace context first
119
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
120
+ const globalTraceContext = (globalThis as any).__onebunCurrentTraceContext;
121
+ if (globalTraceContext && globalTraceContext.traceId) {
122
+ currentTraceInfo = globalTraceContext;
123
+ } else {
124
+ // Fallback to trace service
125
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
126
+ const globalTraceService = (globalThis as any).__onebunTraceService;
127
+ if (globalTraceService && globalTraceService.getCurrentTraceContext) {
128
+ try {
129
+ // Extract current trace context from global trace service
130
+
131
+ const currentContext = Effect.runSync(
132
+ globalTraceService.getCurrentTraceContext(),
133
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
134
+ ) as any;
135
+ if (currentContext && currentContext.traceId) {
136
+ currentTraceInfo = {
137
+ traceId: currentContext.traceId,
138
+ spanId: currentContext.spanId,
139
+ parentSpanId: currentContext.parentSpanId,
140
+ };
141
+ }
142
+ } catch {
143
+ // Ignore errors getting trace context
144
+ }
145
+ }
146
+ }
147
+ }
148
+
149
+ // Parse additional arguments
150
+ const { error, context: argsContext, additionalData } = parseLogArgs(args);
151
+
152
+ // Merge contexts
153
+ const mergedContext = {
154
+ ...this.config.defaultContext,
155
+ ...this.context,
156
+ ...argsContext,
157
+ };
158
+
159
+ // Add additional data to context if present
160
+ if (additionalData && additionalData.length > 0) {
161
+ mergedContext.__additionalData = additionalData;
162
+ }
163
+
164
+ // Create log entry
165
+ const entry = {
166
+ level,
167
+ message,
168
+ timestamp: new Date(),
169
+ context: Object.keys(mergedContext).length > 0 ? mergedContext : undefined,
170
+ error,
171
+ trace: currentTraceInfo || undefined,
172
+ };
173
+
174
+ // Format and send through transport
175
+ const formattedEntry = this.config.formatter.format(entry);
176
+
177
+ return this.config.transport.log(formattedEntry, entry);
178
+ });
179
+ }
180
+
181
+ trace(message: string, ...args: unknown[]): Effect.Effect<void> {
182
+ return this.log(LogLevel.Trace, message, ...args);
183
+ }
184
+
185
+ debug(message: string, ...args: unknown[]): Effect.Effect<void> {
186
+ return this.log(LogLevel.Debug, message, ...args);
187
+ }
188
+
189
+ info(message: string, ...args: unknown[]): Effect.Effect<void> {
190
+ return this.log(LogLevel.Info, message, ...args);
191
+ }
192
+
193
+ warn(message: string, ...args: unknown[]): Effect.Effect<void> {
194
+ return this.log(LogLevel.Warning, message, ...args);
195
+ }
196
+
197
+ error(message: string, ...args: unknown[]): Effect.Effect<void> {
198
+ return this.log(LogLevel.Error, message, ...args);
199
+ }
200
+
201
+ fatal(message: string, ...args: unknown[]): Effect.Effect<void> {
202
+ return this.log(LogLevel.Fatal, message, ...args);
203
+ }
204
+
205
+ child(context: Record<string, unknown>): Logger {
206
+ return new LoggerImpl(this.config, { ...this.context, ...context });
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Creates a development logger with pretty console output
212
+ */
213
+ export const makeDevLogger = (config?: Partial<LoggerConfig>): Layer.Layer<Logger> => {
214
+ return Layer.succeed(
215
+ LoggerService,
216
+ new LoggerImpl({
217
+ minLevel: LogLevel.Debug,
218
+ formatter: new PrettyFormatter(),
219
+ transport: new ConsoleTransport(),
220
+ defaultContext: {},
221
+ ...config,
222
+ }),
223
+ );
224
+ };
225
+
226
+ /**
227
+ * Creates a production logger with JSON output
228
+ */
229
+ export const makeProdLogger = (config?: Partial<LoggerConfig>): Layer.Layer<Logger> => {
230
+ return Layer.succeed(
231
+ LoggerService,
232
+ new LoggerImpl({
233
+ minLevel: LogLevel.Info,
234
+ formatter: new JsonFormatter(),
235
+ transport: new ConsoleTransport(),
236
+ defaultContext: {},
237
+ ...config,
238
+ }),
239
+ );
240
+ };
241
+
242
+ /**
243
+ * Synchronous logger wrapper
244
+ */
245
+ class SyncLoggerImpl implements SyncLogger {
246
+ constructor(private logger: Logger) {}
247
+
248
+ private runWithTraceContext<T>(effect: Effect.Effect<T>): T {
249
+ // Try to get trace context from global context
250
+ let traceEffect = effect;
251
+ if (typeof globalThis !== 'undefined') {
252
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
253
+ const globalTraceContext = (globalThis as any).__onebunCurrentTraceContext;
254
+ if (globalTraceContext && globalTraceContext.traceId) {
255
+ traceEffect = Effect.provide(
256
+ Effect.flatMap(FiberRef.set(CurrentLoggerTraceContext, globalTraceContext), () => effect),
257
+ Layer.empty,
258
+ );
259
+ }
260
+ }
261
+
262
+ return Effect.runSync(traceEffect);
263
+ }
264
+
265
+ trace(message: string, ...args: unknown[]): void {
266
+ this.runWithTraceContext(this.logger.trace(message, ...args));
267
+ }
268
+
269
+ debug(message: string, ...args: unknown[]): void {
270
+ this.runWithTraceContext(this.logger.debug(message, ...args));
271
+ }
272
+
273
+ info(message: string, ...args: unknown[]): void {
274
+ this.runWithTraceContext(this.logger.info(message, ...args));
275
+ }
276
+
277
+ warn(message: string, ...args: unknown[]): void {
278
+ this.runWithTraceContext(this.logger.warn(message, ...args));
279
+ }
280
+
281
+ error(message: string, ...args: unknown[]): void {
282
+ this.runWithTraceContext(this.logger.error(message, ...args));
283
+ }
284
+
285
+ fatal(message: string, ...args: unknown[]): void {
286
+ this.runWithTraceContext(this.logger.fatal(message, ...args));
287
+ }
288
+
289
+ child(context: Record<string, unknown>): SyncLogger {
290
+ return new SyncLoggerImpl(this.logger.child(context));
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Create a synchronous logger from async logger
296
+ */
297
+ export const createSyncLogger = (logger: Logger): SyncLogger => {
298
+ return new SyncLoggerImpl(logger);
299
+ };
300
+
301
+ /**
302
+ * Create a logger based on NODE_ENV
303
+ */
304
+ export const makeLogger = (config?: Partial<LoggerConfig>): Layer.Layer<Logger> => {
305
+ const isDev = process.env.NODE_ENV !== 'production';
306
+
307
+ return isDev ? makeDevLogger(config) : makeProdLogger(config);
308
+ };
@@ -0,0 +1,37 @@
1
+ import { Effect } from 'effect';
2
+
3
+ import {
4
+ type LogEntry,
5
+ LogLevel,
6
+ type LogTransport,
7
+ } from './types';
8
+
9
+ /**
10
+ * Console transport for logging
11
+ */
12
+ export class ConsoleTransport implements LogTransport {
13
+ log(formattedEntry: string, entry: LogEntry): Effect.Effect<void> {
14
+ return Effect.sync(() => {
15
+ switch (entry.level) {
16
+ case LogLevel.Error:
17
+ case LogLevel.Fatal:
18
+ // eslint-disable-next-line no-console
19
+ console.error(formattedEntry);
20
+ break;
21
+ case LogLevel.Warning:
22
+ // eslint-disable-next-line no-console
23
+ console.warn(formattedEntry);
24
+ break;
25
+ case LogLevel.Info:
26
+ // eslint-disable-next-line no-console
27
+ console.info(formattedEntry);
28
+ break;
29
+ case LogLevel.Debug:
30
+ case LogLevel.Trace:
31
+ default:
32
+ // eslint-disable-next-line no-console
33
+ console.log(formattedEntry);
34
+ }
35
+ });
36
+ }
37
+ }
package/src/types.ts ADDED
@@ -0,0 +1,61 @@
1
+ import type { Effect } from 'effect';
2
+
3
+ /**
4
+ * Определяем уровни логирования
5
+ */
6
+ /* eslint-disable no-magic-numbers -- Standard log levels based on syslog defined in one place */
7
+ export enum LogLevel {
8
+ Fatal = 60,
9
+ Error = 50,
10
+ Warning = 40,
11
+ Info = 30,
12
+ Debug = 20,
13
+ Trace = 10,
14
+ None = 0,
15
+ }
16
+ /* eslint-enable no-magic-numbers */
17
+
18
+ /**
19
+ * Trace information for log entries
20
+ */
21
+ export interface TraceInfo {
22
+ traceId: string;
23
+ spanId: string;
24
+ parentSpanId?: string;
25
+ }
26
+
27
+ /**
28
+ * Log entry structure
29
+ */
30
+ export interface LogEntry {
31
+ level: LogLevel;
32
+ message: string;
33
+ timestamp: Date;
34
+ context?: Record<string, unknown>;
35
+ error?: Error;
36
+ trace?: TraceInfo;
37
+ }
38
+
39
+ /**
40
+ * Log formatter interface
41
+ */
42
+ export interface LogFormatter {
43
+ format(entry: LogEntry): string;
44
+ }
45
+
46
+ /**
47
+ * Log transport interface
48
+ */
49
+ export interface LogTransport {
50
+ log(formattedEntry: string, entry: LogEntry): Effect.Effect<void>;
51
+ }
52
+
53
+ /**
54
+ * Logger configuration
55
+ */
56
+ export interface LoggerConfig {
57
+ minLevel: LogLevel;
58
+ formatter: LogFormatter;
59
+ transport: LogTransport;
60
+ defaultContext?: Record<string, unknown>;
61
+ }