日志系统的建设要点

前言:主要事项

1、异常数据的发现及补充、日志分类优化
2、日志数据的文件保存、滚动存储
3、日志文件上传
4、日志回捞

一、日志收集

1、日志分类

1.1、日志目标

序号 目标(Target)
1 app
2 sdk 三方库
3 h5 网页

<a name="日志类型"></a>

1.2、日志类型

序号 类型 描述
1 api_app app中的网络请求
2 api_app_cache app中的网络请求的网络缓存请求
3 api_buriedPoint 埋点的网络请求
4 sdk_other sdk的各种事件(初始化等)
5 sdk_api sdk中的网络请求
6 dart 语法
7 widget 视图(布局像素越界等)
8 click_other 点击、
9 click_share 分享
10 native_route 路由/跳转
11 h5_route 与网页跳转有关
12 h5_js js交互
13 monitor_network 监控:网络类型变化
14 monitor_lifecycle 监控:生命周期变化
15 buriedPoint_other 埋点数据生成等
16 im IM
17 heartbeat 心跳
18 other 其他

2、日志等级

序号 目标(Level) 描述
1 Normal 正常信息(目前用于请求开始)
2 Success 成功信息(目前用于请求结束:成功)
3 Warning 警告信息(目前用于请求结束:报错)
4 Error 错误日志(目前用于请求结束:失败)
5 Dangerous 危险(处理白屏等) 一般会进行额外的埋点

<a name="日志整合归类"></a>

3、日志整合归类

序号 列表 标志 包含
1 全部 all 所有
2 警告 warning 所有的警告
3 错误 error 所有的错误
4 接口 api api_app、api_cache(不包括sdk_api、api_buriedPoint)
5 点击 click click_share、click_other、h5_js
6 路由 route navite_route、h5_route
7 网页 H5 h5_route、h5_js
8 sdk sdk sdk_api、sdk_other
9 code code dart、widget
10 埋点 buriedPoint api_buriedPoint、buriedPoint_other
11 监控 monitor monitor_lifecycle、monitor_network
12 其他 other other
13 api结果 api_result type=api_app & level != Normal
14 im im 本地缓存消息、历史消息、收到的消息等
15 心跳 heartbeat

<img src="日志系统/devtool_log_home_page.gif" alt="devtool_log_home_page" style="zoom:50%;" />

4、日志的补充收集及其策略

要收集的数据已在上述《1.2、日志类型》中说明。

4.1、页面跳转

详见上述 《3、日志整合归类》中的"路由"

4.2、截断的收集

序号 关键字 场景
1 break for 、 swtich
2 return

4.3、用户关键行为

序号 行为 用途的场景说明
1 用户的资源选择 判断选择的本地资源是否上传到错误的存储桶

二、日志数据的文件保存、滚动存储

1、数据的文件保存

写入xlog文件中。

2、数据的文件滚动存储

2.1、日志的过期清理

1、本地日志文件结构

-- Document
    -- log
        -- 2020-10-01
            -- xlog_2020-10-01_1.xlog
            -- xlog_2020-10-01_2.xlog
        -- 2020-10-18
            -- xlog_2020-10-18_1.xlog

2、日志保留清理的配置信息(摘自/同 下文的日志接口中的《日志文件配置信息》)

序号 说明 字段 示例
接口回值1 最多保留几个天的日志目录 maxDayDirCount 7
接口回值2 最多每天保留几个文件 maxDayFileCount 10
接口回值3 每个日志文件超过多少后创建新文件 perFileMaxMB 5

3、清理方案

清理时机:在切换前后台的时候,

  • 日志配置信息的更新
  • 根据最新的日志配置信息,延迟10s后(避免影响启动),进行清理判断及清理
    • 遍历总日志目录 Document/log 下的所有日志文件夹
    • 如果天日志文件夹个数超过保留个数(避免是用保留时间,导致保留时间内只有一个日志,也被删掉),则按顺序删除早期日志
      • 遍历天日志目录 Document/log/yyyy-MM-dd 下的所有日志文件夹
      • 如果天日志文件夹个数超过保留个数(避免是用保留时间,导致保留时间内只有一个日志,也被删掉),则按顺序删除早期日志

2.2、日志文件的滚动写入

要增加的日志记录,如果添加后会超过日志文件的大小,则使用新文件写入。

细节点
1、日志文件的大小判断,不要每次都去读取文件,而是记录住大小变化
2、日志不要有一条写一条,而是使用写入缓冲区+定时器,5s尝试写入一次

3、数据的加密、解密

1、加密:

写入日志字符串时候,对编码后的字节数据,额外增加一个三位随机值。

  /// 使用 GZip 压缩算法将字符串进行压缩,并返回压缩后的字节数据(List<int> 类型)。
  List<int> _gzipCompressString(String inputString) {
    final codec = GZipCodec();
    
    // 将输入字符串 inputString 编码为 UTF-8 格式的字节数据
    final encoded = utf8.encode(inputString);
    
    // 对编码后的字节数据进行压缩,并返回压缩后的字节数据。
    final compressedData = codec.encode(encoded);
    
    return compressedData;
  }

  /// 加密日志字符串
  String _encryptLogString(String logString) {
    final compressedData = _gzipCompressString(logString); // 压缩字符串,并返回压缩后的字节数据(List<int> 类型)
    final randomNumber = Random().nextInt(800) + 100;
    final confound = compressedData.toString().substring(0, compressedData.toString().length - 1) + ", $randomNumber]"; // 在压缩后的字符串尾部加上一个随机字符串

    return confound;
  }

2、解密:

使用python脚本对log文件进行解密

import sys
import os
import gzip

# 读取.clog文件并解析数据
def parse_clog_file(input_file):
    parsed_data = []

    with open(input_file, 'r') as clog_file:
        current_data = ""
        for line in clog_file:
            if current_data:
                parsed_data.append(current_data)
            binary_data = line[:-6] + ']' # 去除加密时候额外添加的三位随机数
            byte_data = bytes(eval(binary_data))
            decompressed_data = gzip.decompress(byte_data)
            # current_data = byte_data.decode('utf-8')
            print(decompressed_data)
            current_data = decompressed_data.decode('utf-8')

        # 添加最后一个数据条目
        if current_data:
            parsed_data.append(current_data)

    return parsed_data

# 将解析后的数据写入与输入文件同名的.log文件
def write_to_log_file(parsed_data, input_file):
    base_name = os.path.splitext(input_file)[0]
    output_file = base_name + ".log"

    with open(output_file, 'w') as log_file:
        for data in parsed_data:
            log_file.write(data + "\n")
            log_file.write("\n")
            log_file.write("=================================================================================================" + "\n")
            log_file.write("\n")

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: python script.py <clogFileName>")
        sys.exit(1)

    input_clog_file = sys.argv[1]

    # 解析.clog文件
    parsed_data = parse_clog_file(input_clog_file)

    # 将解析后的数据写入与输入文件同名的.log文件
    write_to_log_file(parsed_data, input_clog_file)

    print(f"Log data parsed from {input_clog_file} and written to {input_clog_file}.log.")

三、日志文件上传

1、日志文件命名及目录规范

以用户10000012020-01时候上传声音文件 beautiful_scenery.aac为例,其路径完整示例如下:

https://media.xxx.com/app1_test1/audio/1/1000001/2020-01/beautiful_scenery.aac

要上传的存储桶:

序号 说明 参数 约定值示例
1 存储桶region
regionGetFunction
网络环境 ap-shanghai
2 存储桶bucket
bucketGetFunction
网络环境 xxx-pro-image-1302324914
3 上传成功后,桶的映射路径值
cosFileUrlPrefixGetFunction
1、图片桶:https://images.xxx.com/
2、媒体桶:https://media.xxx.com/
3、日志桶:https://static.xxx.com/
4 要上传到桶的哪个相对路径下
cosFileRelativePathGetFunction
上传成功后完整路径=以上桶值+此相对路径
见下文 见下文
eg:app1_test1/audio/1/1000001/2020-01/beautiful_scenery.aac

《要上传到桶的哪个相对路径下 cosFileRelativePathGetFunction 》的文件路径分批说明:

序号 层次说明 层次值算法 层次值描述
1.1 存储桶里相对路径的前缀 app标志_环境标志
eg:app1_pro、app1_test1、app1_dev1
文件区分
1.2 是否在存储桶里进行进一步路径区分(可选) 根据场景决定是否对桶进行进一步分类。
if ( mediaType == xxx ) {
if(mediaScene == yyy) {

}
}

1、都是图片,但自拍图片要求独立出来
2、都是多媒体,但音视频要区分开
2.1 用户1级 int.parse(uid) % 1000; // 取余数 用户名取余,有效减少同层上文件夹个数
2.2 用户2级 uid 用户名
3 年月 DateTime.now().toString().substring(0, 7); 年-月
4 文件名 {fileOriginName}_<br>{DateTime.now().microsecondsSinceEpoch}
.$fileExtensionType
文件名

附1:MediaType 媒体类型

序号 UploadMediaType 媒体类型 描述
1 unkonw 未知(默认值)
2 image 图片
3 audio 音频
4 video 视频
5 xlog 日志文件

附2:MediaScene场景 说明

序号 UploadMediaScene场景 描述
1 unkonw 未知(默认值)
2 selfie 自拍(安全等级较多)
3 im 会话聊天
4 live 直播

四、日志接口

1、日志记录接口(区别于回捞接口,需要在回捞前就有日志信息)

1、是否记录日志的开关及其配置信息

序号 说明 字段 示例
网页配置1 是否开启日志记录 isLogOn 1:开启、其他:关
网页配置2 要开启日志的用户(灰度上线)
(当且仅当开启上述日志记录时有效)
📢:未配置但开关为开时为全记录
logUserids ["101", "102"]
网页配置3 要开启日志的设备(灰度上线)
(当且仅当开启上述日志记录时有效)
📢:未配置但开关为开时为全记录
logDeviceIds ["udid001", "udid002"]
接口回值1 是否进行日志记录(灰度上线) needLog 1:记录、其他:不记录

2、不需要记录什么日志信息

序号 说明 字段 示例
接口回值1 不需要记录的日志类型 ignoreLogLevel ["Normal", "Success"]
接口回值2 不需要记录的日志等级 ignoreLogType ["api_buriedPoint", "buriedPoint_other"]

3、日志文件配置信息

序号 说明 字段 示例
接口回值1 最多保留几个天的日志目录 maxDayDirCount 7
接口回值2 最多每天保留几个文件 maxDayFileCount 10
接口回值3 每个日志文件超过多少后创建新文件 perFileMaxMB 5

4、日志文件问题回滚

序号 说明 字段 示例
网页配置1 强制删除什么时间前的日志 removeLogBeforeDate 2020-12-31
网页配置2 强制删除哪个用户的日志 removeUserids ["101", "102"]
网页配置2 强制删除哪个设备的日志 logDeviceIds ["udid001", "udid002"]
接口回值1 强制删除什么时间前的日志(不删除时空) removeLogBeforeDate 2020-12-31 或 空值

接口名定义:

function getLoggerConfig(userid, deviceId)

接口结果的json示例如下:

{
    "logConfig": {
        "needLog": 1,
        "ignoreLogLevel": ["Normal", "Success"],
        "ignoreLogType": ["api_buriedPoint", "buriedPoint_other"],
        "maxDayDirCount": 7,
        "maxDayFileCount": 10,
        "perFileMaxMB": 5
    },
    "logRevert": {
        "removeLogBeforeDate": "2020-12-31"
    }
}

2、日志回捞及问题回滚接口

1、日志回捞

序号 说明 字段 示例
网页配置1 要回捞日志的用户 salvageUserids ["101", "102"]
网页配置2 要回捞日志的设备 salvageDeviceIds ["udid001", "udid002"]
网页配置3 日志回捞的开始时间 salvageStartDate 2020-01-01
网页配置4 日志回捞的结束时间 salvageEndDate 2020-01-07
接口回值1 此用户此设备日志回捞的开始时间 salvageStartDate 2020-01-01
接口回值1 此用户此设备日志回捞的结束时间 salvageEndDate 2020-01-07

2、日志文件问题回滚

序号 说明 字段 示例
网页配置1 强制删除什么时间前的日志 removeLogBeforeDate 2020-12-31
网页配置2 强制删除哪个用户的日志 removeUserids ["101", "102"]
网页配置2 强制删除哪个设备的日志 logDeviceIds ["udid001", "udid002"]
接口回值1 强制删除什么时间前的日志(不删除时空) removeLogBeforeDate 2020-12-31 或 空值

接口名定义:

function getLoggerSalvageAndRevert(salvageUserids, salvageDeviceIds)

接口结果的json示例如下:

{
  "salvageId": "1001", 
    "salvageStartDate": "2020-01-01",
  "salvageEndDate": "2020-01-07",
    "removeLogBeforeDate": "2020-12-31"
}

3、日志文件上传到cos后同步给后台

用途:日志文件上传到cos后同步给后台,后台数据库记录每个用户,每个设备都捞到了什么数据。

接口名定义:

function addLoggerFile(userid, deviceId)

接口结果的json示例如下:

{

}

四、日志回捞

1、后台通过接口返回要回捞的信息。

2、前端用户在下次使用,收到后台信息时候,进行本地日志的上传。在腾云存储桶能收到对应日志,即代表成功。

五、方案实施

灰度方案:请参照 灰度系统 。(附:日志记录开关里目前已有灰度策略。)

目的:避免功能异常,出现集体性问题。

End

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容