millas 0.2.6 → 0.2.8
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 +1 -1
- package/src/admin/resources/AdminResource.js +61 -18
- package/src/admin/views/layouts/base.njk +1 -1
- package/src/index.js +23 -10
- package/src/logger/Logger.js +341 -0
- package/src/logger/channels/ConsoleChannel.js +39 -0
- package/src/logger/channels/FileChannel.js +101 -0
- package/src/logger/channels/index.js +48 -0
- package/src/logger/formatters/JsonFormatter.js +52 -0
- package/src/logger/formatters/PrettyFormatter.js +95 -0
- package/src/logger/formatters/SimpleFormatter.js +37 -0
- package/src/logger/index.js +69 -0
- package/src/logger/levels.js +52 -0
- package/src/middleware/LogMiddleware.js +54 -28
- package/src/providers/LogServiceProvider.js +138 -0
- package/src/scaffold/templates.js +37 -26
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* NullChannel
|
|
5
|
+
*
|
|
6
|
+
* Silently discards all log entries.
|
|
7
|
+
* Useful in tests where you want to suppress all output.
|
|
8
|
+
*
|
|
9
|
+
* Log.configure({ channels: [new NullChannel()] });
|
|
10
|
+
*/
|
|
11
|
+
class NullChannel {
|
|
12
|
+
write() { /* intentionally empty */ }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* StackChannel
|
|
17
|
+
*
|
|
18
|
+
* Fans a single log entry out to multiple channels simultaneously.
|
|
19
|
+
* This is the standard "stack" pattern — one channel for console,
|
|
20
|
+
* another for file, optionally one for an external service.
|
|
21
|
+
*
|
|
22
|
+
* new StackChannel([
|
|
23
|
+
* new ConsoleChannel({ formatter: new PrettyFormatter() }),
|
|
24
|
+
* new FileChannel({ formatter: new SimpleFormatter(), minLevel: LEVELS.INFO }),
|
|
25
|
+
* ])
|
|
26
|
+
*/
|
|
27
|
+
class StackChannel {
|
|
28
|
+
/**
|
|
29
|
+
* @param {Array} channels — array of channel instances
|
|
30
|
+
*/
|
|
31
|
+
constructor(channels = []) {
|
|
32
|
+
this._channels = channels;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Add a channel at runtime. */
|
|
36
|
+
add(channel) {
|
|
37
|
+
this._channels.push(channel);
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
write(entry) {
|
|
42
|
+
for (const ch of this._channels) {
|
|
43
|
+
try { ch.write(entry); } catch {}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { NullChannel, StackChannel };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { LEVEL_NAMES } = require('../levels');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* JsonFormatter
|
|
7
|
+
*
|
|
8
|
+
* Emits one JSON object per log entry — ideal for production environments
|
|
9
|
+
* where logs are shipped to Datadog, Elasticsearch, CloudWatch, etc.
|
|
10
|
+
*
|
|
11
|
+
* Output (one line per entry):
|
|
12
|
+
* {"ts":"2026-03-15T12:00:00.000Z","level":"INFO","tag":"Auth","msg":"Login","ctx":{...}}
|
|
13
|
+
*/
|
|
14
|
+
class JsonFormatter {
|
|
15
|
+
/**
|
|
16
|
+
* @param {object} options
|
|
17
|
+
* @param {boolean} [options.pretty=false] — pretty-print JSON (for debugging)
|
|
18
|
+
* @param {object} [options.extra] — static fields merged into every entry (e.g. service name)
|
|
19
|
+
*/
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
this.pretty = options.pretty || false;
|
|
22
|
+
this.extra = options.extra || {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
format(entry) {
|
|
26
|
+
const { level, tag, message, context, error, timestamp } = entry;
|
|
27
|
+
|
|
28
|
+
const record = {
|
|
29
|
+
ts: timestamp || new Date().toISOString(),
|
|
30
|
+
level: LEVEL_NAMES[level] || String(level),
|
|
31
|
+
...this.extra,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (tag) record.tag = tag;
|
|
35
|
+
record.msg = message;
|
|
36
|
+
if (context !== undefined && context !== null) record.ctx = context;
|
|
37
|
+
|
|
38
|
+
if (error instanceof Error) {
|
|
39
|
+
record.error = {
|
|
40
|
+
message: error.message,
|
|
41
|
+
name: error.name,
|
|
42
|
+
stack: error.stack,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return this.pretty
|
|
47
|
+
? JSON.stringify(record, null, 2)
|
|
48
|
+
: JSON.stringify(record);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = JsonFormatter;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { LEVEL_NAMES, LEVEL_TAGS, LEVEL_COLOURS, RESET, BOLD, DIM } = require('../levels');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* PrettyFormatter
|
|
7
|
+
*
|
|
8
|
+
* Colourful, human-readable output. Designed for development.
|
|
9
|
+
* Inspired by Timber (Android) and Laravel's log formatting.
|
|
10
|
+
*
|
|
11
|
+
* Output:
|
|
12
|
+
* [2026-03-15 12:00:00] I UserController User #5 logged in
|
|
13
|
+
* [2026-03-15 12:00:01] E Database Connection refused { host: 'localhost' }
|
|
14
|
+
* [2026-03-15 12:00:02] W Auth Token expiring soon
|
|
15
|
+
*
|
|
16
|
+
* WTF level also prints the full stack trace.
|
|
17
|
+
*/
|
|
18
|
+
class PrettyFormatter {
|
|
19
|
+
/**
|
|
20
|
+
* @param {object} options
|
|
21
|
+
* @param {boolean} [options.timestamp=true] — show timestamp
|
|
22
|
+
* @param {boolean} [options.tag=true] — show tag/component name
|
|
23
|
+
* @param {boolean} [options.colour=true] — ANSI colour (disable for pipes/files)
|
|
24
|
+
* @param {string} [options.timestampFormat] — 'iso' | 'short' (default: 'short')
|
|
25
|
+
*/
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
this.showTimestamp = options.timestamp !== false;
|
|
28
|
+
this.showTag = options.tag !== false;
|
|
29
|
+
this.colour = options.colour !== false;
|
|
30
|
+
this.tsFormat = options.timestampFormat || 'short';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
format(entry) {
|
|
34
|
+
const { level, tag, message, context, error } = entry;
|
|
35
|
+
|
|
36
|
+
const c = this.colour ? LEVEL_COLOURS[level] : '';
|
|
37
|
+
const r = this.colour ? RESET : '';
|
|
38
|
+
const b = this.colour ? BOLD : '';
|
|
39
|
+
const d = this.colour ? '\x1b[2m' : '';
|
|
40
|
+
const lvl = LEVEL_TAGS[level] || '?';
|
|
41
|
+
|
|
42
|
+
const parts = [];
|
|
43
|
+
|
|
44
|
+
// Timestamp
|
|
45
|
+
if (this.showTimestamp) {
|
|
46
|
+
const ts = this._timestamp();
|
|
47
|
+
parts.push(`${d}[${ts}]${r}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Level tag (single letter, coloured)
|
|
51
|
+
parts.push(`${c}${b}${lvl}${r}`);
|
|
52
|
+
|
|
53
|
+
// Component/tag
|
|
54
|
+
if (this.showTag && tag) {
|
|
55
|
+
const tagStr = tag.padEnd(18);
|
|
56
|
+
parts.push(`${b}${tagStr}${r}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Message
|
|
60
|
+
parts.push(`${c}${message}${r}`);
|
|
61
|
+
|
|
62
|
+
// Context object
|
|
63
|
+
if (context !== undefined && context !== null) {
|
|
64
|
+
const ctx = typeof context === 'object'
|
|
65
|
+
? JSON.stringify(context, null, 0)
|
|
66
|
+
: String(context);
|
|
67
|
+
parts.push(`${d}${ctx}${r}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let output = parts.join(' ');
|
|
71
|
+
|
|
72
|
+
// Error stack
|
|
73
|
+
if (error instanceof Error) {
|
|
74
|
+
output += `\n${d}${error.stack || error.message}${r}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// WTF: print big warning banner
|
|
78
|
+
if (level === 5) {
|
|
79
|
+
const banner = this.colour
|
|
80
|
+
? `\x1b[35m\x1b[1m${'━'.repeat(60)}\x1b[0m`
|
|
81
|
+
: '━'.repeat(60);
|
|
82
|
+
output = `${banner}\n${output}\n${banner}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return output;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_timestamp() {
|
|
89
|
+
const now = new Date();
|
|
90
|
+
if (this.tsFormat === 'iso') return now.toISOString();
|
|
91
|
+
return now.toISOString().replace('T', ' ').slice(0, 19);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = PrettyFormatter;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { LEVEL_NAMES } = require('../levels');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SimpleFormatter
|
|
7
|
+
*
|
|
8
|
+
* Plain, no-colour text. Suitable for file output or any sink
|
|
9
|
+
* where ANSI codes would be noise.
|
|
10
|
+
*
|
|
11
|
+
* Output:
|
|
12
|
+
* [2026-03-15 12:00:00] [INFO] Auth: User logged in
|
|
13
|
+
* [2026-03-15 12:00:01] [ERROR] DB: Query failed {"table":"users"}
|
|
14
|
+
*/
|
|
15
|
+
class SimpleFormatter {
|
|
16
|
+
format(entry) {
|
|
17
|
+
const { level, tag, message, context, error, timestamp } = entry;
|
|
18
|
+
|
|
19
|
+
const ts = (timestamp || new Date().toISOString()).replace('T', ' ').slice(0, 23);
|
|
20
|
+
const lvlName = (LEVEL_NAMES[level] || String(level)).padEnd(7);
|
|
21
|
+
const tagPart = tag ? `${tag}: ` : '';
|
|
22
|
+
|
|
23
|
+
let line = `[${ts}] [${lvlName}] ${tagPart}${message}`;
|
|
24
|
+
|
|
25
|
+
if (context !== undefined && context !== null) {
|
|
26
|
+
line += ' ' + (typeof context === 'object' ? JSON.stringify(context) : String(context));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (error instanceof Error) {
|
|
30
|
+
line += '\n ' + (error.stack || error.message).replace(/\n/g, '\n ');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return line;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = SimpleFormatter;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Logger = require('./Logger');
|
|
4
|
+
const { LEVELS, LEVEL_NAMES } = require('./levels');
|
|
5
|
+
const PrettyFormatter = require('./formatters/PrettyFormatter');
|
|
6
|
+
const JsonFormatter = require('./formatters/JsonFormatter');
|
|
7
|
+
const SimpleFormatter = require('./formatters/SimpleFormatter');
|
|
8
|
+
const ConsoleChannel = require('./channels/ConsoleChannel');
|
|
9
|
+
const FileChannel = require('./channels/FileChannel');
|
|
10
|
+
const { NullChannel, StackChannel } = require('./channels/index');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Log
|
|
14
|
+
*
|
|
15
|
+
* The global logger singleton — the one you import everywhere.
|
|
16
|
+
*
|
|
17
|
+
* const { Log } = require('millas');
|
|
18
|
+
*
|
|
19
|
+
* Log.i('App booted');
|
|
20
|
+
* Log.tag('UserService').d('Fetching user', { id: 5 });
|
|
21
|
+
* Log.e('Payment', 'Stripe failed', error);
|
|
22
|
+
* Log.wtf('Impossible state reached');
|
|
23
|
+
*
|
|
24
|
+
* Configured automatically by LogServiceProvider when you add it
|
|
25
|
+
* to your providers list. For manual setup:
|
|
26
|
+
*
|
|
27
|
+
* Log.configure({
|
|
28
|
+
* minLevel: LEVELS.INFO,
|
|
29
|
+
* channel: new StackChannel([
|
|
30
|
+
* new ConsoleChannel({ formatter: new PrettyFormatter() }),
|
|
31
|
+
* new FileChannel({ formatter: new SimpleFormatter(), minLevel: LEVELS.WARN }),
|
|
32
|
+
* ]),
|
|
33
|
+
* });
|
|
34
|
+
*/
|
|
35
|
+
const Log = new Logger();
|
|
36
|
+
|
|
37
|
+
// Apply sensible defaults so Log works out-of-the-box before
|
|
38
|
+
// LogServiceProvider runs (during framework boot, tests, etc.)
|
|
39
|
+
Log.configure({
|
|
40
|
+
minLevel: process.env.NODE_ENV === 'production' ? LEVELS.INFO : LEVELS.DEBUG,
|
|
41
|
+
channel: new ConsoleChannel({
|
|
42
|
+
formatter: new PrettyFormatter({
|
|
43
|
+
colour: process.stdout.isTTY !== false,
|
|
44
|
+
}),
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
// The singleton you use everywhere
|
|
50
|
+
Log,
|
|
51
|
+
|
|
52
|
+
// The class (for constructing named loggers)
|
|
53
|
+
Logger,
|
|
54
|
+
|
|
55
|
+
// Level constants
|
|
56
|
+
LEVELS,
|
|
57
|
+
LEVEL_NAMES,
|
|
58
|
+
|
|
59
|
+
// Formatters
|
|
60
|
+
PrettyFormatter,
|
|
61
|
+
JsonFormatter,
|
|
62
|
+
SimpleFormatter,
|
|
63
|
+
|
|
64
|
+
// Channels
|
|
65
|
+
ConsoleChannel,
|
|
66
|
+
FileChannel,
|
|
67
|
+
NullChannel,
|
|
68
|
+
StackChannel,
|
|
69
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Log levels — ordered by severity (lowest to highest).
|
|
5
|
+
*
|
|
6
|
+
* Inspired by Android's Timber / Log class:
|
|
7
|
+
* VERBOSE — extremely detailed; usually filtered out in production
|
|
8
|
+
* DEBUG — development diagnostics
|
|
9
|
+
* INFO — normal operational messages (default minimum in production)
|
|
10
|
+
* WARN — something unexpected but recoverable
|
|
11
|
+
* ERROR — something failed; needs attention
|
|
12
|
+
* WTF — "What a Terrible Failure" — should never happen; always logged
|
|
13
|
+
*/
|
|
14
|
+
const LEVELS = {
|
|
15
|
+
VERBOSE: 0,
|
|
16
|
+
DEBUG: 1,
|
|
17
|
+
INFO: 2,
|
|
18
|
+
WARN: 3,
|
|
19
|
+
ERROR: 4,
|
|
20
|
+
WTF: 5,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Reverse map: number → name */
|
|
24
|
+
const LEVEL_NAMES = Object.fromEntries(
|
|
25
|
+
Object.entries(LEVELS).map(([k, v]) => [v, k])
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
/** Single-letter tags (like Android logcat) */
|
|
29
|
+
const LEVEL_TAGS = {
|
|
30
|
+
0: 'V',
|
|
31
|
+
1: 'D',
|
|
32
|
+
2: 'I',
|
|
33
|
+
3: 'W',
|
|
34
|
+
4: 'E',
|
|
35
|
+
5: 'F', // Fatal / WTF
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** ANSI colour codes for each level */
|
|
39
|
+
const LEVEL_COLOURS = {
|
|
40
|
+
0: '\x1b[90m', // VERBOSE — dark grey
|
|
41
|
+
1: '\x1b[36m', // DEBUG — cyan
|
|
42
|
+
2: '\x1b[32m', // INFO — green
|
|
43
|
+
3: '\x1b[33m', // WARN — yellow
|
|
44
|
+
4: '\x1b[31m', // ERROR — red
|
|
45
|
+
5: '\x1b[35m', // WTF — magenta
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const RESET = '\x1b[0m';
|
|
49
|
+
const BOLD = '\x1b[1m';
|
|
50
|
+
const DIM = '\x1b[2m';
|
|
51
|
+
|
|
52
|
+
module.exports = { LEVELS, LEVEL_NAMES, LEVEL_TAGS, LEVEL_COLOURS, RESET, BOLD, DIM };
|
|
@@ -5,53 +5,79 @@ const Middleware = require('./Middleware');
|
|
|
5
5
|
/**
|
|
6
6
|
* LogMiddleware
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* Django-style HTTP request logger.
|
|
9
|
+
* Uses the Millas Log singleton — output goes through your configured channels.
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* Log levels per status:
|
|
12
|
+
* 2xx, 3xx → INFO (green)
|
|
13
|
+
* 4xx → WARN (yellow)
|
|
14
|
+
* 5xx → ERROR (red)
|
|
15
|
+
* slow req → WARN (with slow=true in context)
|
|
12
16
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
17
|
+
* Output:
|
|
18
|
+
* I HTTP GET /api/users 200 4ms
|
|
19
|
+
* W HTTP POST /api/login 401 12ms
|
|
20
|
+
* E HTTP GET /api/crash 500 3ms
|
|
21
|
+
*
|
|
22
|
+
* Usage (register as middleware):
|
|
23
|
+
* app.use(new LogMiddleware().handle.bind(new LogMiddleware()));
|
|
24
|
+
*
|
|
25
|
+
* Or use Log.requestMiddleware() directly for more options.
|
|
15
26
|
*
|
|
16
27
|
* Options:
|
|
17
|
-
* silent
|
|
18
|
-
*
|
|
28
|
+
* silent — suppress all output (default: false)
|
|
29
|
+
* includeQuery — include query string in URL (default: false)
|
|
30
|
+
* includeIp — include client IP (default: true)
|
|
31
|
+
* slowThreshold — warn if response > Nms (default: 1000)
|
|
32
|
+
* skip — function(req, res) => bool — skip matching routes
|
|
19
33
|
*/
|
|
20
34
|
class LogMiddleware extends Middleware {
|
|
21
35
|
constructor(options = {}) {
|
|
22
36
|
super();
|
|
23
|
-
this.silent
|
|
24
|
-
this.
|
|
37
|
+
this.silent = options.silent ?? false;
|
|
38
|
+
this.includeQuery = options.includeQuery ?? false;
|
|
39
|
+
this.includeIp = options.includeIp ?? true;
|
|
40
|
+
this.slowThreshold = options.slowThreshold ?? 1000;
|
|
41
|
+
this.skip = options.skip ?? null;
|
|
25
42
|
}
|
|
26
43
|
|
|
27
44
|
async handle(req, res, next) {
|
|
28
45
|
if (this.silent) return next();
|
|
46
|
+
if (typeof this.skip === 'function' && this.skip(req, res)) return next();
|
|
47
|
+
|
|
48
|
+
// Lazy-require Log so this file loads even before LogServiceProvider runs
|
|
49
|
+
let Log;
|
|
50
|
+
try {
|
|
51
|
+
Log = require('../logger/index').Log;
|
|
52
|
+
} catch {
|
|
53
|
+
return next();
|
|
54
|
+
}
|
|
29
55
|
|
|
30
56
|
const start = Date.now();
|
|
31
|
-
const {
|
|
57
|
+
const { LEVELS } = require('../logger/levels');
|
|
32
58
|
|
|
33
59
|
res.on('finish', () => {
|
|
34
60
|
const ms = Date.now() - start;
|
|
35
61
|
const status = res.statusCode;
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
: status < 500 ? '\x1b[33m'
|
|
42
|
-
: '\x1b[31m';
|
|
43
|
-
const reset = '\x1b[0m';
|
|
44
|
-
const methodColor = '\x1b[1m';
|
|
45
|
-
console.log(
|
|
46
|
-
` ${'\x1b[90m'}[${ts}]${reset} ` +
|
|
47
|
-
`${methodColor}${method.padEnd(7)}${reset} ` +
|
|
48
|
-
`${routePath || url} ` +
|
|
49
|
-
`${statusColor}${status}${reset} ` +
|
|
50
|
-
`${'\x1b[90m'}${ms}ms${reset}`
|
|
51
|
-
);
|
|
52
|
-
} else {
|
|
53
|
-
console.log(`[${ts}] ${method} ${routePath || url} ${status} ${ms}ms`);
|
|
62
|
+
const method = req.method;
|
|
63
|
+
let url = req.path || req.url || '/';
|
|
64
|
+
|
|
65
|
+
if (this.includeQuery && req.url && req.url.includes('?')) {
|
|
66
|
+
url = req.url;
|
|
54
67
|
}
|
|
68
|
+
|
|
69
|
+
// Level based on status + response time
|
|
70
|
+
let level;
|
|
71
|
+
if (status >= 500) level = LEVELS.ERROR;
|
|
72
|
+
else if (status >= 400) level = LEVELS.WARN;
|
|
73
|
+
else if (ms > this.slowThreshold) level = LEVELS.WARN;
|
|
74
|
+
else level = LEVELS.INFO;
|
|
75
|
+
|
|
76
|
+
const ctx = { status, ms };
|
|
77
|
+
if (this.includeIp) ctx.ip = req.ip || req.connection?.remoteAddress;
|
|
78
|
+
if (ms > this.slowThreshold) ctx.slow = true;
|
|
79
|
+
|
|
80
|
+
Log._log(level, 'HTTP', `${method} ${url} ${status} ${ms}ms`, ctx);
|
|
55
81
|
});
|
|
56
82
|
|
|
57
83
|
next();
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ServiceProvider = require('./ServiceProvider');
|
|
4
|
+
const {
|
|
5
|
+
Log,
|
|
6
|
+
Logger,
|
|
7
|
+
LEVELS,
|
|
8
|
+
PrettyFormatter,
|
|
9
|
+
JsonFormatter,
|
|
10
|
+
SimpleFormatter,
|
|
11
|
+
ConsoleChannel,
|
|
12
|
+
FileChannel,
|
|
13
|
+
NullChannel,
|
|
14
|
+
StackChannel,
|
|
15
|
+
} = require('../logger/index');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* LogServiceProvider
|
|
19
|
+
*
|
|
20
|
+
* Reads config/logging.js and configures the Log singleton.
|
|
21
|
+
* Also registers Log and Logger in the DI container.
|
|
22
|
+
*
|
|
23
|
+
* Add to bootstrap/app.js:
|
|
24
|
+
* app.providers([LogServiceProvider, ...]);
|
|
25
|
+
*
|
|
26
|
+
* The provider is intentionally ordered first so every other provider
|
|
27
|
+
* can use Log during their boot() method.
|
|
28
|
+
*
|
|
29
|
+
* config/logging.js is optional — sensible defaults apply if absent.
|
|
30
|
+
*/
|
|
31
|
+
class LogServiceProvider extends ServiceProvider {
|
|
32
|
+
register(container) {
|
|
33
|
+
container.instance('Log', Log);
|
|
34
|
+
container.instance('Logger', Logger);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async boot(container, app) {
|
|
38
|
+
let config = {};
|
|
39
|
+
try {
|
|
40
|
+
config = require(process.cwd() + '/config/logging');
|
|
41
|
+
} catch {
|
|
42
|
+
// No config — use defaults already set in logger/index.js
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const channels = this._buildChannels(config);
|
|
47
|
+
|
|
48
|
+
Log.configure({
|
|
49
|
+
minLevel: this._resolveLevel(config.level ?? config.minLevel),
|
|
50
|
+
defaultTag: config.defaultTag || 'App',
|
|
51
|
+
channel: channels.length === 1
|
|
52
|
+
? channels[0]
|
|
53
|
+
: new StackChannel(channels),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
Log.tag('Millas').i(`Logger configured — level: ${this._levelName(Log._minLevel)}, channels: ${channels.length}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─── Private ──────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
_buildChannels(config) {
|
|
62
|
+
const channelDefs = config.channels || ['console'];
|
|
63
|
+
const built = [];
|
|
64
|
+
|
|
65
|
+
for (const def of channelDefs) {
|
|
66
|
+
// String shorthand: 'console' | 'file' | 'null'
|
|
67
|
+
if (def === 'console' || def?.driver === 'console') {
|
|
68
|
+
built.push(this._buildConsole(def));
|
|
69
|
+
} else if (def === 'file' || def?.driver === 'file') {
|
|
70
|
+
built.push(this._buildFile(def));
|
|
71
|
+
} else if (def === 'null' || def?.driver === 'null') {
|
|
72
|
+
built.push(new NullChannel());
|
|
73
|
+
} else if (def && typeof def.write === 'function') {
|
|
74
|
+
// Already an instantiated channel — use directly
|
|
75
|
+
built.push(def);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Always have at least a console channel
|
|
80
|
+
if (!built.length) built.push(this._buildConsole({}));
|
|
81
|
+
|
|
82
|
+
return built;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_buildConsole(opts = {}) {
|
|
86
|
+
const fmt = this._buildFormatter(opts.formatter || opts.format || 'pretty', opts);
|
|
87
|
+
return new ConsoleChannel({
|
|
88
|
+
formatter: fmt,
|
|
89
|
+
minLevel: this._resolveLevel(opts.level ?? opts.minLevel),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_buildFile(opts = {}) {
|
|
94
|
+
const fmt = this._buildFormatter(opts.formatter || opts.format || 'simple', opts);
|
|
95
|
+
return new FileChannel({
|
|
96
|
+
path: opts.path || 'storage/logs',
|
|
97
|
+
prefix: opts.prefix || 'millas',
|
|
98
|
+
formatter: fmt,
|
|
99
|
+
minLevel: this._resolveLevel(opts.level ?? opts.minLevel),
|
|
100
|
+
maxFiles: opts.maxFiles ?? 30,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_buildFormatter(name, opts = {}) {
|
|
105
|
+
if (name && typeof name === 'object' && typeof name.format === 'function') {
|
|
106
|
+
return name; // already an instance
|
|
107
|
+
}
|
|
108
|
+
switch (String(name).toLowerCase()) {
|
|
109
|
+
case 'json':
|
|
110
|
+
return new JsonFormatter({ extra: opts.extra });
|
|
111
|
+
case 'simple':
|
|
112
|
+
return new SimpleFormatter();
|
|
113
|
+
case 'pretty':
|
|
114
|
+
default:
|
|
115
|
+
return new PrettyFormatter({
|
|
116
|
+
colour: opts.colour !== false && process.stdout.isTTY !== false,
|
|
117
|
+
timestamp: opts.timestamp !== false,
|
|
118
|
+
tag: opts.tag !== false,
|
|
119
|
+
timestampFormat: opts.timestampFormat || 'short',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_resolveLevel(level) {
|
|
125
|
+
if (level === undefined || level === null) {
|
|
126
|
+
return process.env.NODE_ENV === 'production' ? LEVELS.INFO : LEVELS.DEBUG;
|
|
127
|
+
}
|
|
128
|
+
if (typeof level === 'number') return level;
|
|
129
|
+
return LEVELS[String(level).toUpperCase()] ?? LEVELS.DEBUG;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
_levelName(n) {
|
|
133
|
+
const names = ['VERBOSE','DEBUG','INFO','WARN','ERROR','WTF'];
|
|
134
|
+
return names[n] || String(n);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = LogServiceProvider;
|