@mantiq/logging 0.0.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 ADDED
@@ -0,0 +1,19 @@
1
+ # @mantiq/logging
2
+
3
+ Channel-based logging for MantiqJS — console, file, daily rotation, and stack drivers.
4
+
5
+ Part of [MantiqJS](https://github.com/abdullahkhan/mantiq) — a batteries-included TypeScript web framework for Bun.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ bun add @mantiq/logging
11
+ ```
12
+
13
+ ## Documentation
14
+
15
+ See the [MantiqJS repository](https://github.com/abdullahkhan/mantiq) for full documentation.
16
+
17
+ ## License
18
+
19
+ MIT
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@mantiq/logging",
3
+ "version": "0.0.1",
4
+ "description": "Channel-based logging",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Abdullah Khan",
8
+ "homepage": "https://github.com/abdullahkhan/mantiq/tree/main/packages/logging",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/abdullahkhan/mantiq.git",
12
+ "directory": "packages/logging"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/abdullahkhan/mantiq/issues"
16
+ },
17
+ "keywords": [
18
+ "mantiq",
19
+ "mantiqjs",
20
+ "bun",
21
+ "typescript",
22
+ "framework",
23
+ "logging"
24
+ ],
25
+ "engines": {
26
+ "bun": ">=1.1.0"
27
+ },
28
+ "main": "./src/index.ts",
29
+ "types": "./src/index.ts",
30
+ "exports": {
31
+ ".": {
32
+ "bun": "./src/index.ts",
33
+ "default": "./src/index.ts"
34
+ }
35
+ },
36
+ "files": [
37
+ "src/",
38
+ "package.json",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "scripts": {
43
+ "build": "bun build ./src/index.ts --outdir ./dist --target bun",
44
+ "test": "bun test",
45
+ "typecheck": "tsc --noEmit",
46
+ "clean": "rm -rf dist"
47
+ },
48
+ "dependencies": {
49
+ "@mantiq/core": "workspace:*"
50
+ },
51
+ "devDependencies": {
52
+ "bun-types": "latest",
53
+ "typescript": "^5.7.0"
54
+ }
55
+ }
@@ -0,0 +1,133 @@
1
+ import type { DriverManager } from '@mantiq/core'
2
+ import type { LogLevel, LoggerDriver, LogConfig, ChannelConfig, LogFormatter } from './contracts/Logger.ts'
3
+ import { ConsoleDriver } from './drivers/ConsoleDriver.ts'
4
+ import { FileDriver } from './drivers/FileDriver.ts'
5
+ import { DailyDriver } from './drivers/DailyDriver.ts'
6
+ import { StackDriver } from './drivers/StackDriver.ts'
7
+ import { NullDriver } from './drivers/NullDriver.ts'
8
+ import { LineFormatter } from './formatters/LineFormatter.ts'
9
+ import { JsonFormatter } from './formatters/JsonFormatter.ts'
10
+
11
+ export class LogManager implements DriverManager<LoggerDriver>, LoggerDriver {
12
+ private readonly config: LogConfig
13
+ private readonly channels = new Map<string, LoggerDriver>()
14
+ private readonly customCreators = new Map<string, (config: ChannelConfig) => LoggerDriver>()
15
+
16
+ constructor(config?: Partial<LogConfig>) {
17
+ this.config = {
18
+ default: config?.default ?? 'console',
19
+ channels: config?.channels ?? {},
20
+ }
21
+ }
22
+
23
+ // ── DriverManager ─────────────────────────────────────────────────────────
24
+
25
+ driver(name?: string): LoggerDriver {
26
+ const channelName = name ?? this.getDefaultDriver()
27
+
28
+ if (!this.channels.has(channelName)) {
29
+ this.channels.set(channelName, this.createDriver(channelName))
30
+ }
31
+
32
+ return this.channels.get(channelName)!
33
+ }
34
+
35
+ channel(name?: string): LoggerDriver {
36
+ return this.driver(name)
37
+ }
38
+
39
+ extend(name: string, factory: (config: ChannelConfig) => LoggerDriver): void {
40
+ this.customCreators.set(name, factory)
41
+ }
42
+
43
+ getDefaultDriver(): string {
44
+ return this.config.default
45
+ }
46
+
47
+ forgetChannel(name: string): void {
48
+ this.channels.delete(name)
49
+ }
50
+
51
+ forgetChannels(): void {
52
+ this.channels.clear()
53
+ }
54
+
55
+ // ── LoggerDriver (delegates to default channel) ───────────────────────────
56
+
57
+ log(level: LogLevel, message: string, context?: Record<string, any>): void {
58
+ this.driver().log(level, message, context)
59
+ }
60
+
61
+ emergency(message: string, context?: Record<string, any>): void { this.driver().emergency(message, context) }
62
+ alert(message: string, context?: Record<string, any>): void { this.driver().alert(message, context) }
63
+ critical(message: string, context?: Record<string, any>): void { this.driver().critical(message, context) }
64
+ error(message: string, context?: Record<string, any>): void { this.driver().error(message, context) }
65
+ warning(message: string, context?: Record<string, any>): void { this.driver().warning(message, context) }
66
+ notice(message: string, context?: Record<string, any>): void { this.driver().notice(message, context) }
67
+ info(message: string, context?: Record<string, any>): void { this.driver().info(message, context) }
68
+ debug(message: string, context?: Record<string, any>): void { this.driver().debug(message, context) }
69
+
70
+ // ── Internal ──────────────────────────────────────────────────────────────
71
+
72
+ private createDriver(name: string): LoggerDriver {
73
+ const channelConfig = this.config.channels[name]
74
+ const driverName = channelConfig?.driver ?? name
75
+
76
+ const custom = this.customCreators.get(driverName)
77
+ if (custom) return custom(channelConfig ?? { driver: driverName })
78
+
79
+ if (!channelConfig && driverName === 'console') {
80
+ return new ConsoleDriver('console', 'debug')
81
+ }
82
+
83
+ if (!channelConfig) {
84
+ throw new Error(`Logging channel "${name}" is not configured. Define it in config/logging.ts or use extend().`)
85
+ }
86
+
87
+ const formatter = this.resolveFormatter(channelConfig)
88
+
89
+ switch (driverName) {
90
+ case 'console':
91
+ return new ConsoleDriver(
92
+ name,
93
+ channelConfig.level ?? 'debug',
94
+ formatter,
95
+ )
96
+
97
+ case 'file':
98
+ return new FileDriver(
99
+ name,
100
+ channelConfig.path as string ?? 'storage/logs/mantiq.log',
101
+ channelConfig.level ?? 'debug',
102
+ formatter,
103
+ )
104
+
105
+ case 'daily':
106
+ return new DailyDriver(
107
+ name,
108
+ channelConfig.path as string ?? 'storage/logs/mantiq.log',
109
+ channelConfig.level ?? 'debug',
110
+ (channelConfig.days as number) ?? 14,
111
+ formatter,
112
+ )
113
+
114
+ case 'stack': {
115
+ const channelNames = (channelConfig.channels as string[]) ?? []
116
+ const drivers = channelNames.map((ch) => this.driver(ch))
117
+ return new StackDriver(drivers)
118
+ }
119
+
120
+ case 'null':
121
+ return new NullDriver()
122
+
123
+ default:
124
+ throw new Error(`Unsupported logging driver: "${driverName}". Use extend() to register custom drivers.`)
125
+ }
126
+ }
127
+
128
+ private resolveFormatter(config: ChannelConfig): LogFormatter | undefined {
129
+ if (config.formatter === 'json') return new JsonFormatter()
130
+ if (config.formatter === 'line') return new LineFormatter()
131
+ return undefined
132
+ }
133
+ }
@@ -0,0 +1,54 @@
1
+ import { ServiceProvider, ConfigRepository } from '@mantiq/core'
2
+ import type { LogConfig } from './contracts/Logger.ts'
3
+ import { LogManager } from './LogManager.ts'
4
+ import { LOGGING_MANAGER } from './helpers/log.ts'
5
+
6
+ const DEFAULT_CONFIG: LogConfig = {
7
+ default: 'stack',
8
+ channels: {
9
+ stack: {
10
+ driver: 'stack',
11
+ channels: ['console', 'daily'],
12
+ },
13
+ console: {
14
+ driver: 'console',
15
+ level: 'debug',
16
+ },
17
+ daily: {
18
+ driver: 'daily',
19
+ path: 'storage/logs/mantiq.log',
20
+ level: 'debug',
21
+ days: 14,
22
+ },
23
+ file: {
24
+ driver: 'file',
25
+ path: 'storage/logs/mantiq.log',
26
+ level: 'debug',
27
+ },
28
+ null: {
29
+ driver: 'null',
30
+ },
31
+ },
32
+ }
33
+
34
+ export class LoggingServiceProvider extends ServiceProvider {
35
+ override register(): void {
36
+ this.app.singleton(LogManager, (c) => {
37
+ const config = c.make(ConfigRepository).get<LogConfig>('logging', DEFAULT_CONFIG)
38
+
39
+ // Resolve relative log paths to absolute from app base path
40
+ const basePath = this.app.basePath_()
41
+ if (config.channels) {
42
+ for (const ch of Object.values(config.channels)) {
43
+ if (typeof ch.path === 'string' && !ch.path.startsWith('/')) {
44
+ ch.path = `${basePath}/${ch.path}`
45
+ }
46
+ }
47
+ }
48
+
49
+ return new LogManager(config)
50
+ })
51
+
52
+ this.app.alias(LogManager, LOGGING_MANAGER)
53
+ }
54
+ }
@@ -0,0 +1,49 @@
1
+ export type LogLevel = 'emergency' | 'alert' | 'critical' | 'error' | 'warning' | 'notice' | 'info' | 'debug'
2
+
3
+ export const LOG_LEVELS: Record<LogLevel, number> = {
4
+ emergency: 0,
5
+ alert: 1,
6
+ critical: 2,
7
+ error: 3,
8
+ warning: 4,
9
+ notice: 5,
10
+ info: 6,
11
+ debug: 7,
12
+ }
13
+
14
+ export interface LogEntry {
15
+ level: LogLevel
16
+ message: string
17
+ context: Record<string, any>
18
+ timestamp: Date
19
+ channel: string
20
+ }
21
+
22
+ export interface LogFormatter {
23
+ format(entry: LogEntry): string
24
+ }
25
+
26
+ export interface LoggerDriver {
27
+ log(level: LogLevel, message: string, context?: Record<string, any>): void
28
+
29
+ emergency(message: string, context?: Record<string, any>): void
30
+ alert(message: string, context?: Record<string, any>): void
31
+ critical(message: string, context?: Record<string, any>): void
32
+ error(message: string, context?: Record<string, any>): void
33
+ warning(message: string, context?: Record<string, any>): void
34
+ notice(message: string, context?: Record<string, any>): void
35
+ info(message: string, context?: Record<string, any>): void
36
+ debug(message: string, context?: Record<string, any>): void
37
+ }
38
+
39
+ export interface ChannelConfig {
40
+ driver: string
41
+ level?: LogLevel
42
+ formatter?: 'line' | 'json'
43
+ [key: string]: unknown
44
+ }
45
+
46
+ export interface LogConfig {
47
+ default: string
48
+ channels: Record<string, ChannelConfig>
49
+ }
@@ -0,0 +1,45 @@
1
+ import type { LogLevel, LogEntry, LogFormatter, LoggerDriver } from '../contracts/Logger.ts'
2
+ import { LOG_LEVELS } from '../contracts/Logger.ts'
3
+ import { LineFormatter } from '../formatters/LineFormatter.ts'
4
+
5
+ export class ConsoleDriver implements LoggerDriver {
6
+ private readonly minLevel: number
7
+ private readonly formatter: LogFormatter
8
+ private readonly channelName: string
9
+
10
+ constructor(channel: string = 'console', minLevel: LogLevel = 'debug', formatter?: LogFormatter) {
11
+ this.channelName = channel
12
+ this.minLevel = LOG_LEVELS[minLevel]
13
+ this.formatter = formatter ?? new LineFormatter()
14
+ }
15
+
16
+ log(level: LogLevel, message: string, context: Record<string, any> = {}): void {
17
+ if (LOG_LEVELS[level] > this.minLevel) return
18
+
19
+ const entry: LogEntry = {
20
+ level,
21
+ message,
22
+ context,
23
+ timestamp: new Date(),
24
+ channel: this.channelName,
25
+ }
26
+
27
+ const formatted = this.formatter.format(entry)
28
+
29
+ // Use stderr for error-level and above, stdout for the rest
30
+ if (LOG_LEVELS[level] <= LOG_LEVELS.error) {
31
+ process.stderr.write(formatted + '\n')
32
+ } else {
33
+ process.stdout.write(formatted + '\n')
34
+ }
35
+ }
36
+
37
+ emergency(message: string, context?: Record<string, any>): void { this.log('emergency', message, context) }
38
+ alert(message: string, context?: Record<string, any>): void { this.log('alert', message, context) }
39
+ critical(message: string, context?: Record<string, any>): void { this.log('critical', message, context) }
40
+ error(message: string, context?: Record<string, any>): void { this.log('error', message, context) }
41
+ warning(message: string, context?: Record<string, any>): void { this.log('warning', message, context) }
42
+ notice(message: string, context?: Record<string, any>): void { this.log('notice', message, context) }
43
+ info(message: string, context?: Record<string, any>): void { this.log('info', message, context) }
44
+ debug(message: string, context?: Record<string, any>): void { this.log('debug', message, context) }
45
+ }
@@ -0,0 +1,109 @@
1
+ import { appendFile, mkdir, readdir, rm } from 'node:fs/promises'
2
+ import { dirname, join, basename } from 'node:path'
3
+ import type { LogLevel, LogEntry, LogFormatter, LoggerDriver } from '../contracts/Logger.ts'
4
+ import { LOG_LEVELS } from '../contracts/Logger.ts'
5
+ import { LineFormatter } from '../formatters/LineFormatter.ts'
6
+
7
+ export class DailyDriver implements LoggerDriver {
8
+ private readonly basePath: string
9
+ private readonly days: number
10
+ private readonly minLevel: number
11
+ private readonly formatter: LogFormatter
12
+ private readonly channelName: string
13
+ private _lastDate: string = ''
14
+ private _currentPath: string = ''
15
+ private _dirEnsured = false
16
+ private _pruned = false
17
+
18
+ constructor(
19
+ channel: string,
20
+ basePath: string,
21
+ minLevel: LogLevel = 'debug',
22
+ days: number = 14,
23
+ formatter?: LogFormatter,
24
+ ) {
25
+ this.channelName = channel
26
+ this.basePath = basePath
27
+ this.days = days
28
+ this.minLevel = LOG_LEVELS[minLevel]
29
+ this.formatter = formatter ?? new LineFormatter()
30
+ }
31
+
32
+ log(level: LogLevel, message: string, context: Record<string, any> = {}): void {
33
+ if (LOG_LEVELS[level] > this.minLevel) return
34
+
35
+ const entry: LogEntry = {
36
+ level,
37
+ message,
38
+ context,
39
+ timestamp: new Date(),
40
+ channel: this.channelName,
41
+ }
42
+
43
+ const line = this.formatter.format(entry) + '\n'
44
+ void this.writeLine(line, entry.timestamp)
45
+ }
46
+
47
+ private async writeLine(line: string, now: Date): Promise<void> {
48
+ const dateStr = now.toISOString().slice(0, 10) // YYYY-MM-DD
49
+ if (dateStr !== this._lastDate) {
50
+ this._lastDate = dateStr
51
+ this._currentPath = this.pathForDate(dateStr)
52
+ this._dirEnsured = false
53
+ }
54
+
55
+ if (!this._dirEnsured) {
56
+ await mkdir(dirname(this._currentPath), { recursive: true })
57
+ this._dirEnsured = true
58
+ }
59
+
60
+ await appendFile(this._currentPath, line)
61
+
62
+ if (!this._pruned) {
63
+ this._pruned = true
64
+ void this.pruneOldFiles()
65
+ }
66
+ }
67
+
68
+ private pathForDate(dateStr: string): string {
69
+ // e.g., /storage/logs/mantiq-2024-03-19.log
70
+ const dir = dirname(this.basePath)
71
+ const name = basename(this.basePath, '.log')
72
+ return join(dir, `${name}-${dateStr}.log`)
73
+ }
74
+
75
+ private async pruneOldFiles(): Promise<void> {
76
+ const dir = dirname(this.basePath)
77
+ const prefix = basename(this.basePath, '.log')
78
+ const cutoff = Date.now() - this.days * 86400000
79
+
80
+ try {
81
+ const entries = await readdir(dir)
82
+ for (const entry of entries) {
83
+ // Match files like mantiq-2024-03-19.log
84
+ const match = entry.match(new RegExp(`^${escapeRegExp(prefix)}-(\\d{4}-\\d{2}-\\d{2})\\.log$`))
85
+ if (match) {
86
+ const fileDate = new Date(match[1]! + 'T00:00:00Z')
87
+ if (fileDate.getTime() < cutoff) {
88
+ await rm(join(dir, entry)).catch(() => {})
89
+ }
90
+ }
91
+ }
92
+ } catch {
93
+ // Pruning is best-effort
94
+ }
95
+ }
96
+
97
+ emergency(message: string, context?: Record<string, any>): void { this.log('emergency', message, context) }
98
+ alert(message: string, context?: Record<string, any>): void { this.log('alert', message, context) }
99
+ critical(message: string, context?: Record<string, any>): void { this.log('critical', message, context) }
100
+ error(message: string, context?: Record<string, any>): void { this.log('error', message, context) }
101
+ warning(message: string, context?: Record<string, any>): void { this.log('warning', message, context) }
102
+ notice(message: string, context?: Record<string, any>): void { this.log('notice', message, context) }
103
+ info(message: string, context?: Record<string, any>): void { this.log('info', message, context) }
104
+ debug(message: string, context?: Record<string, any>): void { this.log('debug', message, context) }
105
+ }
106
+
107
+ function escapeRegExp(s: string): string {
108
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
109
+ }
@@ -0,0 +1,54 @@
1
+ import { appendFile, mkdir } from 'node:fs/promises'
2
+ import { dirname } from 'node:path'
3
+ import type { LogLevel, LogEntry, LogFormatter, LoggerDriver } from '../contracts/Logger.ts'
4
+ import { LOG_LEVELS } from '../contracts/Logger.ts'
5
+ import { LineFormatter } from '../formatters/LineFormatter.ts'
6
+
7
+ export class FileDriver implements LoggerDriver {
8
+ private readonly path: string
9
+ private readonly minLevel: number
10
+ private readonly formatter: LogFormatter
11
+ private readonly channelName: string
12
+ private _dirEnsured = false
13
+
14
+ constructor(channel: string, path: string, minLevel: LogLevel = 'debug', formatter?: LogFormatter) {
15
+ this.channelName = channel
16
+ this.path = path
17
+ this.minLevel = LOG_LEVELS[minLevel]
18
+ this.formatter = formatter ?? new LineFormatter()
19
+ }
20
+
21
+ log(level: LogLevel, message: string, context: Record<string, any> = {}): void {
22
+ if (LOG_LEVELS[level] > this.minLevel) return
23
+
24
+ const entry: LogEntry = {
25
+ level,
26
+ message,
27
+ context,
28
+ timestamp: new Date(),
29
+ channel: this.channelName,
30
+ }
31
+
32
+ const line = this.formatter.format(entry) + '\n'
33
+
34
+ // Fire-and-forget — logging should never block the request
35
+ void this.writeLine(line)
36
+ }
37
+
38
+ private async writeLine(line: string): Promise<void> {
39
+ if (!this._dirEnsured) {
40
+ await mkdir(dirname(this.path), { recursive: true })
41
+ this._dirEnsured = true
42
+ }
43
+ await appendFile(this.path, line)
44
+ }
45
+
46
+ emergency(message: string, context?: Record<string, any>): void { this.log('emergency', message, context) }
47
+ alert(message: string, context?: Record<string, any>): void { this.log('alert', message, context) }
48
+ critical(message: string, context?: Record<string, any>): void { this.log('critical', message, context) }
49
+ error(message: string, context?: Record<string, any>): void { this.log('error', message, context) }
50
+ warning(message: string, context?: Record<string, any>): void { this.log('warning', message, context) }
51
+ notice(message: string, context?: Record<string, any>): void { this.log('notice', message, context) }
52
+ info(message: string, context?: Record<string, any>): void { this.log('info', message, context) }
53
+ debug(message: string, context?: Record<string, any>): void { this.log('debug', message, context) }
54
+ }
@@ -0,0 +1,13 @@
1
+ import type { LogLevel, LoggerDriver } from '../contracts/Logger.ts'
2
+
3
+ export class NullDriver implements LoggerDriver {
4
+ log(_level: LogLevel, _message: string, _context?: Record<string, any>): void {}
5
+ emergency(_message: string, _context?: Record<string, any>): void {}
6
+ alert(_message: string, _context?: Record<string, any>): void {}
7
+ critical(_message: string, _context?: Record<string, any>): void {}
8
+ error(_message: string, _context?: Record<string, any>): void {}
9
+ warning(_message: string, _context?: Record<string, any>): void {}
10
+ notice(_message: string, _context?: Record<string, any>): void {}
11
+ info(_message: string, _context?: Record<string, any>): void {}
12
+ debug(_message: string, _context?: Record<string, any>): void {}
13
+ }
@@ -0,0 +1,24 @@
1
+ import type { LogLevel, LoggerDriver } from '../contracts/Logger.ts'
2
+
3
+ export class StackDriver implements LoggerDriver {
4
+ private readonly drivers: LoggerDriver[]
5
+
6
+ constructor(drivers: LoggerDriver[]) {
7
+ this.drivers = drivers
8
+ }
9
+
10
+ log(level: LogLevel, message: string, context?: Record<string, any>): void {
11
+ for (const driver of this.drivers) {
12
+ driver.log(level, message, context)
13
+ }
14
+ }
15
+
16
+ emergency(message: string, context?: Record<string, any>): void { this.log('emergency', message, context) }
17
+ alert(message: string, context?: Record<string, any>): void { this.log('alert', message, context) }
18
+ critical(message: string, context?: Record<string, any>): void { this.log('critical', message, context) }
19
+ error(message: string, context?: Record<string, any>): void { this.log('error', message, context) }
20
+ warning(message: string, context?: Record<string, any>): void { this.log('warning', message, context) }
21
+ notice(message: string, context?: Record<string, any>): void { this.log('notice', message, context) }
22
+ info(message: string, context?: Record<string, any>): void { this.log('info', message, context) }
23
+ debug(message: string, context?: Record<string, any>): void { this.log('debug', message, context) }
24
+ }
@@ -0,0 +1,13 @@
1
+ import type { LogEntry, LogFormatter } from '../contracts/Logger.ts'
2
+
3
+ export class JsonFormatter implements LogFormatter {
4
+ format(entry: LogEntry): string {
5
+ return JSON.stringify({
6
+ timestamp: entry.timestamp.toISOString(),
7
+ channel: entry.channel,
8
+ level: entry.level,
9
+ message: entry.message,
10
+ ...(Object.keys(entry.context).length > 0 ? { context: entry.context } : {}),
11
+ })
12
+ }
13
+ }
@@ -0,0 +1,12 @@
1
+ import type { LogEntry, LogFormatter } from '../contracts/Logger.ts'
2
+
3
+ export class LineFormatter implements LogFormatter {
4
+ format(entry: LogEntry): string {
5
+ const ts = entry.timestamp.toISOString()
6
+ const level = entry.level.toUpperCase().padEnd(9)
7
+ const ctx = Object.keys(entry.context).length > 0
8
+ ? ' ' + JSON.stringify(entry.context)
9
+ : ''
10
+ return `[${ts}] ${entry.channel}.${level} ${entry.message}${ctx}`
11
+ }
12
+ }
@@ -0,0 +1,13 @@
1
+ import { Application } from '@mantiq/core'
2
+ import type { LoggerDriver } from '../contracts/Logger.ts'
3
+ import type { LogManager } from '../LogManager.ts'
4
+
5
+ export const LOGGING_MANAGER = Symbol('LogManager')
6
+
7
+ export function log(): LogManager
8
+ export function log(channel: string): LoggerDriver
9
+ export function log(channel?: string): LogManager | LoggerDriver {
10
+ const manager = Application.getInstance().make<LogManager>(LOGGING_MANAGER)
11
+ if (channel === undefined) return manager
12
+ return manager.channel(channel)
13
+ }
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ // ── Contracts ─────────────────────────────────────────────────────────────────
2
+ export type {
3
+ LogLevel,
4
+ LogEntry,
5
+ LogFormatter,
6
+ LoggerDriver,
7
+ ChannelConfig,
8
+ LogConfig,
9
+ } from './contracts/Logger.ts'
10
+ export { LOG_LEVELS } from './contracts/Logger.ts'
11
+
12
+ // ── Core ──────────────────────────────────────────────────────────────────────
13
+ export { LogManager } from './LogManager.ts'
14
+ export { LoggingServiceProvider } from './LoggingServiceProvider.ts'
15
+
16
+ // ── Formatters ────────────────────────────────────────────────────────────────
17
+ export { LineFormatter } from './formatters/LineFormatter.ts'
18
+ export { JsonFormatter } from './formatters/JsonFormatter.ts'
19
+
20
+ // ── Drivers ───────────────────────────────────────────────────────────────────
21
+ export { ConsoleDriver } from './drivers/ConsoleDriver.ts'
22
+ export { FileDriver } from './drivers/FileDriver.ts'
23
+ export { DailyDriver } from './drivers/DailyDriver.ts'
24
+ export { StackDriver } from './drivers/StackDriver.ts'
25
+ export { NullDriver } from './drivers/NullDriver.ts'
26
+
27
+ // ── Testing ───────────────────────────────────────────────────────────────────
28
+ export { LogFake } from './testing/LogFake.ts'
29
+ export type { LoggedMessage } from './testing/LogFake.ts'
30
+
31
+ // ── Helpers ───────────────────────────────────────────────────────────────────
32
+ export { log, LOGGING_MANAGER } from './helpers/log.ts'
@@ -0,0 +1,96 @@
1
+ import type { LogLevel, LoggerDriver } from '../contracts/Logger.ts'
2
+ import { LOG_LEVELS } from '../contracts/Logger.ts'
3
+
4
+ export interface LoggedMessage {
5
+ level: LogLevel
6
+ message: string
7
+ context: Record<string, any>
8
+ }
9
+
10
+ export class LogFake implements LoggerDriver {
11
+ private readonly logged: LoggedMessage[] = []
12
+
13
+ log(level: LogLevel, message: string, context: Record<string, any> = {}): void {
14
+ this.logged.push({ level, message, context })
15
+ }
16
+
17
+ emergency(message: string, context?: Record<string, any>): void { this.log('emergency', message, context ?? {}) }
18
+ alert(message: string, context?: Record<string, any>): void { this.log('alert', message, context ?? {}) }
19
+ critical(message: string, context?: Record<string, any>): void { this.log('critical', message, context ?? {}) }
20
+ error(message: string, context?: Record<string, any>): void { this.log('error', message, context ?? {}) }
21
+ warning(message: string, context?: Record<string, any>): void { this.log('warning', message, context ?? {}) }
22
+ notice(message: string, context?: Record<string, any>): void { this.log('notice', message, context ?? {}) }
23
+ info(message: string, context?: Record<string, any>): void { this.log('info', message, context ?? {}) }
24
+ debug(message: string, context?: Record<string, any>): void { this.log('debug', message, context ?? {}) }
25
+
26
+ // ── Assertions ────────────────────────────────────────────────────────────
27
+
28
+ assertLogged(level: LogLevel, message?: string | RegExp, count?: number): void {
29
+ const matches = this.matching(level, message)
30
+ if (matches.length === 0) {
31
+ throw new Error(
32
+ `Expected log at level [${level}]${message ? ` matching "${message}"` : ''} but none was found.\n` +
33
+ `Logged messages: ${JSON.stringify(this.logged, null, 2)}`,
34
+ )
35
+ }
36
+ if (count !== undefined && matches.length !== count) {
37
+ throw new Error(
38
+ `Expected ${count} log(s) at level [${level}]${message ? ` matching "${message}"` : ''} but found ${matches.length}.`,
39
+ )
40
+ }
41
+ }
42
+
43
+ assertNotLogged(level: LogLevel, message?: string | RegExp): void {
44
+ const matches = this.matching(level, message)
45
+ if (matches.length > 0) {
46
+ throw new Error(
47
+ `Unexpected log at level [${level}]${message ? ` matching "${message}"` : ''} was found.\n` +
48
+ `Match: ${JSON.stringify(matches[0])}`,
49
+ )
50
+ }
51
+ }
52
+
53
+ assertNothingLogged(): void {
54
+ if (this.logged.length > 0) {
55
+ throw new Error(
56
+ `Expected no logs but ${this.logged.length} were recorded.\n` +
57
+ `First: ${JSON.stringify(this.logged[0])}`,
58
+ )
59
+ }
60
+ }
61
+
62
+ assertLoggedCount(count: number): void {
63
+ if (this.logged.length !== count) {
64
+ throw new Error(`Expected ${count} log(s) but ${this.logged.length} were recorded.`)
65
+ }
66
+ }
67
+
68
+ // ── Inspection ────────────────────────────────────────────────────────────
69
+
70
+ all(): LoggedMessage[] {
71
+ return [...this.logged]
72
+ }
73
+
74
+ forLevel(level: LogLevel): LoggedMessage[] {
75
+ return this.logged.filter((m) => m.level === level)
76
+ }
77
+
78
+ hasLogged(level: LogLevel, message?: string | RegExp): boolean {
79
+ return this.matching(level, message).length > 0
80
+ }
81
+
82
+ reset(): void {
83
+ this.logged.length = 0
84
+ }
85
+
86
+ // ── Internal ──────────────────────────────────────────────────────────────
87
+
88
+ private matching(level: LogLevel, message?: string | RegExp): LoggedMessage[] {
89
+ return this.logged.filter((m) => {
90
+ if (m.level !== level) return false
91
+ if (message === undefined) return true
92
+ if (typeof message === 'string') return m.message === message
93
+ return message.test(m.message)
94
+ })
95
+ }
96
+ }