millas 0.2.5 → 0.2.7

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.
@@ -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
- * Logs every incoming request and its response time.
8
+ * Django-style HTTP request logger.
9
+ * Uses the Millas Log singleton — output goes through your configured channels.
9
10
  *
10
- * Output format:
11
- * [2026-03-14 12:00:00] GET /api/users 200 12ms
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
- * Register:
14
- * middlewareRegistry.register('log', LogMiddleware);
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 — suppress output (default: false)
18
- * format'dev' (coloured) | 'combined' (plain)
28
+ * silent — suppress all output (default: false)
29
+ * includeQueryinclude 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 = options.silent || false;
24
- this.format = options.format || 'dev';
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 { method, path: routePath, url } = req;
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 ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
37
-
38
- if (this.format === 'dev') {
39
- const statusColor = status < 300 ? '\x1b[32m'
40
- : status < 400 ? '\x1b[36m'
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;
@@ -66,8 +66,6 @@ storage/logs/*.log
66
66
  storage/uploads/*
67
67
  !storage/uploads/.gitkeep
68
68
  database/database.sqlite
69
- # Millas migration snapshot — auto-generated, do not commit
70
- .millas/
71
69
  `,
72
70
 
73
71
  // ─── millas.config.js ─────────────────────────────────────────
@@ -106,6 +104,7 @@ function resolveMillas() {
106
104
  const {
107
105
  Application,
108
106
  Admin,
107
+ LogServiceProvider,
109
108
  CacheServiceProvider,
110
109
  StorageServiceProvider,
111
110
  MailServiceProvider,
@@ -124,6 +123,7 @@ const app = new Application(expressApp);
124
123
 
125
124
  // ── Register service providers ──────────────────────────────────
126
125
  app.providers([
126
+ LogServiceProvider, // first — so Log works in all other providers
127
127
  CacheServiceProvider,
128
128
  StorageServiceProvider,
129
129
  MailServiceProvider,
@@ -198,6 +198,38 @@ module.exports = function (Route) {
198
198
 
199
199
  });
200
200
  };
201
+ `,
202
+
203
+ // ─── config/logging.js ────────────────────────────────────────
204
+ 'config/logging.js': `'use strict';
205
+
206
+ /**
207
+ * Logging Configuration
208
+ *
209
+ * Levels (lowest → highest): verbose | debug | info | warn | error | wtf
210
+ * Channels: 'console' | 'file' | 'null'
211
+ * Formats: 'pretty' (coloured) | 'simple' (plain text) | 'json' (structured)
212
+ */
213
+ module.exports = {
214
+ level: process.env.LOG_LEVEL || (process.env.NODE_ENV === 'production' ? 'info' : 'debug'),
215
+
216
+ defaultTag: process.env.APP_NAME || 'App',
217
+
218
+ channels: [
219
+ {
220
+ driver: 'console',
221
+ format: 'pretty',
222
+ },
223
+ {
224
+ driver: 'file',
225
+ format: 'simple',
226
+ path: 'storage/logs',
227
+ prefix: '${projectName}',
228
+ level: 'warn',
229
+ maxFiles: 30,
230
+ },
231
+ ],
232
+ };
201
233
  `,
202
234
 
203
235
  // ─── config/app.js ────────────────────────────────────────────
@@ -358,37 +390,17 @@ millas make:controller UserController
358
390
 
359
391
  # Generate a model
360
392
  millas make:model User
361
- \`\`\`
362
393
 
363
- ## Database Migrations (Django-style)
364
-
365
- Millas handles migrations automatically — you only edit your model files.
366
-
367
- \`\`\`bash
368
- # 1. Edit app/models/User.js — add, remove, or change fields
369
- # 2. Generate migration files from your changes
370
- millas makemigrations
371
-
372
- # 3. Apply pending migrations to the database
394
+ # Run migrations
373
395
  millas migrate
374
396
  \`\`\`
375
397
 
376
- Other migration commands:
377
-
378
- \`\`\`bash
379
- millas migrate:status # Show which migrations have run
380
- millas migrate:rollback # Undo the last batch
381
- millas migrate:fresh # Drop everything and re-run all migrations
382
- millas migrate:reset # Roll back all migrations
383
- millas migrate:refresh # Reset + re-run (like fresh but using down() methods)
384
- \`\`\`
385
-
386
398
  ## Project Structure
387
399
 
388
400
  \`\`\`
389
401
  app/
390
402
  controllers/ # HTTP controllers
391
- models/ # ORM models ← only file you edit for schema changes
403
+ models/ # ORM models
392
404
  services/ # Business logic
393
405
  middleware/ # HTTP middleware
394
406
  jobs/ # Background jobs
@@ -396,14 +408,13 @@ bootstrap/
396
408
  app.js # Application entry point
397
409
  config/ # Configuration files
398
410
  database/
399
- migrations/ # Auto-generated — do not edit by hand
411
+ migrations/ # Database migrations
400
412
  seeders/ # Database seeders
401
413
  routes/
402
414
  web.js # Web routes
403
415
  api.js # API routes
404
416
  storage/ # Logs, uploads
405
417
  providers/ # Service providers
406
- .millas/ # Migration snapshot (gitignored)
407
418
  \`\`\`
408
419
  `,
409
420
  };