在日常的项目开发中,很多场景都需要用到日志记录,它可以是用户的操作行为记录,也可以是每个接口的访问请求记录等。一个良好的日志记录系统可以帮助开发者或用户更好的处理突发状况,迅速定位问题,从而提高开发效率和保障系统的稳定性。本文将使用Node.js中间件技术,编写一个简单的全局中间件,用于记录访问者的各种信息。
1.中间件技术
Node中间件主要是指封装http请求细节处理的方法,其本质上就是在进入具体的业务处理之前,先让特定过滤器处理;对于Web应用而言,引入Node中间件可以简化和封装一些基础逻辑处理细节。本文所编写的中间件则是在请求进入处理之前,首先生成用户信息的访问日志,然后进行后面的逻辑处理。
2.获取请求信息
获取请求的方法一般是对请求头headers里面的内容进行解析,从中可以解析出请求的ip地址,浏览器名称及版本号,操作系统名称及版本号等等。
(1)获取真实ip
我们的 Web 服务,往往需要获取用户的真实 IP,比如防刷、API限流、账号归属地等等场景。
每一个 TCP 连接都有 remoteAddress 属性,通过它可以直接获取到请求的 IP 地址。而在 HTTP 请求中,我们可以通过 request.socket.remoteAddress 访问到这个属性
一般来说我们的应用服务都不会直接接收外部的请求,而会将服务部署在接入层之后,从而实现多台机器的负载均衡和服务的平滑发布,保证高可用。如阿里云 SLB 或 Nginx 反向代理。
此时,我们通过 remoteAddress 获取到的就是代理服务器的 IP 而不是用户的真实 IP。这是时候可以通过 X-Forwarded-For 请求头获取。所有的反向代理都实现一个统一的约定,在转发请求给下游服务之前,把请求代理的 IP 地址写入到 X-Forwarded-For 头中,形成了一个 IP 地址列X-Forwarded-For: client, proxy1, proxy2
// 获取 ip 地址 function getClientIP(req) { let ip = req.headers['x-forwarded-for'] || // 判断是否有反向代理 IP req.headers['x-real-ip'] || req.ip || req.connection.remoteAddress || // 判断 connection 的远程 IP req.socket.remoteAddress || // 判断后端的 socket 的 IP req.connection.socket.remoteAddress || '' // 如果有代理 第一个为真实地址 if (ip.split(',').length > 0) { ip = ip.split(',')[0] } // ::ffff:127.0.0.1 切割出后面的ip地址段 ip = ip.substr(ip.lastIndexOf(':') + 1, ip.length); return ip; }
(2)获取ip的地理信息
这里我们可以借助一些第三方接口实现将ip转换为具体的地理信息。下文以百度地图接口为例:
- 1.注册百度地图开放平台并认证开发者
- 2.在控制台中创建应用获得访问应用的AK值
- 3.在开发文档中找到定位API
参考文档:百度地图定位API
// 获取地理位置 const axios = require('axios'); async function getIPLocation(ip) { let ipurl = 'https://api.map.baidu.com/location/ip?coor=bd09ll&ak=这里换成自己的ak值&ip='; let res = await axios.get(ipurl + ip); return res.data.status ? '未知' : res.data.content.address; // ip位置 }
上述代码中的ak值需要换成自己的。如果使用其他平台API请参考对应文档进行编写,实现步骤基本一致,只要可以通过ip获取地理信息即可。
(3)获取浏览器和操作系统信息
浏览器和操作系统信息可以在ctx.headers[‘user-agent’]中获取,可以对其字符串进行解析实现。当然也有现成的第三方模块帮助我们更方便的获取到浏览器和操作系统信息。
ua-parser-js:用于从用户代理数据中检测浏览器、引擎、操作系统、CPU和设备类型/型号的模块。参考文档
const parser = require('ua-parser-js'); const ua = parser(ctx.headers['user-agent']); let browser = ua.browser.name + ua.browser.version; // 浏览器 let os = ua.os.name + ua.os.version; // 操作系统
(4)获取时间
使用moment模块获取格式化之后时间:
// 获取当前时间 function getTime() { return moment().format('YYYY-MM-DD HH:mm:ss'); }
3.编写中间件
4.注册成为全局中间件
在app.js中引入并注册成为全局的中间件:
const getLog = require('./config/log.js'); // 引入中间件 app.use(getLog); // 注册
5.配置nodemon
如果项目使用nodemon启动,在每次向log文件夹中写入日志时,nodemon也会检测到文件发生变化导致项目重启,但我们并不希望这样做。此时需要在package.json文件中添加关于nodemon的配置,表示要忽略对log文件夹内容变化的监听。
"nodemonConfig": { "ignore": ["log/*"] },
6.效果展示
配置好中间件后,当我们的Node服务器每次被访问时,都会生成一条json记录,每天的日志记录即为当天日期命名的json文件。