日志分析查看 grep、sed、sort、awk 的运用
我们日常应用中都离不开日志。可以说日志是我们在排查问题的一个重要依据。但是日志并不是写了就好了,当你想查看日志的时候,你会发现线上日志堆积的长度已经超越了你一行行浏览的耐性的极限了。
于是,很有必要通过一些手段来高效地辅助你来快速的从日志中找到你要找的问题。本文通过一个从项目中衍生出来的例子从查找日志,筛选日志和统计日志 3 个方面层层递进来简述日志文件查看中一些有用的手段。(注:在 linux 环境下)
例子背景
后台跑一个定时任务,对指定时间段的订单数据表中的每一条记录进行以此任务处理。在日志中输出:
- 订单 id
- 订单处理状态
- 日志类别
准备工具:sort、tail、less、uniqu、grep、sed、awk
示例日志: demo.log
[plain] view plaincopy
2011-08-23 19:57:00,610 [] INFO bo.CommodityCerOrderBO - =====>属性订正任务执行开始|每页读取 100 条数据
2011-08-23 19:57:05,012 [] INFO bo.CommodityCerOrderBO - 当前正在处理页数:1
2011-08-23 19:57:30,688 [] INFO bo.CommodityCerOrderBO - order-fix.curr_id:10117,status:attr_ids 不含 0 跳过
2011-08-23 19:57:30,709 [] ERROR bo.CommodityCerOrderBO - order-fix.curr_id:10117,status:添加属性 id,但由于认证分类参数有误默认取匹配属性名称的第一个属性 id:100104
2011-08-23 19:57:31,721 [] ERROR bo.CommodityCerOrderBO - order-fix.curr_id:10117,status:添加属性 id,但由于认证分类参数有误默认取匹配属性名称的第一个属性 id:100105
2011-08-23 19:57:32,727 [] ERROR bo.CommodityCerOrderBO - order-fix.curr_id:10117,status:添加属性 id,但由于认证分类参数有误默认取匹配属性名称的第一个属性 id:100107
2011-08-23 19:57:32,782 [] INFO bo.CommodityCerOrderBO - order-fix.curr_id:10117,status:attr_ids 成功保存为 0|100104|0|100105|100107
2011-08-23 19:57:32,782 [] INFO bo.CommodityCerOrderBO - order-fix.curr_id:10226,status:attr_ids 不含 0 跳过
2011-08-23 19:57:32,805 [] ERROR bo.CommodityCerOrderBO - order-fix.curr_id:10226,status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:100104
2011-08-23 19:57:33,828 [] ERROR bo.CommodityCerOrderBO - order-fix.curr_id:10226,status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:100107
2011-08-23 19:57:33,838 [] ERROR bo.CommodityCerOrderBO - order-fix.curr_id:10226,status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:46
2011-08-23 19:57:34,850 [] ERROR bo.CommodityCerOrderBO - order-fix.curr_id:10226,status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:100106
2011-08-23 19:57:35,860 [] ERROR bo.CommodityCerOrderBO - order-fix.curr_id:10226,status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:100105
2011-08-23 19:57:36,871 [] ERROR bo.CommodityCerOrderBO - order-fix.curr_id:10226,status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:3
2011-08-23 19:57:36,884 [] ERROR bo.CommodityCerOrderBO - order-fix.curr_id:10226,status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:3
2011-08-23 19:57:36,891 [] INFO bo.CommodityCerOrderBO - order-fix.curr_id:10226,status:attr_ids 成功保存为 6|100104|0|0|100107|46|100106|100105|3|3
2011-08-23 19:57:36,891 [] INFO bo.CommodityCerOrderBO - order-fix.curr_id:10222,status:attr_ids 不含 0 跳过
2011-08-23 19:57:36,928 [] ERROR bo.CommodityCerOrderBO - order-fix.curr_id:10222,status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:3
2011-08-23 19:57:36,942 [] ERROR bo.CommodityCerOrderBO - order-fix.curr_id:10222,status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:100104
2011-08-23 19:57:36,955 [] ERROR bo.CommodityCerOrderBO - order-fix.curr_id:10222,status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:100105
2011-08-23 19:57:36,969 [] ERROR bo.CommodityCerOrderBO - order-fix.curr_id:10222,status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:100107
2011-08-23 19:57:36,980 [] ERROR bo.CommodityCerOrderBO - order-fix.curr_id:10222,status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:46
2011-08-23 19:57:36,992 [] ERROR bo.CommodityCerOrderBO - order-fix.curr_id:10222,status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:100106
2011-08-23 19:57:37,011 [] ERROR bo.CommodityCerOrderBO - order-fix.curr_id:10222,status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:3
0. 一些最基础的日志查看命令
最简单的日志查看命令就是浏览日志文件了,一般会从有限浏览文件末尾的
[plain] view plaincopy
tail -400f demo.log #监控最后 400 行日志文件的变化 等价与 tail -n 400 -f (-f 参数是实时)
less demo.log #查看日志文件,支持上下滚屏,查找功能
uniq -c demo.log #标记该行重复的数量,不重复值为 1
以上命令具体使用详见本机 man 手册
1. 查找关键日志记录 grep
浏览了日志文件后你会发现,日志文件成千上万行,怎么能找到我要找的内容呢。这时候,就可已用 grep 来进行日志的关键行提取了。
grep 简单使用
规则: grep [选项]...模式 [文件]...
(模式是正则表达式)
例子 1:
[plain] view plaincopy
grep 'INFO' demo.log #在文件 demo.log 中查找所有包行 INFO 的行
输出:
2011-08-23 19:57:00,610 [] INFO bo.CommodityCerOrderBO - =====>属性订正任务执行开始|每页读取 100 条数据
2011-08-23 19:57:05,012 [] INFO bo.CommodityCerOrderBO - 当前正在处理页数:1
2011-08-23 19:57:30,688 [] INFO bo.CommodityCerOrderBO - order-fix.curr_id:10117,status:attr_ids 不含 0 跳过
...(略)
例子 2:
[plain] view plaincopy
grep -o 'order-fix.curr_id:[0−9]\+
' demo.log #-o 选项只提取 order-fix.curr_id:xxx 的内容(而不是一整行),并输出到屏幕上
输出:
order-fix.curr_id:10117
order-fix.curr_id:10117
order-fix.curr_id:10117
order-fix.curr_id:10117
order-fix.curr_id:10117
order-fix.curr_id:10226
...(略)
例子 3:
[plain] view plaincopy
grep -c 'ERROR' demo.log #输出文件 demo.log 中查找所有包行 ERROR 的行的数量
输出: 17
例子 4:
[plain] view plaincopy
grep -v 'ERROR' demo.log # 查找不含 ERROR 的行
输出:(功能和 grep 'INFO' demo.log
命令一样,输出略)
2. 精简日志内容 sed
从 n 多行的日志文件中提取到一定数量的行后,可能你还会觉得有些功能不够,比如你每行并不需要有哪个类抛出的描述,比如你不需要日志时间,或者要把时间格式换个形式展示等等,这时候你就可以通过 sed 的替换命令来进行对日志文件提取具体内容了。
如果把 grep 比作过滤器,那 sed 就是个修改器了。
sed 简单用法:
[plain] view plaincopy
sed [-n][-e] '命令' 文件 #-n 选项是默认不输出信息,除非使用了 p 命令或者是 s 命令的 p 标志符;-e 是表明空格后面接的是一个命令
sed [-n] -f 脚本 文件 #这个用法是把命令写在脚本里
»'命令'的格式: [地址 1[,地址 2]][!] 指令 [参数]
» 地址的格式:用行号标识(1 表明匹配第一行),或者用正则表达式匹配('^INFO'表明该地址匹配以 INFO 打头的行)
» 指令的例子:p 打印指令,s 替换指令,d 删除指令等等(以下表格摘自 abs 的 sed 小册子):
操作符 名字 效果
[地址范围]/p 打印 打印[指定的地址范围]
[地址范围]/d 删除 删除[指定的地址范围]
s/pattern1/pattern2/ 替换 将指定行中, 将第一个匹配到的 pattern1, 替换为 pattern2.
[地址范围]/s/pattern1/pattern2/ 替换 在地址范围指定的每一行中, 将第一个匹配到的 pattern1, 替换为 pattern2.
[地址范围]/y/pattern1/pattern2/ transform 在地址范围指定的每一行中, 将 pattern1 中的每个匹配到 pattern2 的字符都使用 pattern2 的相应字符作替换. (等价于 tr 命令)
g 全局 在每个匹配的输入行中, 将每个模式匹配都作相应的操作. (译者注: 不只局限于第一个匹配)
小结:sed 就是遍历对于输入文件的每一行,如果该行匹配地址 1,地址 2 的范围之内,那么就对这一行执行命令。
例 1:(摘自 abs 的 sed 小册子)
8d 删除输入的第 8 行.
/^$/d 删除所有空行.
1,/^$/d 从输入的开头一直删除到第 1 个空行(第一个空行也删除掉).
/Jones/p 只打印那些包含"Jones"的行(使用-n 选项).
s/Windows/Linux/ 在每个输入行中, 将第一个出现的"Windows"实例替换为"Linux".
s/BSOD/stability/g 在每个输入行中, 将所有"BSOD"都替换为"stability".
s/ *$// 删除掉每行结尾的所有空格.
s/00*/0/g 将所有连续出现的 0 都压缩成单个的 0.
/GUI/d 删除掉所有包含"GUI"的行.
s/GUI//g 将所有"GUI"都删除掉, 并保持剩余部分的完整性.
看完基本用法,让我们结合 demo.log 来具体应用下:
例 2:输出 demo.log 中的某个日期中的 ERROR 的行
来具体应用下:
[plain] view plaincopy
sed -n '/^2011-08-23.*ERROR/p' demolog.log
输出:
2011-08-23 19:57:30,709 [] ERROR bo.CommodityCerOrderBO - order-fix.curr_id:10117,status:添加属性 id,但由于认证分类参数有误默认取匹配属性名称的第一个属性 id:100104
2011-08-23 19:57:31,721 [] ERROR bo.CommodityCerOrderBO - order-fix.curr_id:10117,status:添加属性 id,但由于认证分类参数有误默认取匹配属性名称的第一个属性 id:100105
2011-08-23 19:57:32,727 [] ERROR bo.CommodityCerOrderBO - order-fix.curr_id:10117,status:添加属性 id,但由于认证分类参数有误默认取匹配属性名称的第一个属性 id:100107
例 3:提取 demo.log
中的日期,日志级别,订单 id 和状态。
[plain] view plaincopy
sed -f demo.sed2 demo.log
[plain] view plaincopy
#n #这一行用法和命令中的-n 一样意思,就是默认不输出
#demo.sed2
#下面的一行是替换指令,就是把 19 位长的日期和 INFO/ERROR,id,和后面的一截提取出来,然后用 @分割符把这 4 个字段重新按顺序组合
s/^[−0−9]{19}
.*INFO∥ERROR .*order-fix.curr_id:[0−9]\+,.∗$
/\1@\3@\2@\4/p
输出:
2011-08-23 19:57:30@10117@INFO@status:attr_ids 不含 0 跳过
2011-08-23 19:57:30@10117@ERROR@status:添加属性 id,但由于认证分类参数有误默认取匹配属性名称的第一个属性 id:100104
2011-08-23 19:57:31@10117@ERROR@status:添加属性 id,但由于认证分类参数有误默认取匹配属性名称的第一个属性 id:100105
2011-08-23 19:57:32@10117@ERROR@status:添加属性 id,但由于认证分类参数有误默认取匹配属性名称的第一个属性 id:100107
2011-08-23 19:57:32@10117@INFO@status:attr_ids 成功保存为 0|100104|0|100105|100107
...略
sed 详细用法可以参考《sed 与 awk》(第二版), 或者 man 之
或者点击下面这个参考链接 http://www.reddragonfly.org/abscn/x17814.html
3. 对记录进行排序 sort
经过了日志文件的精炼后,我们可能不想对日志进行时间排序,这时候我们就可以用 sort 进行排序。
基本使用
sort [options] [file...]
对于 demo.log,经过了上面的 sed 提取后,我希望先用 id 进行排序,然后再用日志级别倒序进行排序,最后才是日期排序
[plain] view plaincopy
#排序功能 -t 表示用 @作为分割符,-k 表示用分割出来的第几个域排序(不要漏掉后面的,2/,3/,1,详细意思看下面的参考链接,这里不做详述)
sed -f test.sed demolog.log | sort -t@ -k2,2n -k3,3r -k1,1 #n 为按数字排序,r 为倒序
输出:
2011-08-23 19:57:30@10117@INFO@status:attr_ids 不含 0 跳过
2011-08-23 19:57:32@10117@INFO@status:attr_ids 成功保存为 0|100104|0|100105|100107
2011-08-23 19:57:30@10117@ERROR@status:添加属性 id,但由于认证分类参数有误默认取匹配属性名称的第一个属性 id:100104
2011-08-23 19:57:31@10117@ERROR@status:添加属性 id,但由于认证分类参数有误默认取匹配属性名称的第一个属性 id:100105
2011-08-23 19:57:32@10117@ERROR@status:添加属性 id,但由于认证分类参数有误默认取匹配属性名称的第一个属性 id:100107
2011-08-23 19:57:36@10222@INFO@status:attr_ids 不含 0 跳过
2011-08-23 19:57:36@10222@ERROR@status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:100104
2011-08-23 19:57:36@10222@ERROR@status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:100105
2011-08-23 19:57:36@10222@ERROR@status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:100106
2011-08-23 19:57:36@10222@ERROR@status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:100107
2011-08-23 19:57:36@10222@ERROR@status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:3
2011-08-23 19:57:36@10222@ERROR@status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:46
2011-08-23 19:57:37@10222@ERROR@status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:3
2011-08-23 19:57:32@10226@INFO@status:attr_ids 不含 0 跳过
2011-08-23 19:57:36@10226@INFO@status:attr_ids 成功保存为 6|100104|0|0|100107|46|100106|100105|3|3
2011-08-23 19:57:32@10226@ERROR@status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:100104
2011-08-23 19:57:33@10226@ERROR@status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:100107
2011-08-23 19:57:33@10226@ERROR@status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:46
2011-08-23 19:57:34@10226@ERROR@status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:100106
2011-08-23 19:57:35@10226@ERROR@status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:100105
2011-08-23 19:57:36@10226@ERROR@status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:3
2011-08-23 19:57:36@10226@ERROR@status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:3
详尽手册 http://ss64.com/bash/sort.html
4. 统计日志相关记录数 awk
现在日志已经比较清晰了,但是如果我想对不同日志进行统计怎么办,比如我要统计所有 ERROR 的日志记录书,或者要统计每个订单有多少个 ERROR?这就需要我们的 awk 帮忙了。
awk 简单使用:
[plain] view plaincopy
awk [-v 变量名=变量值] [-Fre] [--] '模式 { 语句 }' 变量名=变量值 文件名
awk [-v 变量名=变量值] [-Fre] -f 脚本文件 [--] 变量名=变量值 文件名
和 sed 一样,awk 也支持 2 中方式调用,一种是把 awk 脚本直接在命令行写入,第二种是把 awk 写在文件中在命令行中调用。
awk 处理方式也与 sed 类似,对文件中的每一个输入行进行处理,每个处理首先判断是否是模式中匹配的行,是的话就具体执行相应的语句。
不同的是,awk 侧重与对每一行的列进行处理,并且,awk 脚本和 c 语言类似也拥有变量,条件判断,循环等复杂语句,所以这里只能简单介绍一下基本应用,详细的请查看后面给出的相关链接。
而且,awk 在处理所有行前和处理完行后各有 BEGIN 和 END 语句做预处理和后置处理。
例子 1:打印日志中的第 2,3 列
[plain] view plaincopy
awk 'BEGIN{FS="@"} {print $2,$3}' demo.log_after_sort #BEGIN 中预处理的是,把 @号作为行的列分割符,把分割后的行的第 2,3 列输出
输出:(对于从 sort 得出的结果作为输入)
10117 INFO
10117 INFO
10117 ERROR
10117 ERROR
10117 ERROR
10222 INFO
...略
例子 2. 统计日志中 INFO,ERROR 出现的总数,以及总记录数
[plain] view plaincopy
#下面的例子是作为命令行输入的,利用单引号作为换行标记,这样就不用另外把脚本写进文件调用了
awk '
BEGIN {
FS="@"
}
{
if ($3 == "INFO") {info_count++}
if ($3 == "ERROR") {error_count++}
}
END {
print "order total count:"NR #NR 是 awk 内置变量,是遍历的当前行号,到了 END 区域自然行号就等于总数了
printf("INFO count:%d ERROR count:%d\n",info_count,error_count)
} ' demo.log_after_sort
输出:
order total count:22
INFO count:5 ERROR count:17
例子 3. 对指定时间范围内的日志进行统计,包括输出 INFO,ERROR 总数,记录总数,每个订单记录分类统计
下面的例子综合了前面 sed 和 sort
[plain] view plaincopy
sed -f demo.sed demolog.log | sort -t@ -k2,2n -k3,3r -k1,1 | awk -f demo.awk
[plain] view plaincopy
#demo.awk
BEGIN {
FS="@"
stime="2011-08-23 19:57:31"
etime="2011-08-23 19:57:37"
}
$1 > stime && $1 < etime {
if ($3 == "INFO") {info_count++}
if ($3 == "ERROR") {error_count++}
++total
status[$2]=status[$2]"\t"$1"\t"$3"\t"$4"\n"
}
END {
for(i in status){
printf("id:%s:\n%s\n",i,status[i])
}
print "order total count:"total
printf("INFO count:%d ERROR count:%d\n",info_count,error_count)
} <span style="font-size:18px;"><strong>
</strong></span>
输出:
id:10117:
2011-08-23 19:57:32 INFO status:attr_ids 成功保存为 0|100104|0|100105|100107
2011-08-23 19:57:32 ERROR status:添加属性 id,但由于认证分类参数有误默认取匹配属性名称的第一个属性 id:100107
id:10226:
2011-08-23 19:57:32 INFO status:attr_ids 不含 0 跳过
2011-08-23 19:57:32 ERROR status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:100104
2011-08-23 19:57:33 ERROR status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:100107
2011-08-23 19:57:33 ERROR status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:46
2011-08-23 19:57:34 ERROR status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:100106
2011-08-23 19:57:35 ERROR status:添加属性 id,但由于没有属性在该分类下默认取匹配属性名称的第一个属性 id:100105
#这个例子只是举例说明 awk 的统计用法,实际运用中可能会统计超时的次数,页面访问次数等。
补充
其他实践时例子:
1.在本地分支把代码修改从一个分支复制到另一个分支(例子的 b1022st.txt 是一个记录了文件新增或修改的变化的文件路径名)
[plain] view plaincopy
awk 'BEGIN{FS="b1022-scm/"} {system("cp -rf b1022-scm/"$2" b1022-lpscm/"$2);}' /home/nizen/b1022st.txt
通过 awk 和其 system 命令结合,这样就把文件从 b1022-scm 复制到 b1022-lpscm 下
2.内置函数 http://www.cnblogs.com/chengmo/archive/2010/10/08/1845913.html
3.内建变量 http://www.linuxsong.org/2010/09/awk-built-in-variable/
4.shell 变量传递 http://www.51testing.com/?uid-225738-action-viewspace-itemid-246383
5.awk 统计某个开始-结束范围内的关键字累加总数:
[java] view plaincopy
BEGIN {
running=0
count=0
startRow="begin =====>" id #id,nextId 是通过-v 参数从外部传入
endRow="begin =====>" nextId
}
$0 ~ startRow{ # ~是匹配运算符,判断$0 是否满足 startRow 正则表达式
running = 1
# printf("start\n")
}
$0 ~ endRow {
running = 0
# printf("end\n")
}
{
if(running==1) { # 仅在 startRow 和 endRow 范围内统计
if($0 ~ "it show") {
# printf($0 "\n")
str=$0
sub(/^.*show times:/, "", str)
sub(/ .*$/, "", str)
printf(str "\n")
count = count + str
}
}
}
END {
printf("showTimeCount:"+count)
}
6. printf "10ms occur:%.2lf%%\n",t10/total*100
输出百分比数据,精确到百分位后 2 位
5. 日志规范化
从前面可以看出,日志文件为了要让后续工具能够对里面的内容进行提取和处理,就必须要让日志文件规范的输出。
个人想到有几个点可以规范:
- 记录日志时候可以写入一些特殊的文本语句,一遍与工具的检索和处理。
- 记录日志最好不要用中文,因为在不同语言环境下对日志的处理可能因为编码不同导致没法处理日志。
后面再贴下淘宝中找到的一些打印日志的建议:
- 正常情况下应该返回 true, 却返回 false 的, 反正就是你在对返回值进行检查的时候, 如果不正常, log 一下
- 出现异常的地方, 以前认为 hsf.log 会帮我们记下所有的异常, 但是这个也不一定可靠, 所以还得我们自己记一下
- 日志必须包含上下文信息,如果出于统计的需要, 可打可不打,在完成代码之后, 查看一下整个代码结构, 在一些关键的点, 加上日志, 正常的 info, 少数情况出现的 warning, 异常情况的 error 或者 warning
- 打印的日志内容要容易查询, 以前我比较倾向于打中文日志, 虽然易读, 但是中文在 linux 下的搜索统计稍微有些麻烦,所以如果能加上英文标识(比如说用于唯一标识的前缀), 能识别不同日志, 这个对定位也是非常有好处的.
6. 一些容易遇到的问题
a. 处理中文出现乱码
这个主要是因为你的 linux locale 的配置,与编辑文件的语言环境,还有你登录 ssh 客户端的编码规则有关,所以最好还是不用中文记录日志。
b. 正则表达式不同工具的区别
这个主要是因为不同工具的正则表达式定义的元字符不同,网上有总结的,可点击正则迷雾参考
后记
目前只是简单介绍了 grep、sed、sort、awk 的几个简单应用,实际上的日志监控回根据不同的情景进行不同的处理。比如需要对调用的耗时进行统计(平均时间或者超时记录),对访问量进行统计,但是基本原理都和本文例子出发点一致。
本文一方面是为了记录下学习过程中积累的东西,另一方面为了抛砖引玉引起大家对日志记录的关注。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论