koa3-cli 1.0.4 → 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,86 +1,97 @@
1
- /**
2
- * 用户控制器
3
- */
4
- const userService = require('../service/user');
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
+ }
5
11
 
6
12
  class UserController {
7
- /**
8
- * 获取用户列表
9
- */
10
13
  async list(ctx) {
11
14
  try {
12
15
  const users = await userService.getUserList();
16
+ ctx.logger.info('User list fetched', logMeta(ctx, { count: Array.isArray(users) ? users.length : undefined }));
13
17
  ctx.body = users;
14
18
  } catch (error) {
19
+ ctx.logger.error('Failed to fetch user list', logMeta(ctx, { message: error.message, stack: error.stack }));
15
20
  ctx.throw(500, error.message);
16
21
  }
17
22
  }
18
23
 
19
- /**
20
- * 获取用户详情
21
- */
22
24
  async detail(ctx) {
25
+ const { id } = ctx.params;
26
+
23
27
  try {
24
- const { id } = ctx.params;
25
28
  const user = await userService.getUserById(id);
26
29
  if (!user) {
30
+ ctx.logger.warn('User detail not found', logMeta(ctx, { userId: id }));
27
31
  ctx.status = 404;
28
32
  ctx.body = { message: 'User not found' };
29
33
  return;
30
34
  }
35
+
36
+ ctx.logger.info('User detail fetched', logMeta(ctx, { userId: id }));
31
37
  ctx.body = user;
32
38
  } catch (error) {
39
+ ctx.logger.error('Failed to fetch user detail', logMeta(ctx, { userId: id, message: error.message, stack: error.stack }));
33
40
  ctx.throw(500, error.message);
34
41
  }
35
42
  }
36
43
 
37
- /**
38
- * 创建用户
39
- */
40
44
  async create(ctx) {
45
+ const userData = ctx.request.body;
46
+
41
47
  try {
42
- const userData = ctx.request.body;
43
48
  const user = await userService.createUser(userData);
49
+ ctx.logger.info('User created', logMeta(ctx, { userId: user && user.id }));
44
50
  ctx.status = 201;
45
51
  ctx.body = user;
46
52
  } catch (error) {
53
+ ctx.logger.error('Failed to create user', logMeta(ctx, { message: error.message, stack: error.stack }));
47
54
  ctx.throw(500, error.message);
48
55
  }
49
56
  }
50
57
 
51
- /**
52
- * 更新用户
53
- */
54
58
  async update(ctx) {
59
+ const { id } = ctx.params;
60
+ const userData = ctx.request.body;
61
+
55
62
  try {
56
- const { id } = ctx.params;
57
- const userData = ctx.request.body;
58
63
  const user = await userService.updateUser(id, userData);
59
64
  if (!user) {
65
+ ctx.logger.warn('User update target not found', logMeta(ctx, { userId: id }));
60
66
  ctx.status = 404;
61
67
  ctx.body = { message: 'User not found' };
62
68
  return;
63
69
  }
70
+
71
+ ctx.logger.info('User updated', logMeta(ctx, { userId: id }));
64
72
  ctx.body = user;
65
73
  } catch (error) {
74
+ ctx.logger.error('Failed to update user', logMeta(ctx, { userId: id, message: error.message, stack: error.stack }));
66
75
  ctx.throw(500, error.message);
67
76
  }
68
77
  }
69
78
 
70
- /**
71
- * 删除用户
72
- */
73
79
  async delete(ctx) {
80
+ const { id } = ctx.params;
81
+
74
82
  try {
75
- const { id } = ctx.params;
76
83
  const result = await userService.deleteUser(id);
77
84
  if (!result) {
85
+ ctx.logger.warn('User delete target not found', logMeta(ctx, { userId: id }));
78
86
  ctx.status = 404;
79
87
  ctx.body = { message: 'User not found' };
80
88
  return;
81
89
  }
90
+
91
+ ctx.logger.info('User deleted', logMeta(ctx, { userId: id }));
82
92
  ctx.status = 204;
83
93
  } catch (error) {
94
+ ctx.logger.error('Failed to delete user', logMeta(ctx, { userId: id, message: error.message, stack: error.stack }));
84
95
  ctx.throw(500, error.message);
85
96
  }
86
97
  }
@@ -0,0 +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
+ };
@@ -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
+ };
@@ -0,0 +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
+ };
@@ -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,111 +1,38 @@
1
- const Koa = require('koa');
2
- const bodyParser = require('koa-bodyparser');
3
- const static = require('koa-static');
4
- const views = require('@ladjs/koa-views');
5
- const path = require('path');
6
- const fs = require('fs');
7
-
8
- // 加载环境变量
9
1
  require('dotenv').config();
10
2
 
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
- // 环境配置文件不存在时忽略
23
- }
24
- const config = Object.assign({}, defaultConfig, envConfig);
25
-
26
- // 加载中间件
27
- const middleware = require('./app/middleware');
28
-
29
- // 加载路由
30
- const router = require('./app/router');
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');
31
8
 
9
+ const config = loadConfig();
32
10
  const app = new Koa();
33
-
34
- // 应用配置
35
- app.keys = config.keys || ['koa3-cli-secret-key'];
36
-
37
- // 静态资源
38
- if (config.static && config.static.enable !== false) {
39
- const staticPath = path.join(__dirname, config.static.dir || 'public');
40
- if (fs.existsSync(staticPath)) {
41
- app.use(static(staticPath, config.static.options || {}));
42
- }
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
- // 请求体解析
56
- app.use(bodyParser(config.bodyParser || {}));
57
-
58
- // 自定义中间件
59
- if (middleware && typeof middleware === 'function') {
60
- app.use(middleware);
61
- }
62
-
63
- // 错误处理中间件
64
- app.use(async (ctx, next) => {
65
- try {
66
- await next();
67
- } catch (err) {
68
- ctx.status = err.status || 500;
69
- ctx.body = {
70
- success: false,
71
- message: err.message || 'Internal Server Error',
72
- ...(config.env === 'development' && { stack: err.stack })
73
- };
74
- ctx.app.emit('error', err, ctx);
75
- }
76
- });
77
-
78
- // 日志中间件
79
- app.use(async (ctx, next) => {
80
- const start = Date.now();
81
- await next();
82
- const ms = Date.now() - start;
83
- console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
11
+ const logger = createLogger({
12
+ ...(config.logger || {}),
13
+ appName: config.name || 'koa3-cli',
14
+ cwd: __dirname
84
15
  });
85
16
 
86
- // 注册路由
87
- app.use(router.routes()).use(router.allowedMethods());
17
+ app.keys = config.keys || ['koa3-cli-secret-key'];
18
+ app.context.logger = logger;
88
19
 
89
- // 404 处理(必须在路由之后)
90
- app.use(async (ctx) => {
91
- if (ctx.status === 404) {
92
- ctx.body = {
93
- success: false,
94
- message: 'Not Found'
95
- };
96
- }
97
- });
20
+ setup(app, config, logger);
98
21
 
99
- // 错误事件监听
100
22
  app.on('error', (err, ctx) => {
101
- console.error('Server error:', err);
23
+ logger.error('Server error event', {
24
+ requestId: ctx && ctx.state && ctx.state.requestId,
25
+ message: err.message,
26
+ stack: err.stack
27
+ });
102
28
  });
103
29
 
104
- // 启动服务器
30
+ setupProcessEvents(logger);
31
+
105
32
  const port = config.port || 3000;
106
33
  app.listen(port, () => {
107
- console.log(`Server is running on http://localhost:${port}`);
108
- console.log(`Environment: ${config.env}`);
34
+ logger.info(`Server is running on http://localhost:${port}`);
35
+ logger.info(`Environment: ${config.env}`);
109
36
  });
110
37
 
111
38
  module.exports = app;
@@ -1,37 +1,36 @@
1
1
  /**
2
- * 默认配置文件
3
- * 所有环境都会加载此配置
2
+ * Default config loaded in all environments.
4
3
  */
5
4
  module.exports = {
6
- // 应用名称
5
+ // Application name
7
6
  name: 'koa3-cli',
8
-
9
- // 运行环境: development, production, test
7
+
8
+ // Runtime env: development, production, test
10
9
  env: process.env.NODE_ENV || 'development',
11
-
12
- // 服务端口
10
+
11
+ // Server port
13
12
  port: process.env.PORT || 3000,
14
-
15
- // 密钥,用于加密cookie等
13
+
14
+ // Cookie signing keys
16
15
  keys: process.env.KEYS ? process.env.KEYS.split(',') : ['koa3-cli-secret-key'],
17
-
18
- // 静态资源配置
16
+
17
+ // Static assets
19
18
  static: {
20
19
  enable: true,
21
20
  dir: 'public',
22
21
  options: {
23
- maxAge: 365 * 24 * 60 * 60 * 1000, // 1年
22
+ maxAge: 365 * 24 * 60 * 60 * 1000,
24
23
  gzip: true
25
24
  }
26
25
  },
27
-
28
- // VuePress 文档配置
26
+
27
+ // Docs build config
29
28
  docs: {
30
29
  enable: true,
31
30
  buildDir: 'public/docs'
32
31
  },
33
-
34
- // 视图配置
32
+
33
+ // View engine
35
34
  view: {
36
35
  enable: true,
37
36
  root: 'app/view',
@@ -42,15 +41,15 @@ module.exports = {
42
41
  }
43
42
  }
44
43
  },
45
-
46
- // bodyParser配置
44
+
45
+ // bodyParser
47
46
  bodyParser: {
48
47
  enableTypes: ['json', 'form', 'text'],
49
48
  jsonLimit: '10mb',
50
49
  formLimit: '10mb'
51
50
  },
52
-
53
- // 数据库配置(示例)
51
+
52
+ // Database (example)
54
53
  database: {
55
54
  client: 'mysql',
56
55
  connection: {
@@ -65,18 +64,20 @@ module.exports = {
65
64
  max: 10
66
65
  }
67
66
  },
68
-
69
- // Redis配置(示例)
67
+
68
+ // Redis (example)
70
69
  redis: {
71
70
  host: process.env.REDIS_HOST || 'localhost',
72
71
  port: process.env.REDIS_PORT || 6379,
73
72
  password: process.env.REDIS_PASSWORD || '',
74
73
  db: process.env.REDIS_DB || 0
75
74
  },
76
-
77
- // 日志配置
75
+
76
+ // Logger
78
77
  logger: {
79
78
  level: process.env.LOG_LEVEL || 'info',
80
- dir: 'logs'
79
+ dir: process.env.LOG_DIR || 'logs',
80
+ enableConsole: process.env.LOG_ENABLE_CONSOLE !== 'false',
81
+ enableFile: process.env.LOG_ENABLE_FILE !== 'false'
81
82
  }
82
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/env.example CHANGED
@@ -25,3 +25,7 @@ REDIS_DB=0
25
25
 
26
26
  # 日志级别: debug, info, warn, error
27
27
  LOG_LEVEL=info
28
+
29
+ LOG_DIR=logs
30
+ LOG_ENABLE_CONSOLE=true
31
+ LOG_ENABLE_FILE=true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koa3-cli",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Koa3脚手架",
5
5
  "main": "app.js",
6
6
  "bin": {
@@ -25,17 +25,17 @@
25
25
  "homepage": "https://atwzc.cn/",
26
26
  "license": "MIT",
27
27
  "dependencies": {
28
- "@koa/router": "^15.2.0",
28
+ "@koa/router": "^15.3.1",
29
29
  "@ladjs/koa-views": "^9.0.0",
30
- "dotenv": "^17.2.3",
31
- "ejs": "^3.1.10",
32
- "koa": "^3.1.1",
30
+ "dotenv": "^17.3.1",
31
+ "ejs": "^4.0.1",
32
+ "koa": "^3.1.2",
33
33
  "koa-bodyparser": "^4.4.1",
34
34
  "koa-cors": "^0.0.16",
35
35
  "koa-static": "^5.0.0"
36
36
  },
37
37
  "devDependencies": {
38
- "nodemon": "^3.1.11"
38
+ "nodemon": "^3.1.14"
39
39
  },
40
40
  "volta": {
41
41
  "node": "20.18.1"
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
  <!-- 开发指南 -->