koa3-cli 1.0.5 → 1.0.6

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 CHANGED
@@ -49,12 +49,16 @@ koa3-cli/
49
49
  │ │ └── user.js # 用户模型
50
50
  │ ├── middleware/ # 中间件目录
51
51
  │ │ ├── index.js # 中间件入口
52
- │ │ └── auth.js # 认证中间件示例
52
+ │ │ ├── auth.js # 认证中间件示例
53
+ │ │ └── requestLogger.js # 请求日志中间件
54
+ │ ├── lib/ # 基础能力目录
55
+ │ │ └── logger.js # 日志工具
53
56
  │ └── router.js # 路由配置
54
57
  ├── config/ # 配置文件目录
55
58
  │ ├── config.default.js # 默认配置
56
59
  │ ├── config.local.js # 本地开发配置
57
60
  │ └── config.prod.js # 生产环境配置
61
+ ├── logs/ # 日志输出目录(运行时自动创建)
58
62
  ├── public/ # 静态资源目录
59
63
  │ └── index.html # 首页
60
64
  ├── app.js # 应用入口文件
@@ -71,6 +75,7 @@ koa3-cli/
71
75
  - ✅ MVC 架构(Controller/Service/Model)
72
76
  - ✅ 中间件支持
73
77
  - ✅ 统一的错误处理
78
+ - ✅ 内置日志系统(访问日志、错误日志、请求追踪)
74
79
  - ✅ RESTful API 示例
75
80
 
76
81
  ## 快速开始
@@ -109,6 +114,31 @@ npm start
109
114
 
110
115
  可以通过 `.env` 文件配置环境变量(参考 `.env.example`)。
111
116
 
117
+ ### 日志配置
118
+
119
+ 日志系统默认开启控制台和文件输出,支持 4 个级别:`debug`、`info`、`warn`、`error`。
120
+
121
+ `.env` 可配置项:
122
+
123
+ ```bash
124
+ LOG_LEVEL=info
125
+ LOG_DIR=logs
126
+ LOG_ENABLE_CONSOLE=true
127
+ LOG_ENABLE_FILE=true
128
+ ```
129
+
130
+ 运行后会在日志目录按天生成文件:
131
+
132
+ - `<date>.log`:通用应用日志
133
+ - `<date>.access.log`:请求访问日志(JSON 行格式)
134
+ - `<date>.error.log`:错误级别日志
135
+
136
+ 请求日志中间件会自动处理 `x-request-id`:
137
+
138
+ - 如果请求头带有 `x-request-id`,服务端会透传并写入日志
139
+ - 如果没有,服务端会自动生成并在响应头返回
140
+ - 出错日志会带上 `requestId`,便于串联排查
141
+
112
142
  ## API 示例
113
143
 
114
144
  ### 获取用户列表
@@ -1,100 +1,100 @@
1
- const userService = require('../service/user');
2
-
3
- function logMeta(ctx, extra = {}) {
4
- return {
5
- requestId: ctx.state && ctx.state.requestId,
6
- method: ctx.method,
7
- url: ctx.originalUrl || ctx.url,
8
- ...extra
9
- };
10
- }
11
-
12
- class UserController {
13
- async list(ctx) {
14
- try {
15
- const users = await userService.getUserList();
16
- ctx.logger.info('User list fetched', logMeta(ctx, { count: Array.isArray(users) ? users.length : undefined }));
17
- ctx.body = users;
18
- } catch (error) {
19
- ctx.logger.error('Failed to fetch user list', logMeta(ctx, { message: error.message, stack: error.stack }));
20
- ctx.throw(500, error.message);
21
- }
22
- }
23
-
24
- async detail(ctx) {
25
- const { id } = ctx.params;
26
-
27
- try {
28
- const user = await userService.getUserById(id);
29
- if (!user) {
30
- ctx.logger.warn('User detail not found', logMeta(ctx, { userId: id }));
31
- ctx.status = 404;
32
- ctx.body = { message: 'User not found' };
33
- return;
34
- }
35
-
36
- ctx.logger.info('User detail fetched', logMeta(ctx, { userId: id }));
37
- ctx.body = user;
38
- } catch (error) {
39
- ctx.logger.error('Failed to fetch user detail', logMeta(ctx, { userId: id, message: error.message, stack: error.stack }));
40
- ctx.throw(500, error.message);
41
- }
42
- }
43
-
44
- async create(ctx) {
45
- const userData = ctx.request.body;
46
-
47
- try {
48
- const user = await userService.createUser(userData);
49
- ctx.logger.info('User created', logMeta(ctx, { userId: user && user.id }));
50
- ctx.status = 201;
51
- ctx.body = user;
52
- } catch (error) {
53
- ctx.logger.error('Failed to create user', logMeta(ctx, { message: error.message, stack: error.stack }));
54
- ctx.throw(500, error.message);
55
- }
56
- }
57
-
58
- async update(ctx) {
59
- const { id } = ctx.params;
60
- const userData = ctx.request.body;
61
-
62
- try {
63
- const user = await userService.updateUser(id, userData);
64
- if (!user) {
65
- ctx.logger.warn('User update target not found', logMeta(ctx, { userId: id }));
66
- ctx.status = 404;
67
- ctx.body = { message: 'User not found' };
68
- return;
69
- }
70
-
71
- ctx.logger.info('User updated', logMeta(ctx, { userId: id }));
72
- ctx.body = user;
73
- } catch (error) {
74
- ctx.logger.error('Failed to update user', logMeta(ctx, { userId: id, message: error.message, stack: error.stack }));
75
- ctx.throw(500, error.message);
76
- }
77
- }
78
-
79
- async delete(ctx) {
80
- const { id } = ctx.params;
81
-
82
- try {
83
- const result = await userService.deleteUser(id);
84
- if (!result) {
85
- ctx.logger.warn('User delete target not found', logMeta(ctx, { userId: id }));
86
- ctx.status = 404;
87
- ctx.body = { message: 'User not found' };
88
- return;
89
- }
90
-
91
- ctx.logger.info('User deleted', logMeta(ctx, { userId: id }));
92
- ctx.status = 204;
93
- } catch (error) {
94
- ctx.logger.error('Failed to delete user', logMeta(ctx, { userId: id, message: error.message, stack: error.stack }));
95
- ctx.throw(500, error.message);
96
- }
97
- }
98
- }
99
-
1
+ const userService = require('../service/user');
2
+
3
+ function logMeta(ctx, extra = {}) {
4
+ return {
5
+ requestId: ctx.state && ctx.state.requestId,
6
+ method: ctx.method,
7
+ url: ctx.originalUrl || ctx.url,
8
+ ...extra
9
+ };
10
+ }
11
+
12
+ class UserController {
13
+ async list(ctx) {
14
+ try {
15
+ const users = await userService.getUserList();
16
+ ctx.logger.info('User list fetched', logMeta(ctx, { count: Array.isArray(users) ? users.length : undefined }));
17
+ ctx.body = users;
18
+ } catch (error) {
19
+ ctx.logger.error('Failed to fetch user list', logMeta(ctx, { message: error.message, stack: error.stack }));
20
+ ctx.throw(500, error.message);
21
+ }
22
+ }
23
+
24
+ async detail(ctx) {
25
+ const { id } = ctx.params;
26
+
27
+ try {
28
+ const user = await userService.getUserById(id);
29
+ if (!user) {
30
+ ctx.logger.warn('User detail not found', logMeta(ctx, { userId: id }));
31
+ ctx.status = 404;
32
+ ctx.body = { message: 'User not found' };
33
+ return;
34
+ }
35
+
36
+ ctx.logger.info('User detail fetched', logMeta(ctx, { userId: id }));
37
+ ctx.body = user;
38
+ } catch (error) {
39
+ ctx.logger.error('Failed to fetch user detail', logMeta(ctx, { userId: id, message: error.message, stack: error.stack }));
40
+ ctx.throw(500, error.message);
41
+ }
42
+ }
43
+
44
+ async create(ctx) {
45
+ const userData = ctx.request.body;
46
+
47
+ try {
48
+ const user = await userService.createUser(userData);
49
+ ctx.logger.info('User created', logMeta(ctx, { userId: user && user.id }));
50
+ ctx.status = 201;
51
+ ctx.body = user;
52
+ } catch (error) {
53
+ ctx.logger.error('Failed to create user', logMeta(ctx, { message: error.message, stack: error.stack }));
54
+ ctx.throw(500, error.message);
55
+ }
56
+ }
57
+
58
+ async update(ctx) {
59
+ const { id } = ctx.params;
60
+ const userData = ctx.request.body;
61
+
62
+ try {
63
+ const user = await userService.updateUser(id, userData);
64
+ if (!user) {
65
+ ctx.logger.warn('User update target not found', logMeta(ctx, { userId: id }));
66
+ ctx.status = 404;
67
+ ctx.body = { message: 'User not found' };
68
+ return;
69
+ }
70
+
71
+ ctx.logger.info('User updated', logMeta(ctx, { userId: id }));
72
+ ctx.body = user;
73
+ } catch (error) {
74
+ ctx.logger.error('Failed to update user', logMeta(ctx, { userId: id, message: error.message, stack: error.stack }));
75
+ ctx.throw(500, error.message);
76
+ }
77
+ }
78
+
79
+ async delete(ctx) {
80
+ const { id } = ctx.params;
81
+
82
+ try {
83
+ const result = await userService.deleteUser(id);
84
+ if (!result) {
85
+ ctx.logger.warn('User delete target not found', logMeta(ctx, { userId: id }));
86
+ ctx.status = 404;
87
+ ctx.body = { message: 'User not found' };
88
+ return;
89
+ }
90
+
91
+ ctx.logger.info('User deleted', logMeta(ctx, { userId: id }));
92
+ ctx.status = 204;
93
+ } catch (error) {
94
+ ctx.logger.error('Failed to delete user', logMeta(ctx, { userId: id, message: error.message, stack: error.stack }));
95
+ ctx.throw(500, error.message);
96
+ }
97
+ }
98
+ }
99
+
100
100
  module.exports = new UserController();
package/app/lib/logger.js CHANGED
@@ -1,141 +1,141 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const util = require('util');
4
-
5
- const LEVEL_WEIGHT = {
6
- debug: 10,
7
- info: 20,
8
- warn: 30,
9
- error: 40
10
- };
11
-
12
- function normalizeLevel(level) {
13
- const resolved = String(level || 'info').toLowerCase();
14
- return LEVEL_WEIGHT[resolved] ? resolved : 'info';
15
- }
16
-
17
- function formatDate(date = new Date()) {
18
- const pad = (n) => String(n).padStart(2, '0');
19
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
20
- }
21
-
22
- function formatTimestamp(date = new Date()) {
23
- return date.toLocaleString();
24
- }
25
-
26
- function safeSerialize(meta) {
27
- if (meta === undefined) {
28
- return '';
29
- }
30
-
31
- if (meta instanceof Error) {
32
- return JSON.stringify({
33
- name: meta.name,
34
- message: meta.message,
35
- stack: meta.stack
36
- });
37
- }
38
-
39
- if (typeof meta === 'string') {
40
- return meta;
41
- }
42
-
43
- try {
44
- return JSON.stringify(meta);
45
- } catch (error) {
46
- return util.inspect(meta, { depth: 4, breakLength: 120 });
47
- }
48
- }
49
-
50
- class Logger {
51
- constructor(options = {}) {
52
- this.level = normalizeLevel(options.level);
53
- this.enableConsole = options.enableConsole !== false;
54
- this.enableFile = options.enableFile !== false;
55
- this.dir = options.dir || 'logs';
56
- this.appName = options.appName || 'koa3-cli';
57
- this.cwd = options.cwd || process.cwd();
58
- this.logDir = path.isAbsolute(this.dir) ? this.dir : path.join(this.cwd, this.dir);
59
-
60
- if (this.enableFile) {
61
- fs.mkdirSync(this.logDir, { recursive: true });
62
- }
63
- }
64
-
65
- shouldLog(level) {
66
- return LEVEL_WEIGHT[level] >= LEVEL_WEIGHT[this.level];
67
- }
68
-
69
- write(level, message, meta) {
70
- if (!this.shouldLog(level)) {
71
- return;
72
- }
73
-
74
- const timestamp = formatTimestamp();
75
- const metaText = safeSerialize(meta);
76
- const line = `[${timestamp}] [${this.appName}] [${level.toUpperCase()}] ${message}${metaText ? ` ${metaText}` : ''}`;
77
-
78
- if (this.enableConsole) {
79
- const printer = level === 'error' ? console.error : level === 'warn' ? console.warn : console.log;
80
- printer(line);
81
- }
82
-
83
- if (!this.enableFile) {
84
- return;
85
- }
86
-
87
- const date = formatDate();
88
- const commonPath = path.join(this.logDir, `${date}.log`);
89
- fs.appendFile(commonPath, `${line}\n`, () => {});
90
-
91
- if (level === 'error') {
92
- const errorPath = path.join(this.logDir, `${date}.error.log`);
93
- fs.appendFile(errorPath, `${line}\n`, () => {});
94
- }
95
- }
96
-
97
- access(data) {
98
- const line = {
99
- timestamp: formatTimestamp(),
100
- type: 'access',
101
- app: this.appName,
102
- ...data
103
- };
104
-
105
- if (this.enableConsole && this.shouldLog('info')) {
106
- console.log(`[${line.timestamp}] [${this.appName}] [ACCESS] ${line.method} ${line.url} ${line.status} ${line.duration}ms ${line.requestId}`);
107
- }
108
-
109
- if (!this.enableFile) {
110
- return;
111
- }
112
-
113
- const date = formatDate();
114
- const accessPath = path.join(this.logDir, `${date}.access.log`);
115
- fs.appendFile(accessPath, `${JSON.stringify(line)}\n`, () => {});
116
- }
117
-
118
- debug(message, meta) {
119
- this.write('debug', message, meta);
120
- }
121
-
122
- info(message, meta) {
123
- this.write('info', message, meta);
124
- }
125
-
126
- warn(message, meta) {
127
- this.write('warn', message, meta);
128
- }
129
-
130
- error(message, meta) {
131
- this.write('error', message, meta);
132
- }
133
- }
134
-
135
- function createLogger(options) {
136
- return new Logger(options);
137
- }
138
-
139
- module.exports = {
140
- createLogger
141
- };
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const util = require('util');
4
+
5
+ const LEVEL_WEIGHT = {
6
+ debug: 10,
7
+ info: 20,
8
+ warn: 30,
9
+ error: 40
10
+ };
11
+
12
+ function normalizeLevel(level) {
13
+ const resolved = String(level || 'info').toLowerCase();
14
+ return LEVEL_WEIGHT[resolved] ? resolved : 'info';
15
+ }
16
+
17
+ function formatDate(date = new Date()) {
18
+ const pad = (n) => String(n).padStart(2, '0');
19
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
20
+ }
21
+
22
+ function formatTimestamp(date = new Date()) {
23
+ return date.toLocaleString();
24
+ }
25
+
26
+ function safeSerialize(meta) {
27
+ if (meta === undefined) {
28
+ return '';
29
+ }
30
+
31
+ if (meta instanceof Error) {
32
+ return JSON.stringify({
33
+ name: meta.name,
34
+ message: meta.message,
35
+ stack: meta.stack
36
+ });
37
+ }
38
+
39
+ if (typeof meta === 'string') {
40
+ return meta;
41
+ }
42
+
43
+ try {
44
+ return JSON.stringify(meta);
45
+ } catch (error) {
46
+ return util.inspect(meta, { depth: 4, breakLength: 120 });
47
+ }
48
+ }
49
+
50
+ class Logger {
51
+ constructor(options = {}) {
52
+ this.level = normalizeLevel(options.level);
53
+ this.enableConsole = options.enableConsole !== false;
54
+ this.enableFile = options.enableFile !== false;
55
+ this.dir = options.dir || 'logs';
56
+ this.appName = options.appName || 'koa3-cli';
57
+ this.cwd = options.cwd || process.cwd();
58
+ this.logDir = path.isAbsolute(this.dir) ? this.dir : path.join(this.cwd, this.dir);
59
+
60
+ if (this.enableFile) {
61
+ fs.mkdirSync(this.logDir, { recursive: true });
62
+ }
63
+ }
64
+
65
+ shouldLog(level) {
66
+ return LEVEL_WEIGHT[level] >= LEVEL_WEIGHT[this.level];
67
+ }
68
+
69
+ write(level, message, meta) {
70
+ if (!this.shouldLog(level)) {
71
+ return;
72
+ }
73
+
74
+ const timestamp = formatTimestamp();
75
+ const metaText = safeSerialize(meta);
76
+ const line = `[${timestamp}] [${this.appName}] [${level.toUpperCase()}] ${message}${metaText ? ` ${metaText}` : ''}`;
77
+
78
+ if (this.enableConsole) {
79
+ const printer = level === 'error' ? console.error : level === 'warn' ? console.warn : console.log;
80
+ printer(line);
81
+ }
82
+
83
+ if (!this.enableFile) {
84
+ return;
85
+ }
86
+
87
+ const date = formatDate();
88
+ const commonPath = path.join(this.logDir, `${date}.log`);
89
+ fs.appendFile(commonPath, `${line}\n`, () => {});
90
+
91
+ if (level === 'error') {
92
+ const errorPath = path.join(this.logDir, `${date}.error.log`);
93
+ fs.appendFile(errorPath, `${line}\n`, () => {});
94
+ }
95
+ }
96
+
97
+ access(data) {
98
+ const line = {
99
+ timestamp: formatTimestamp(),
100
+ type: 'access',
101
+ app: this.appName,
102
+ ...data
103
+ };
104
+
105
+ if (this.enableConsole && this.shouldLog('info')) {
106
+ console.log(`[${line.timestamp}] [${this.appName}] [ACCESS] ${line.method} ${line.url} ${line.status} ${line.duration}ms ${line.requestId}`);
107
+ }
108
+
109
+ if (!this.enableFile) {
110
+ return;
111
+ }
112
+
113
+ const date = formatDate();
114
+ const accessPath = path.join(this.logDir, `${date}.access.log`);
115
+ fs.appendFile(accessPath, `${JSON.stringify(line)}\n`, () => {});
116
+ }
117
+
118
+ debug(message, meta) {
119
+ this.write('debug', message, meta);
120
+ }
121
+
122
+ info(message, meta) {
123
+ this.write('info', message, meta);
124
+ }
125
+
126
+ warn(message, meta) {
127
+ this.write('warn', message, meta);
128
+ }
129
+
130
+ error(message, meta) {
131
+ this.write('error', message, meta);
132
+ }
133
+ }
134
+
135
+ function createLogger(options) {
136
+ return new Logger(options);
137
+ }
138
+
139
+ module.exports = {
140
+ createLogger
141
+ };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * 全局错误处理中间件:捕获异常、记录日志、统一错误响应
3
+ */
4
+ module.exports = function createErrorHandler(config, logger) {
5
+ return async function errorHandler(ctx, next) {
6
+ try {
7
+ await next();
8
+ } catch (err) {
9
+ ctx.status = err.status || 500;
10
+ ctx.body = {
11
+ success: false,
12
+ message: err.message || 'Internal Server Error',
13
+ ...(config.env === 'development' && { stack: err.stack })
14
+ };
15
+
16
+ logger.error('Request failed', {
17
+ requestId: ctx.state && ctx.state.requestId,
18
+ method: ctx.method,
19
+ url: ctx.originalUrl || ctx.url,
20
+ status: ctx.status,
21
+ message: err.message,
22
+ stack: err.stack
23
+ });
24
+
25
+ ctx.app.emit('error', err, ctx);
26
+ }
27
+ };
28
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 404 兜底中间件:未匹配路由时返回统一格式
3
+ */
4
+ module.exports = async function notFound(ctx) {
5
+ if (ctx.status === 404) {
6
+ ctx.body = {
7
+ success: false,
8
+ message: 'Not Found'
9
+ };
10
+ }
11
+ };
@@ -1,27 +1,27 @@
1
- function createRequestId() {
2
- return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
3
- }
4
-
5
- module.exports = function requestLogger(logger) {
6
- return async function requestLoggerMiddleware(ctx, next) {
7
- const start = Date.now();
8
- const requestId = ctx.get('x-request-id') || createRequestId();
9
-
10
- ctx.state.requestId = requestId;
11
- ctx.set('x-request-id', requestId);
12
-
13
- try {
14
- await next();
15
- } finally {
16
- const duration = Date.now() - start;
17
- logger.access({
18
- requestId,
19
- method: ctx.method,
20
- url: ctx.originalUrl || ctx.url,
21
- status: ctx.status,
22
- duration,
23
- ip: ctx.ip
24
- });
25
- }
26
- };
27
- };
1
+ function createRequestId() {
2
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
3
+ }
4
+
5
+ module.exports = function requestLogger(logger) {
6
+ return async function requestLoggerMiddleware(ctx, next) {
7
+ const start = Date.now();
8
+ const requestId = ctx.get('x-request-id') || createRequestId();
9
+
10
+ ctx.state.requestId = requestId;
11
+ ctx.set('x-request-id', requestId);
12
+
13
+ try {
14
+ await next();
15
+ } finally {
16
+ const duration = Date.now() - start;
17
+ logger.access({
18
+ requestId,
19
+ method: ctx.method,
20
+ url: ctx.originalUrl || ctx.url,
21
+ status: ctx.status,
22
+ duration,
23
+ ip: ctx.ip
24
+ });
25
+ }
26
+ };
27
+ };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 进程级错误监听:未捕获的 Promise 与异常
3
+ */
4
+ function setupProcessEvents(logger) {
5
+ process.on('unhandledRejection', (reason) => {
6
+ logger.error('Unhandled promise rejection', reason);
7
+ });
8
+
9
+ process.on('uncaughtException', (error) => {
10
+ logger.error('Uncaught exception', error);
11
+ });
12
+ }
13
+
14
+ module.exports = setupProcessEvents;
package/app/setup.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * 应用挂载:静态资源、视图、中间件、路由等
3
+ * 保持 app.js 只做「创建实例 + 调用 setup + 监听 + 启动」
4
+ */
5
+ const path = require('path');
6
+ const fs = require('fs');
7
+ const bodyParser = require('koa-bodyparser');
8
+ const serveStatic = require('koa-static');
9
+ const views = require('@ladjs/koa-views');
10
+
11
+ const createRequestLogger = require('./middleware/requestLogger');
12
+ const createErrorHandler = require('./middleware/errorHandler');
13
+ const notFound = require('./middleware/notFound');
14
+ const middleware = require('./middleware');
15
+ const router = require('./router');
16
+
17
+ const rootDir = path.join(__dirname, '..');
18
+
19
+ function setup(app, config, logger) {
20
+ // 静态资源
21
+ if (config.static && config.static.enable !== false) {
22
+ const staticPath = path.join(rootDir, config.static.dir || 'public');
23
+ if (fs.existsSync(staticPath)) {
24
+ app.use(serveStatic(staticPath, config.static.options || {}));
25
+ }
26
+ }
27
+
28
+ // 视图
29
+ if (config.view && config.view.enable !== false) {
30
+ const viewPath = path.join(rootDir, config.view.root || 'app/view');
31
+ if (fs.existsSync(viewPath)) {
32
+ app.use(views(viewPath, config.view.options || { extension: 'ejs' }));
33
+ }
34
+ }
35
+
36
+ app.use(bodyParser(config.bodyParser || {}));
37
+ app.use(createRequestLogger(logger));
38
+
39
+ if (middleware && typeof middleware === 'function') {
40
+ app.use(middleware);
41
+ }
42
+
43
+ app.use(createErrorHandler(config, logger));
44
+ app.use(router.routes()).use(router.allowedMethods());
45
+ app.use(notFound);
46
+ }
47
+
48
+ module.exports = setup;
package/app.js CHANGED
@@ -1,119 +1,38 @@
1
- const Koa = require('koa');
2
- const bodyParser = require('koa-bodyparser');
3
- const serveStatic = require('koa-static');
4
- const views = require('@ladjs/koa-views');
5
- const path = require('path');
6
- const fs = require('fs');
7
- const { createLogger } = require('./app/lib/logger');
8
- const createRequestLogger = require('./app/middleware/requestLogger');
9
-
10
- require('dotenv').config();
11
-
12
- const env = process.env.NODE_ENV || 'development';
13
- const defaultConfig = require('./config/config.default');
14
- let envConfig = {};
15
- try {
16
- if (env === 'production') {
17
- envConfig = require('./config/config.prod');
18
- } else if (env === 'local' || env === 'development') {
19
- envConfig = require('./config/config.local');
20
- }
21
- } catch (e) {
22
- // Ignore missing env config override.
23
- }
24
- const config = Object.assign({}, defaultConfig, envConfig);
25
-
26
- const middleware = require('./app/middleware');
27
- const router = require('./app/router');
28
-
29
- const app = new Koa();
30
- const logger = createLogger({
31
- ...(config.logger || {}),
32
- appName: config.name || 'koa3-cli',
33
- cwd: __dirname
34
- });
35
-
36
- app.keys = config.keys || ['koa3-cli-secret-key'];
37
- app.context.logger = logger;
38
-
39
- if (config.static && config.static.enable !== false) {
40
- const staticPath = path.join(__dirname, config.static.dir || 'public');
41
- if (fs.existsSync(staticPath)) {
42
- app.use(serveStatic(staticPath, config.static.options || {}));
43
- }
44
- }
45
-
46
- if (config.view && config.view.enable !== false) {
47
- const viewPath = path.join(__dirname, config.view.root || 'app/view');
48
- if (fs.existsSync(viewPath)) {
49
- app.use(views(viewPath, config.view.options || {
50
- extension: 'ejs'
51
- }));
52
- }
53
- }
54
-
55
- app.use(bodyParser(config.bodyParser || {}));
56
- app.use(createRequestLogger(logger));
57
-
58
- if (middleware && typeof middleware === 'function') {
59
- app.use(middleware);
60
- }
61
-
62
- app.use(async (ctx, next) => {
63
- try {
64
- await next();
65
- } catch (err) {
66
- ctx.status = err.status || 500;
67
- ctx.body = {
68
- success: false,
69
- message: err.message || 'Internal Server Error',
70
- ...(config.env === 'development' && { stack: err.stack })
71
- };
72
-
73
- logger.error('Request failed', {
74
- requestId: ctx.state && ctx.state.requestId,
75
- method: ctx.method,
76
- url: ctx.originalUrl || ctx.url,
77
- status: ctx.status,
78
- message: err.message,
79
- stack: err.stack
80
- });
81
-
82
- ctx.app.emit('error', err, ctx);
83
- }
84
- });
85
-
86
- app.use(router.routes()).use(router.allowedMethods());
87
-
88
- app.use(async (ctx) => {
89
- if (ctx.status === 404) {
90
- ctx.body = {
91
- success: false,
92
- message: 'Not Found'
93
- };
94
- }
95
- });
96
-
97
- app.on('error', (err, ctx) => {
98
- logger.error('Server error event', {
99
- requestId: ctx && ctx.state && ctx.state.requestId,
100
- message: err.message,
101
- stack: err.stack
102
- });
103
- });
104
-
105
- process.on('unhandledRejection', (reason) => {
106
- logger.error('Unhandled promise rejection', reason);
107
- });
108
-
109
- process.on('uncaughtException', (error) => {
110
- logger.error('Uncaught exception', error);
111
- });
112
-
113
- const port = config.port || 3000;
114
- app.listen(port, () => {
115
- logger.info(`Server is running on http://localhost:${port}`);
116
- logger.info(`Environment: ${config.env}`);
117
- });
118
-
1
+ require('dotenv').config();
2
+
3
+ const Koa = require('koa');
4
+ const { loadConfig } = require('./config/loader');
5
+ const { createLogger } = require('./app/lib/logger');
6
+ const setup = require('./app/setup');
7
+ const setupProcessEvents = require('./app/processEvents');
8
+
9
+ const config = loadConfig();
10
+ const app = new Koa();
11
+ const logger = createLogger({
12
+ ...(config.logger || {}),
13
+ appName: config.name || 'koa3-cli',
14
+ cwd: __dirname
15
+ });
16
+
17
+ app.keys = config.keys || ['koa3-cli-secret-key'];
18
+ app.context.logger = logger;
19
+
20
+ setup(app, config, logger);
21
+
22
+ app.on('error', (err, ctx) => {
23
+ logger.error('Server error event', {
24
+ requestId: ctx && ctx.state && ctx.state.requestId,
25
+ message: err.message,
26
+ stack: err.stack
27
+ });
28
+ });
29
+
30
+ setupProcessEvents(logger);
31
+
32
+ const port = config.port || 3000;
33
+ app.listen(port, () => {
34
+ logger.info(`Server is running on http://localhost:${port}`);
35
+ logger.info(`Environment: ${config.env}`);
36
+ });
37
+
119
38
  module.exports = app;
@@ -1,83 +1,83 @@
1
- /**
2
- * Default config loaded in all environments.
3
- */
4
- module.exports = {
5
- // Application name
6
- name: 'koa3-cli',
7
-
8
- // Runtime env: development, production, test
9
- env: process.env.NODE_ENV || 'development',
10
-
11
- // Server port
12
- port: process.env.PORT || 3000,
13
-
14
- // Cookie signing keys
15
- keys: process.env.KEYS ? process.env.KEYS.split(',') : ['koa3-cli-secret-key'],
16
-
17
- // Static assets
18
- static: {
19
- enable: true,
20
- dir: 'public',
21
- options: {
22
- maxAge: 365 * 24 * 60 * 60 * 1000,
23
- gzip: true
24
- }
25
- },
26
-
27
- // Docs build config
28
- docs: {
29
- enable: true,
30
- buildDir: 'public/docs'
31
- },
32
-
33
- // View engine
34
- view: {
35
- enable: true,
36
- root: 'app/view',
37
- options: {
38
- extension: 'ejs',
39
- map: {
40
- html: 'ejs'
41
- }
42
- }
43
- },
44
-
45
- // bodyParser
46
- bodyParser: {
47
- enableTypes: ['json', 'form', 'text'],
48
- jsonLimit: '10mb',
49
- formLimit: '10mb'
50
- },
51
-
52
- // Database (example)
53
- database: {
54
- client: 'mysql',
55
- connection: {
56
- host: process.env.DB_HOST || 'localhost',
57
- port: process.env.DB_PORT || 3306,
58
- user: process.env.DB_USER || 'root',
59
- password: process.env.DB_PASSWORD || '',
60
- database: process.env.DB_NAME || 'test'
61
- },
62
- pool: {
63
- min: 2,
64
- max: 10
65
- }
66
- },
67
-
68
- // Redis (example)
69
- redis: {
70
- host: process.env.REDIS_HOST || 'localhost',
71
- port: process.env.REDIS_PORT || 6379,
72
- password: process.env.REDIS_PASSWORD || '',
73
- db: process.env.REDIS_DB || 0
74
- },
75
-
76
- // Logger
77
- logger: {
78
- level: process.env.LOG_LEVEL || 'info',
79
- dir: process.env.LOG_DIR || 'logs',
80
- enableConsole: process.env.LOG_ENABLE_CONSOLE !== 'false',
81
- enableFile: process.env.LOG_ENABLE_FILE !== 'false'
82
- }
1
+ /**
2
+ * Default config loaded in all environments.
3
+ */
4
+ module.exports = {
5
+ // Application name
6
+ name: 'koa3-cli',
7
+
8
+ // Runtime env: development, production, test
9
+ env: process.env.NODE_ENV || 'development',
10
+
11
+ // Server port
12
+ port: process.env.PORT || 3000,
13
+
14
+ // Cookie signing keys
15
+ keys: process.env.KEYS ? process.env.KEYS.split(',') : ['koa3-cli-secret-key'],
16
+
17
+ // Static assets
18
+ static: {
19
+ enable: true,
20
+ dir: 'public',
21
+ options: {
22
+ maxAge: 365 * 24 * 60 * 60 * 1000,
23
+ gzip: true
24
+ }
25
+ },
26
+
27
+ // Docs build config
28
+ docs: {
29
+ enable: true,
30
+ buildDir: 'public/docs'
31
+ },
32
+
33
+ // View engine
34
+ view: {
35
+ enable: true,
36
+ root: 'app/view',
37
+ options: {
38
+ extension: 'ejs',
39
+ map: {
40
+ html: 'ejs'
41
+ }
42
+ }
43
+ },
44
+
45
+ // bodyParser
46
+ bodyParser: {
47
+ enableTypes: ['json', 'form', 'text'],
48
+ jsonLimit: '10mb',
49
+ formLimit: '10mb'
50
+ },
51
+
52
+ // Database (example)
53
+ database: {
54
+ client: 'mysql',
55
+ connection: {
56
+ host: process.env.DB_HOST || 'localhost',
57
+ port: process.env.DB_PORT || 3306,
58
+ user: process.env.DB_USER || 'root',
59
+ password: process.env.DB_PASSWORD || '',
60
+ database: process.env.DB_NAME || 'test'
61
+ },
62
+ pool: {
63
+ min: 2,
64
+ max: 10
65
+ }
66
+ },
67
+
68
+ // Redis (example)
69
+ redis: {
70
+ host: process.env.REDIS_HOST || 'localhost',
71
+ port: process.env.REDIS_PORT || 6379,
72
+ password: process.env.REDIS_PASSWORD || '',
73
+ db: process.env.REDIS_DB || 0
74
+ },
75
+
76
+ // Logger
77
+ logger: {
78
+ level: process.env.LOG_LEVEL || 'info',
79
+ dir: process.env.LOG_DIR || 'logs',
80
+ enableConsole: process.env.LOG_ENABLE_CONSOLE !== 'false',
81
+ enableFile: process.env.LOG_ENABLE_FILE !== 'false'
82
+ }
83
83
  };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * 配置加载器:合并 default + 环境配置
3
+ */
4
+ const path = require('path');
5
+
6
+ const defaultConfig = require('./config.default');
7
+
8
+ function loadConfig() {
9
+ const env = process.env.NODE_ENV || 'development';
10
+ let envConfig = {};
11
+
12
+ try {
13
+ if (env === 'production') {
14
+ envConfig = require('./config.prod');
15
+ } else if (env === 'local' || env === 'development') {
16
+ envConfig = require('./config.local');
17
+ }
18
+ } catch (e) {
19
+ // 忽略缺失的环境配置文件
20
+ }
21
+
22
+ return Object.assign({}, defaultConfig, envConfig);
23
+ }
24
+
25
+ module.exports = { loadConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koa3-cli",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Koa3脚手架",
5
5
  "main": "app.js",
6
6
  "bin": {
package/public/index.html CHANGED
@@ -69,6 +69,10 @@
69
69
  <h3>✅ 错误处理</h3>
70
70
  <p>统一的错误处理机制</p>
71
71
  </div>
72
+ <div class="feature">
73
+ <h3>📝 日志系统</h3>
74
+ <p>支持访问日志、错误日志和 requestId 链路追踪</p>
75
+ </div>
72
76
  <div class="feature">
73
77
  <h3>⚡ CLI 工具</h3>
74
78
  <p>一键创建项目,快速上手</p>
@@ -144,7 +148,10 @@ npm run dev</code></pre>
144
148
  │ │ └── user.js # 用户模型
145
149
  │ ├── middleware/ # 中间件目录
146
150
  │ │ ├── index.js # 中间件入口
147
- │ │ └── auth.js # 认证中间件示例
151
+ │ │ ├── auth.js # 认证中间件示例
152
+ │ │ └── requestLogger.js # 请求日志中间件
153
+ │ ├── lib/ # 基础能力目录
154
+ │ │ └── logger.js # 日志工具
148
155
  │ ├── router.js # 路由配置
149
156
  │ └── view/ # 视图模板目录(可选)
150
157
  │ └── docs.ejs # 页面模板
@@ -157,6 +164,7 @@ npm run dev</code></pre>
157
164
  ├── public/ # 静态资源目录
158
165
  │ ├── docs/ # 文档资源
159
166
  │ └── index.html # 文档首页
167
+ ├── logs/ # 日志目录(运行时自动创建)
160
168
  ├── app.js # 应用入口文件
161
169
  ├── package.json # 项目配置
162
170
  ├── env.example # 环境变量示例
@@ -237,7 +245,16 @@ REDIS_PASSWORD=
237
245
  REDIS_DB=0
238
246
 
239
247
  # 日志级别
240
- LOG_LEVEL=info</code></pre>
248
+ LOG_LEVEL=info
249
+
250
+ # 日志目录
251
+ LOG_DIR=logs
252
+
253
+ # 是否输出到控制台
254
+ LOG_ENABLE_CONSOLE=true
255
+
256
+ # 是否输出到文件
257
+ LOG_ENABLE_FILE=true</code></pre>
241
258
  <h2>配置项说明</h2>
242
259
  <h3>基础配置</h3>
243
260
  <pre><code class="language-javascript">{
@@ -288,11 +305,26 @@ LOG_LEVEL=info</code></pre>
288
305
  password: '',
289
306
  db: 0
290
307
  }</code></pre>
291
- <h3>日志配置</h3>
292
- <pre><code class="language-javascript">logger: {
308
+ <h3>日志配置</h3>
309
+ <pre><code class="language-javascript">logger: {
293
310
  level: 'info', // 日志级别: debug, info, warn, error
294
- dir: 'logs' // 日志目录
311
+ dir: 'logs', // 日志目录
312
+ enableConsole: true, // 是否输出到控制台
313
+ enableFile: true // 是否输出到文件
295
314
  }</code></pre>
315
+ <h3>日志文件说明</h3>
316
+ <ul>
317
+ <li><code>YYYY-MM-DD.log</code>: 通用应用日志</li>
318
+ <li><code>YYYY-MM-DD.access.log</code>: 请求访问日志(JSON 行格式)</li>
319
+ <li><code>YYYY-MM-DD.error.log</code>: 错误级别日志</li>
320
+ </ul>
321
+ <h3>请求追踪(x-request-id)</h3>
322
+ <p>每个请求都会自动记录 <code>requestId</code>:</p>
323
+ <ul>
324
+ <li>请求头带有 <code>x-request-id</code> 时会透传该值</li>
325
+ <li>未提供时服务端会自动生成并在响应头返回</li>
326
+ <li>访问日志与错误日志都会带上 <code>requestId</code>,便于串联排查</li>
327
+ </ul>
296
328
  </div>
297
329
 
298
330
  <!-- 开发指南 -->