1.开篇
日志包含什么访问人数啊、峰值啊、bug 啊什么的,如果没有日志那么很容易失控。 访问日志可以参考我们 http-server,每次访问都会有这些东西。自定义日志是则并不是每次访问都打印的,根据情况或自己的需要,有点像 console.log。
日志:
- 系统没有日志,就等于人没有眼睛——抓瞎
- 第一,访问日志 access log ( server端最重要的日志)
- 第二,自定义日志(包括自定义事件、错误记录等)
开发的话自然是打印在控制台,而上线了当然是要把日志保存到文件中。 如果不用 stream 的话,直接操作文件会很大的消耗 cpu 和内存。
目录
- nodejs文件操作,nodejs stream
- 日志功能开发和使用
- 日志文件拆分,日志内容分析
日志文件很大自然不可能放到 redis(占超级多内存而且也是异步的,没必要马上记日志)mysql 的话需要表结构(B树)才比较合适(而且文件每个地方都可以访问,不必要安 装 mysql 文件)。
nodejs文件操作
- 日志要存储到文件中
- 为何不存储到mysql中?
- 为何不存储到redis中?
2.nodejs文件操作
新建一个文件夹 file-test ,直接建一个 test1.js ,引入两个 node.js 自带的 fs 和 path 库(因为 Linux 和 windows 的文件目录路径是不一样的,需要这个 path 来统一)。 再建一个 data.text ,随便写点什么作为我们测试的文件。 回到 test1 ,利用 resolve 方法, __dirname 代表当前文件的目录获取到 data 我们就去读取文件, readFile 方法,异步的 (注意获取的是数据其实是二进制,记得转换成字符串)。
但是按照目前这种写法,虽然可以打印出 data ,倘若这个 data 有 5 个 G 这么大,我们还这样搞,那么势必是比较浪费性能和内存(一个进程最大也就 3G )的。
先看怎么写入文件,定义写入的文件,定义 option(flag 为 a 代表 append 追加写入,而 w 是直接覆盖)。然后就可以调用 writeFile(包含四个参数:要被写入的文件、写入的内容、 选项、回调函数)。同理,写入也是有耗费性能的情况(每次写入一行都 writeFile,一直 writeFile,就不停的打开文件,而且也很难写入非常大的数据)。再看看另一个,判断文件是否存在,调用 exists 方法,接收文件名和回调,会返回一个布尔值,注意也是异步。
const fs = require('fs' ) const path = require( 'path') const fileName = path.resolve(_dirname, 'data.txt') //拿到要操作的文件 //读取文件 fs.readFile(fileName,(err ,data) => { if (err) { console. log(err) return } console. log(data.tostring()) //注意拿到的是二进制文件,需要转换成字符串! }) //写入文件 const content ='三星阿卡丽\n' //随便写入点东西 const opt = flag:'a' } //定义写入的形式,a代表追加写入 fs .writeFile(fileName, content, opt, (err) => { if (err) { console.log(err) return } })
3.stream
IO 就是输入输出(input 和 output)。网络 IO 常见于视频播放几个 G,不可能直接下载到客户端再看,一来占用内存,二来带宽问题。IO 实在是慢,读写文件发送网络请求什么的。
IO操作的性能瓶颈:
- IO包括"网络IO”和"文件IO"
- 相比于CPU计算和内存读写, IO的突出特点就是:慢!
- 如何在有限的硬件资源下提高I0的操作效率?
借助下图来说,我们之前操作文件就是直接把整个桶给搬起来到到另一个桶,这就要你力气大(硬件资源超好,带宽超强)。但是在资源有限的情况下,那个管子插进去慢慢流就好,慢慢加载。
stream
实际上 request 和 response 都是继承了 stream 的一些特性或者说本身就是 stream。
4.stream演示
新建一个 stream-test 文件夹,新建一个 test1.js 。其实这个 pipe 就是 Linux 里面的标准输入出,输入啥打印啥,不用管,std 就是标准的意思, in 就是输入嘛, out 就是输出咯。就演示一下管道,输入就是流入嘛,输出就是流出嘛。接下来演示那个直接返回 request 数据,这里我们 post 的 request 内容就是 response 展示的内容, request 和 response 是桶,用管道连接,是流的关系。当然要是输入的很多,就 会一点点的流过去。 接下来是操作文件对象。
var fs = require('fs') var path = require( 'path') //两个文件名 var fileName1 = path.resolve(_dirname, data. txt') var fileName2 = path.resolve(_dirname, 'data- bak. txt') //读取文件的stream 对象 var readStrekm = fs. createReadStream( fi leName1) //写入文件的stream 对象 var writeStream = fs.createWr iteSt ream(f i LeName2 ) //执行拷贝,通过pipe readSt ream. pipe (writeSt ream ) //数据读取完成,即拷贝完成 readStream. on( 'end', function () { console. log('拷贝完成') })
创建写入文件和读取文件两个对象(桶)读取文件的话是从 stream 传给返回值,这些都是变成桶与桶然后流转过去,效率非常高。
//直接返Erequest数据 const http = require( http' ) const server = http .createserver((req, res) => { if (req.method === 'POST') { req.pipe(res) //从我们发送的request波到传回来的response,显示在返回结果那里 } }) server.listen(8008)
创建两个随便的数据文件 data.txt 和 data-bak.txt ,导入这两 个文件,分别创建读与写两个 stream 对象(桶)。这个同样可以监听 data 和 end(这样子就知道确实是一点点读取文件的,而传统的操作是一下子都给拿出来)
//复制文件 const fs = require('fs' ) const path = require('path') const fileName1 = path.resolve(_ dirname, 'data.txt' ) const fileName2 = path. resolve(_ dirname, 'data-bak.txt') const readstream = fs .createReadstream(fileName1) const writestream = fs.createwritestream(fileName2) //拿到对应的文件且创建读写对象 readstream.pipe(writestream) //将读到的文件流入写入的文件(也就是拷贝了) readstream.on('data', chunk => { console. log(chunk.tostring()) }) readstream.on('end', () => { //监听拷贝完成 console.log('copy done ' ) })
// http请求 文件 const fs = require('fs' ) const path = require('path') const http = require('http') const fileName1 = path.resolve(_dirname,'data.txt') const readstream = fs .createReadstream(fileName1 ) const server = http.createserver((req, res) => { if (req.method === 'GET') { readstream.pipe(res) //直接将读取到的文件一点点传到返回值那里 } }) server.listen(8000 )
5.写日志
在 blog-1 下建一个 logs 文件夹,下面建三个文件:access.log、 event.log、 error.log 。 在 src 下建一个 utils 文件夹,里面建一个 log.js 。定义一个 createWriteStream 函数,就是生成右边的桶被写入),由 于只是传入 xxx.log ,所以还需要再拼接一下路径,在当前目 录(utils)上翻一层再上翻一层(blog-1),找到下面的 logs 文件夹,再找到传入的文件,就能拿到真正的地址, 根据这个地址去创建流对象(追加方式)然后返回整个对象即可。
将 access.log 传入建一个对应的流对象,然后再定义一个要传给外面的函数 access (传入 log 参数)用来写访问日志, 写日志我们统一定义成一个函数 writeLog ,接收流对象和 log,直接调对象的 write 方法,传入需要写的内容即可记得换行),在 access 调用这个方法即可,其它的日我
们就不管了,也是一样的道理。 那 么 怎 么 去 用 呢 ? 回 到 aoo.js 获 取 access 方 法 , 在serverHandle 使用,把 method 、 url 、 user-agent 、当前时间 戳传入。由于是通过流写入的,效率很高,每次访问都可以写入东西。
6.拆分日志
- 日志内容会慢慢积累,放在一个文件中不好处理
- 按时间划分日志文件,如2019-02-10.access.log
- 实现方式: linux的crontab命令,即定时任务
这个由于服务器基本都是 Linux 和类 Linux 的,定时任务的话 windows 可能没办法实现,所以这一块可以了解下即可,主 要是运维的搞。 解释一下*代表什么意思,第一个*代表分钟(如果不写具体 的值保持*就代表忽略,比如 1 * * * *就代表每天的第一分钟执行这个命令)第二个*代表小时(12***代表每天的第二个小时的第 1 分钟执行)第三个是日期第四个是月份第五个是 星期。command 就是一个 shell 脚本。
crontab:
- 设置定时任务,格式: **** * command
- 将access.log拷贝并重命名为2019-02-10.access.log
- 清空access.log文件,继续积累日志
这个完全不需要修改代码,所以肯定是运维来搞,不过了解也是可以的。Node.js 当然也能做这个,但是就比操作系统隔了一层,不如直接通过操作系统的 shell 脚本来操作更加便捷 和高效(也好分离代码)。 回到 utils ,建一个 copy.sh (即 shell 脚本),第一行是固定的即 shell 的执行文件,将 logs 的路径拷贝过去, cd 到 logs , 拷贝 access.log 重命名为当前时间的 access.log 。 echo 移动空字符串到 access.log 相当于清空 access.log 。然后在 Linux 下面执行就行了!
7.分析日志介绍
接下来就是使用 crontab 了,因为前面已经编写好了 shell 脚本,通过输入 crontab -e 进入编辑器,设置时间,执行脚本的时候需要将整个脚本的路径拷贝过去才行。
日志分析:
- 如针对access.log日志,分析chrome的占比
- 日志是按行存储的, - -行就是一条日志
- 使用nodejs的readline ( 基于stream , 效率高)
通过 readline 可以一行行的去查看日志,我们先去不同浏览器运行下不同的地址,创造出不一样的 logs。
8.readline
获取到不同浏览器的日志后,我们回到 utils 下面建一个readline.js,引入 fs 、 path 、 readline 。拿到 access.log ,并且 基于这个建一个 readStream (因为是读操作)。再调用 createInterface 创一个 readline 对象,输入就是流对象。 定义一个储存 chrome 数量的变量和储存总数的变量, on 监 听 line ,每读完一行就会触发。通过 -- 切割数据,第 3 个即数组[2] 就是有关浏览器信息的,看看否包含 Chrome 决定加减。
const fs = require( ' fs' ) const path = require( 'path' ) const readline = require('readline') //创建流对象(读取) const fileName = path. join(__ dirname, '../', ' ../', 'logs' , access.log' ) const readstream = fs.createReadstream( fileName) //创建readLine对象 const rl = readline.Interface({ input: readstream }) //定义存放浏览器类型的变量 let chromeNum = 0 letsum=e