Node.js + Express + SQLite 项目把日志和本地排查步骤搭好
小型 Node 服务最怕“页面坏了但终端没信息”。日志不是上线后才补的东西,本地开发阶段就应该让请求、错误、关键业务动作都能落到文件里。
下面用 Express + SQLite 举例,重点不是框架花活,而是把问题发生时能看的东西提前放好。
目录先分清楚
目录结构:
app-demo
├─ data
│ ├─ app.sqlite
│ └─ logs
│ ├─ app.log
│ └─ error.log
├─ src
│ ├─ logger.js
│ ├─ db.js
│ └─ server.js
└─ package.json
data/ 放运行期文件,src/ 放源码。这样备份、清理、部署时不容易把数据库和代码混在一起。
1. 初始化项目
mkdir app-demo
cd app-demo
npm init -y
npm install express better-sqlite3
package.json 加一个启动脚本:
{
"scripts": {
"start": "node src/server.js",
"dev": "node --watch src/server.js"
}
}
如果你的项目已经用别的 SQLite 包,也没关系。日志和排障思路一样。
2. 写一个只做文件落盘的 logger
src/logger.js:
const fs = require('fs');
const path = require('path');
const logDir = path.join(__dirname, '..', 'data', 'logs');
const appLogPath = path.join(logDir, 'app.log');
const errorLogPath = path.join(logDir, 'error.log');
fs.mkdirSync(logDir, { recursive: true });
fs.closeSync(fs.openSync(appLogPath, 'a'));
fs.closeSync(fs.openSync(errorLogPath, 'a'));
function writeLine(filePath, level, message, fields = {}) {
const entry = {
time: new Date().toISOString(),
level,
message,
...fields
};
fs.appendFileSync(filePath, `${JSON.stringify(entry)}\n`, 'utf8');
}
function info(message, fields) {
writeLine(appLogPath, 'info', message, fields);
}
function error(message, fields) {
writeLine(errorLogPath, 'error', message, fields);
}
module.exports = { info, error, appLogPath, errorLogPath };
本文用 JSON Lines,一行一条日志。排障时能 tail,也能用脚本按字段过滤。
3. 初始化 SQLite 表
src/db.js:
const path = require('path');
const Database = require('better-sqlite3');
const dbPath = path.join(__dirname, '..', 'data', 'app.sqlite');
const db = new Database(dbPath);
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
db.exec(`
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
body TEXT NOT NULL,
created_at TEXT NOT NULL
)
`);
module.exports = db;
WAL 对小型 Web 服务更友好一些:读写并发时不那么容易互相堵住。它不是万能药,慢查询和大事务仍然要单独处理。
4. 在 Express 里记录请求和错误
src/server.js:
const express = require('express');
const db = require('./db');
const logger = require('./logger');
const app = express();
app.use(express.json());
app.use((req, res, next) => {
const startedAt = Date.now();
res.on('finish', () => {
logger.info('request_finished', {
method: req.method,
path: req.path,
status: res.statusCode,
duration_ms: Date.now() - startedAt
});
});
next();
});
app.get('/health', (req, res) => {
res.json({ ok: true });
});
app.post('/notes', (req, res, next) => {
try {
const title = String(req.body.title || '').trim();
const body = String(req.body.body || '').trim();
if (!title || !body) {
return res.status(400).json({ error: 'title and body are required' });
}
const result = db.prepare(`
INSERT INTO notes(title, body, created_at)
VALUES(?, ?, ?)
`).run(title, body, new Date().toISOString());
logger.info('note_created', { note_id: result.lastInsertRowid });
res.status(201).json({ id: result.lastInsertRowid });
} catch (err) {
next(err);
}
});
app.get('/notes', (req, res, next) => {
try {
const notes = db.prepare(`
SELECT id, title, body, created_at
FROM notes
ORDER BY id DESC
LIMIT 20
`).all();
res.json({ notes });
} catch (err) {
next(err);
}
});
app.use((err, req, res, next) => {
logger.error('request_failed', {
method: req.method,
path: req.path,
message: err.message,
stack: err.stack
});
res.status(500).json({ error: 'internal server error' });
});
const port = Number(process.env.PORT || 3000);
app.listen(port, () => {
logger.info('server_started', { port });
console.log(`listening on http://127.0.0.1:${port}`);
});
注意两点:
- 对外返回的错误信息不要带堆栈。
- 文件日志里也不要写 token、cookie、密码、完整请求头。
5. 跑一次最小验证
启动:
npm run dev
另开终端:
curl -i http://127.0.0.1:3000/health
curl -i -X POST http://127.0.0.1:3000/notes \
-H 'content-type: application/json' \
-d '{"title":"first note","body":"hello sqlite"}'
curl -i http://127.0.0.1:3000/notes
看日志:
tail -n 20 data/logs/app.log
tail -n 20 data/logs/error.log
预期:
app.log有server_started、request_finished、note_created。error.log文件存在,即使还没有内容。/notes能返回刚插入的数据。
6. 故意打一个错误分支
发一个缺字段请求:
curl -i -X POST http://127.0.0.1:3000/notes \
-H 'content-type: application/json' \
-d '{"title":"missing body"}'
这是 400,不应该进 error.log。它是用户输入错误,不是服务异常。
再临时写一个测试路由:
app.get('/boom', (req, res, next) => {
next(new Error('forced test error'));
});
请求:
curl -i http://127.0.0.1:3000/boom
tail -n 5 data/logs/error.log
确认 error.log 里有 request_failed。验证完删掉 /boom,不要把测试炸点留在项目里。
7. 查 SQLite 现场
安装了 sqlite3 命令行的话,可以直接看数据:
sqlite3 data/app.sqlite '.tables'
sqlite3 data/app.sqlite 'select id,title,created_at from notes order by id desc limit 5;'
sqlite3 data/app.sqlite 'pragma integrity_check;'
pragma integrity_check; 正常输出:
ok
如果没有 sqlite3 命令,也可以写一个只读脚本查,但不要在排障时直接拿编辑器改数据库文件。
8. 常见坑
日志目录不存在
服务启动时就 mkdirSync 并预创建日志文件。不要等第一条错误出现才创建 error.log,否则你会分不清“没报错”和“日志系统没工作”。
请求日志太多
保留最基础字段:方法、路径、状态码、耗时。别把 body 全写进去,里面很容易混进隐私数据。
SQLite 被锁
先看是不是长事务或一次写太多数据。排查命令:
lsof data/app.sqlite
ls -lh data/app.sqlite data/app.sqlite-wal data/app.sqlite-shm
WAL 文件长期很大时,可以在低峰期做 checkpoint:
sqlite3 data/app.sqlite 'pragma wal_checkpoint(TRUNCATE);'
停写入或确认低峰再做,不要在线上高峰随手执行。
9. 本地完成标准
每次改接口前后都跑这几条:
npm start
curl -fsS http://127.0.0.1:3000/health
curl -fsS http://127.0.0.1:3000/notes
sqlite3 data/app.sqlite 'pragma integrity_check;'
tail -n 20 data/logs/app.log
tail -n 20 data/logs/error.log
确认健康检查、数据查询、数据库检查和文件日志都能跑,再写业务功能。
还没有评论,欢迎先发第一条。