2023-01-16 經過 TypeScript 整理重寫后,正式將監控系統的腳本開源,命名為?[shin-monitor](https://github.com/pwstrick/shin-monitor)。
## 一、存儲
  在將數據傳送到后臺之前,已經做了一輪清洗工作,如果有需要還可以再做一次清洗。
  日志表如下所示,自增的 id 直接偷懶使用了 bigint,沒有采用分表等其他技術。
~~~
CREATE TABLE `web_monitor` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`project` varchar(45) COLLATE utf8mb4_bin NOT NULL COMMENT '項目名稱',
`project_subdir` varchar(45) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '項目的子目錄,game項目下會有很多活動,放置在各個子目錄中',
`digit` int(11) NOT NULL DEFAULT '1' COMMENT '出現次數',
`message` text COLLATE utf8mb4_bin NOT NULL COMMENT '聚合信息',
`ua` varchar(600) COLLATE utf8mb4_bin NOT NULL COMMENT '代理信息',
`key` varchar(45) COLLATE utf8mb4_bin NOT NULL COMMENT '去重用的標記',
`category` varchar(45) COLLATE utf8mb4_bin NOT NULL COMMENT '日志類型',
`source` varchar(45) COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'SourceMap映射文件的地址',
`ctime` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`identity` varchar(30) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '身份,用于連貫日志上下文',
`day` int(11) DEFAULT NULL COMMENT '格式化的天(冗余字段),用于排序,20210322',
`hour` tinyint(2) DEFAULT NULL COMMENT '格式化的小時(冗余字段),用于分組,11',
`minute` tinyint(2) DEFAULT NULL COMMENT '格式化的分鐘(冗余字段),用于分組,20',
`message_code` int(11) DEFAULT NULL COMMENT '提取接口響應中的code值',
`message_status` int(11) DEFAULT NULL COMMENT 'message中的通信狀態碼',
`message_path` varchar(45) COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'message通信中的 path',
`message_type` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'message中的類別字段',
`referer` varchar(300) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '來源地址',
`os_name` varchar(45) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '操作系統名稱',
`os_version` varchar(45) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '操作系統版本',
`app_version` varchar(45) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '客戶端版本',
`ip` varchar(200) COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'IP地址',
`country` varchar(45) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '國家',
`province` varchar(45) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '省份',
`city` varchar(45) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '城市',
`isp` varchar(45) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '網絡運營商',
`author` varchar(45) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '頁面維護人員',
`fingerprint` varchar(45) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '瀏覽器指紋,用于計算 UV',
PRIMARY KEY (`id`),
KEY `idx_key_category_project_identity` (`key`,`category`,`project`,`identity`),
KEY `idx_category_project_identity` (`category`,`project`,`identity`),
KEY `index_ctime` (`ctime`),
KEY `idx_category_ctime` (`category`,`ctime`),
KEY `idx_category_project_ctime` (`category`,`project`,`ctime`),
KEY `idx_messagepath` (`message_path`),
KEY `idx_category_messagetype_ctime` (`category`,`message_type`,`ctime`),
KEY `idx_messagestatus_day_project_messagepath` (`message_status`,`day`,`project`,`message_path`),
KEY `idx_identity` (`identity`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='前端監控日志'
~~~
  2023-05-08 在表中增加 referer 字段。
  2023-05-16 在表中增加 os_name、os_version、app_version 和 ip 四個字段,用于做錯誤日志的分析。
  2023-06-07 在表中增加 country、province、city 和 isp 四個字段,存儲 IP 解析后的信息,包括國家、省份、城市和網絡運營商。
  IP 解析可以選擇付費服務,得到的結果比較準確,并且還能保持更新。或者選擇開源的[離線 IP 庫](https://github.com/lionsoul2014/ip2region),雖然精度不高,但是免費。
  2023-06-20 在表中增加 author 字段,記錄頁面維護人員。
  2023-07-10 在表中增加 fingerprint 字段,存儲瀏覽器指紋。
  2023-09-25 在表中增加 message_code 字段,提取接口響應中的code或status屬性,用于分析接口的業務邏輯是否正常。
  為了繪制用戶的行為軌跡,對 identity 的增加了一個索引。
  在正式上線后,遇到了幾次慢查詢,阿里云給出了相關索引建議,后面就直接加上了,效果立竿見影。
**1)堆棧壓縮**
  對于數據量很大的公司,像下面這樣的堆棧內容消耗的存儲空間是非常可觀的,因此有必要做一次壓縮。
  例如將重復的內容提取出來,用簡短的標識進行替代,把 URL 被替換成了 # 和數字組成的標識等。
~~~
{
"type": "runtime",
"lineno": 1,
"colno": 100,
"desc": "Uncaught Error: Cannot find module \"./paramPathMap\" at http://localhost:8000/umi.js:248565:7",
"stack": "Error: Cannot find module \"./paramPathMap\"
at Object.<anonymous> (http://localhost:8000/umi.js:248565:7)
at __webpack_require__ (http://localhost:8000/umi.js:679:30)
at fn (http://localhost:8000/umi.js:89:20)
at Object.<anonymous> (http://localhost:8000/umi.js:247749:77)
at __webpack_require__ (http://localhost:8000/umi.js:679:30)
at fn (http://localhost:8000/umi.js:89:20)
at Object.<anonymous> (http://localhost:8000/umi.js:60008:18)
at __webpack_require__ (http://localhost:8000/umi.js:679:30)
at fn (http://localhost:8000/umi.js:89:20)
at render (http://localhost:8000/umi.js:73018:200)
at Object.<anonymous> (http://localhost:8000/umi.js:73021:1)
at __webpack_require__ (http://localhost:8000/umi.js:679:30)
at fn (http://localhost:8000/umi.js:89:20)
at Object.<anonymous> (http://localhost:8000/umi.js:72970:18)
at __webpack_require__ (http://localhost:8000/umi.js:679:30)
at http://localhost:8000/umi.js:725:39
at http://localhost:8000/umi.js:728:10"
}
~~~
  考慮到我所在公司的數據量不會很大,人力資源也比較緊張,為了盡快上線,所以沒有使用壓縮,后期有時間了再來優化。
**2)去除重復**
  雖然沒有做壓縮,但是對于相同的日志還是做了一次去重操作。
  去重規則也很簡單,就是將項目 token、日志類別和日志內容拼接成一段字符串,再用MD5加密,下面是 Node.js 代碼。
  2022-01-17 在去重規則中增加 identity,因為發現之前的去重規則可能會影響用戶的行為軌跡,有些日志我需要單獨記錄。
~~~
const key = crypto.createHash("md5").update(identity + token + category + message).digest("hex");
~~~
  將 key 作為條件判斷數據庫中是否存在這條記錄,若存在就給 digit 字段加一。
:-: 
  在正式上線后,每秒會有幾百幾千次的請求發送過來。
  每次在添加日志時還要做這層判斷,就一度將數據庫阻塞掉。因為每次在做 key 判斷時要全表查詢一次,舊的查詢還沒執行完,新的就來了。
  為了解決此問題,就加上了一個基于 Redis 的隊列:[Kue](https://github.com/Automattic/kue),將判斷、更新和插入的邏輯封裝到一個任務中,異步執行。注意,目前此庫已經不維護了,首頁上推薦了替代品:[Bull](https://github.com/OptimalBits/bull)。
  再加上索引,雙重保障后,現在接收日志時未出現問題。
**3)行為記錄**
  2022-12-21 除了監控信息外,還會將錯誤發生時的行為記錄保存起來,以便排查。
~~~sql
CREATE TABLE `web_monitor_record` (
`id` INT NOT NULL AUTO_INCREMENT,
`monitor_id` BIGINT NOT NULL COMMENT '監控日志的ID',
`record` MEDIUMTEXT NOT NULL COMMENT '回放信息,包括各類元素和DOM操作記錄',
`ctime` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE INDEX `monitor_id_UNIQUE` (`monitor_id` ASC))
DEFAULT CHARACTER SET = utf8mb4
COLLATE = utf8mb4_bin
COMMENT = '直播監控回放';
~~~
  record 字段使用的是 MEDIUMTEXT 類型,可存儲 16M 的信息。
## 二、分析
  目前的分析部分也比較簡單,只包括一個監控看板、趨勢分析、日志列表和定時任務等。
**1)監控看板**
  在監控看板中包含今日數據和往期趨勢折線圖,本來想用[EChart.js](https://echarts.apache.org/zh/index.html)作圖,不過后面集成時出了點問題,并且該庫比較大,要500KB以上,于是換了另一個更小的庫:[Chart.js](https://chartjs.bootcss.com/),只有60KB。
  今日數據有今日和昨日的日志總數、錯誤總數和影響人數,通信、事件、打印和跳轉等總數。
:-: 
  其中錯誤總數會按照 category:"error" 的 sum(digit) 來求和,而影響人數只會按照記錄的個數來計算。
  今日的數量是實時計算的,在使用中發現查詢特別慢,要好幾分鐘才能得到結果,于是為幾個判斷條件中的字段加了個二級索引后(例如為 ctime 和 category 加索引),就能縮短到幾秒鐘響應。
~~~
ALTER TABLE `web_monitor` ADD INDEX `idx_category_ctime` (`category`, `ctime`);
SELECT count(*) AS `count` FROM `web_monitor`
WHERE (`ctime` >= '2021-03-25 16:00:00' AND `ctime` < '2021-03-26 16:00:00')
AND `category` = 'ajax';
~~~
  在往期趨勢中,會展示錯誤、500、502 和 504 錯誤、日志總數折線圖,這些數據會被保存在一張額外的統計表中,這樣就不必每次實時計算了。折線的顏色值取自[AntDesign](https://3x.ant.design/docs/spec/colors-cn#%E5%9F%BA%E7%A1%80%E8%89%B2%E6%9D%BF)。
  計算了一下出現 504 的通信占全部的 0.2%,接下來需要將這個比例再往下降。
:-: 
  在看板中,展示的錯誤日志每天在七八千左右,為了減少到幾百甚至更低的范圍,可采取的措施有:
* 過濾掉無意義的錯誤,例如SyntaxError: Unexpected token ',',該錯誤占了 55%~60% 左右。
* 優化頁面和接口邏輯,504通信錯誤占了25%~30% 左右。
* 將這兩個大頭錯誤搞定,再針對性優化剩下的錯誤,就能將錯誤控制目標范圍內。
  從日志中可以查看到具體的接口路徑,然后就能對其進行針對性的優化。
  例如有一張活動頁面,在進行一個操作時會請求兩個接口,并且每個接口各自發送 3 次通信,這樣會很容易發生 504 錯誤(每天大約有1500個這樣的請求),因此需要改造該邏輯。
  首先是給其中一張表加索引,然后是將兩個接口合并成一個,并且每次返回 20 條以上的數據,這樣就不用頻繁的發起請求了。
  經過改造后,每日的 504 請求從 1500 個左右降低到 200 個左右,減少了整整 7.5 倍,效果立竿見影,大部分的 504 集中在 22 點到 0 點之間,這段時間的活躍度比較高。
  還有一個令人意外的發現,那就是監控日志的量每天也減少了 50W 條。
  2021年9月,在監控看板中新增了504的接口統計查詢,可以倒序看到每個504接口出現的次數,便于我們主動排查問題。
  2022年3月,在監控看板中新增錯誤數量的統計查詢,可以倒序看到每個錯誤出現的次數,也是為了便于我們主動排查問題而添加的。
  2022年4月,在監控看板中留意到了打印總數,每天大約有17W條記錄,而其中很大一部分都是遺留的調試數據,線上其實并不需要,所以很有必要剔除掉。
**2)日志列表**
  在日志列表中會包含幾個過濾條件:編號、關鍵字、日期范圍、項目、日志類型和身份標識等。
  如果輸入了關鍵字,那么會在監控日志搜索結果列表中為其著色,這樣更便于查看,用正則加字符串的 replace() 方法配合實現的。
  在數據量上去后,當對內容(MYSQL 中的類型是 TEXT)進行模糊查詢時,查詢非常慢,用 EXPLAIN 分析SQL語句時,發現在做全表查詢。
  經過一番搜索后,發現了全文索引(match against 語法),在 5.7.6 之前的 MYSQL 不支持中文檢索,好在大部分情況要搜索的內容都是英文字符。
~~~
SELECT * FROM `web_monitor` WHERE MATCH(message) AGAINST('+*test*' IN BOOLEAN MODE)
~~~
  在建完這個索引后,表的容量增加了 3G 多,當前表中包含 1400W 條數據。
~~~
CREATE FULLTEXT INDEX ft_message ON web_monitor(message)
~~~
  有時候還是需要模糊匹配的,所以想了下加個下拉選項,來手動命令后臺使用哪種方式的查詢,但如果是模糊匹配,必須選擇日期來縮小查找范圍。
  其實后面在將數據導入 ElasticSearch 后,就可以非常快速的模糊查詢了,也不需要特地選擇檢索方式或時間范圍。
:-: 
  2022-11-16 新增日期選擇快捷按鈕,可快速填充今天、昨天和前天的時間范圍。
  2023-01-09 新增快捷按鈕,當點擊列表中的身份時,可以自動填充查詢條件中的身份文本框,并且取消對類別的選擇。
  之前在分析奔潰原因時,就會手動將身份信息復制到文本框中,再手動取消類別,比較影響操作體驗,所以才設計了這個快捷按鈕。
  2024-05-16 在條件中增加來源地址,因為接口日志的 message_path 字段保存的是接口地址中的路徑,而不是來源地址的路徑。
  所以當知道來源地址,想要查詢相關接口,就可以使用此條件。
:-: 
  2023-09-19 在查詢按鈕附近,增加受影響的人數,但只有當查詢錯誤記錄時,才會做分析。
  2023-10-30 在日志列表的詳情彈框中增加一欄:受影響的用戶列表,其中第一列是指紋,第二列是數量,便于找出當前最受影響的用戶。
:-: 
  在實際使用時,又發現缺張能直觀展示峰值的圖表,例如我想知道在哪個時間段某個特定錯誤的數量最多,于是又加了個按鈕和柱狀圖,支持跨天計算。
:-: 
  2022-12-06 在閱讀了相關數據分析的書籍后,了解到對于上面描述分布情況,更適合用直方圖(各個柱子之間沒有空隙)展示。
  身份標識可以查詢到某個用戶的一系列操作,更容易鎖定錯誤發生時的情境。
  每次查詢列表時,在后臺就會通過Source Map文件映射位置,注意,必須得有列號才能還原,并且需要安裝[source-map](https://www.npmjs.com/package/source-map)庫。
~~~
const sourceMap = require("source-map");
/**
* 讀取指定的Source-Map文件
*/
function readSourceMap(filePath) {
let parsedData = null;
try {
parsedData = fs.readFileSync(filePath, "utf8");
parsedData = JSON.parse(parsedData);
} catch (e) {
logger.info(`sourceMap:error`);
}
return parsedData;
}
/**
* 處理映射邏輯
*/
async function getSourceMap(row) {
// 拼接映射文件的地址
const filePath = path.resolve(
__dirname,
config.get("sourceMapPath"),
process.env.NODE_ENV + "-" + row.project + "/" + row.source
);
let { message } = row;
message = JSON.parse(message);
// 不存在行號或列號
if (!message.lineno || !message.colno) {
return row;
}
// 打包后的sourceMap文件
const rawSourceMap = readSourceMap(filePath);
if (!rawSourceMap) {
return row;
}
const errorPos = {
line: message.lineno,
column: message.colno
};
// 過傳入打包后的代碼位置來查詢源代碼的位置
const consumer = await new sourceMap.SourceMapConsumer(rawSourceMap);
// 獲取出錯代碼在哪一個源文件及其對應位置
const originalPosition = consumer.originalPositionFor({
line: errorPos.line,
column: errorPos.column
});
// 根據源文件名尋找對應源文件
const sourceIndex = consumer.sources.findIndex(
(item) => item === originalPosition.source
);
const sourceCode = consumer.sourcesContent[sourceIndex];
if (sourceCode) {
row.sourceInfo = {
code: sourceCode,
lineno: originalPosition.line,
path: originalPosition.source
};
}
// 銷毀,否則會報內存訪問超出范圍
consumer.destroy();
return row;
}
~~~
  點擊詳情,就能在彈框中查看到代碼具體位置了,編碼著色采用了[highlight.js](https://highlightjs.org/)。
  而每行代碼的行號使用了一個擴展的[highlight-line-numbers.js](https://github.com/wcoder/highlightjs-line-numbers.js/),柔和的淡紅色的色值是 #FFECEC。
:-: 
  圖中還有個上下文的 tab,這是一個很有用的功能,可以查詢到當前這條記錄前面和后面的所有日志。
  本以為萬事大吉,但是沒想到在檢索時用模糊查詢,直接將數據庫跑掛了。
  無奈,從服務器上將日志數據拉下來,導入本地數據庫中,在本地做查詢優化,2000W條數據倒了整整兩個小時。
  和數據組的同事溝通后,他們說可以引入 ElasticSearch 做檢索功能。當他們看到我的 message 字段中的內容時,他們建議我先做關鍵字優化。
  就是將我比較關心的內容放到單獨的字段中,提升命中率,而將一些可變的或不重要的數據再放到另一個字段中,單純的做存儲。
  例如通信內容中,我比較關心的是 url 和 status,那么就將它們抽取出來,并且去除無關緊要的信息(例如錯誤的 stack、通信的 headers)給 message 字段瘦身,最多的能減少三分之二以上。
~~~
{
"type": "GET",
"url": "/api/monitor/list?category=error&category=script&msg=",
"status": 200,
"endBytes": "0.15KB",
"interval": "22.07ms",
"network": {
"bandwidth": 0,
"type": "3G"
},
}
~~~
  最后決定報表統計的邏輯仍然用 MySQL,而檢索改成 ElasticSearch,由大數據組的同事提供接口,我們這邊傳數據給他們。
  而之前的檢索方式也可以棄用了,MySQL中存儲的日志數據也從 14 天減少到 7 天。
  在使用過程中遇到了幾個問題:
* 沒有將所有的數據傳遞到ES庫中,丟失了將近33%的數據,后面排查發現有些數據傳遞到了預發環境,而預發環境中有個參數沒配置導致無法推送。
* 在檢索時,返回的列表會漏幾條記錄,在一個可視化操作界面中輸入查詢條件可以得到連續的數據。經過排查發現,可能是在后臺查詢時,由于異步隊列的原因,那幾條數據還未推送,這樣的話就會得不到那幾條記錄,導致不連續。
  通過日志列表中的通信和點擊事件,可以計算出業務方日常工作的耗時,這個值可以作為指標,來驗證對業務優化后,前后的對比,這樣的量化能讓大家知道自己工作給業務方帶來了多少提升。
**3)定時任務**
  每天的凌晨4點,統計昨天的日志信息,保存到 web\_monitor\_statis 表中。
~~~
CREATE TABLE `web_monitor_statis` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`date` int(11) NOT NULL COMMENT '日期,格式為20210318,一天只存一條記錄',
`statis` text COLLATE utf8mb4_bin NOT NULL COMMENT '以JSON格式保存的統計信息',
PRIMARY KEY (`id`),
UNIQUE KEY `date_UNIQUE` (`date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='前端監控日志統計信息表';
~~~
  在 statis 字段中,保存的是一段JSON數據,類似于下面這樣,key 值是項目的 token。
~~~
{
"backend-app": {
allCount: 0,
errorCount: 1,
errorSum: 1,
error500Count: 0,
error502Count: 0,
error504Count: 1,
ajaxCount: 20,
consoleCount: 0,
eventCount: 0,
redirectCount: 0
}
}
~~~
  還有個定時任務會在每天的凌晨3點,將一周前的數據清除(web\_monitor 和 web\_monitor\_record),并將三周前的 map 文件刪除。
  之所以有個時間差是為了避免一周內的數據中還有需要引用兩周前的 map 文件,當然這個時間差還可以更久。
  注意,MySQL中表的數據通過 delete 命令刪除,如果使用的是 InnoDB 存儲引擎,那么是不會釋放磁盤空間的,需要執行 optimize 語句,例如:
~~~
optimize table `web_monitor`
~~~
  原先每日的數據量在180W左右,每條數據在 800B 左右,每天占用空間 1.3G 左右。
  后面優化了請求量,過濾掉重復和無意義的請求后(例如后臺每次都要發的身份驗證的請求、活動頁面的埋點請求等),每天的日志量控制在 100W 左右。
  而在經過上述活動的504優化后,請求量降到了 50W 左右,優化效果很喜人。
  保存 map 文件的空間在100G,應該是妥妥夠的。
  在未來會將監控拓展到小程序,并且會加上告警機制,在合適的時候用郵件、飛書、微信或短信等方式通知相關人員,后面還有很多擴展可做。
  敘述的比較簡單,但過程還說蠻艱辛的,修修補補,加起來的代碼大概有4、5千行的樣子。
  2023-09-07 完善每天的指標通知,根據 Nginx 訪問日志和性能日志計算得到,這塊的內容不是通過前端日志而來的,但是對于日常的監控卻也很重要。
  原先的飛書通知只會包含總的 SLA(5XX 請求的占比) 和慢響應,以及白屏和首屏占比。
~~~
Web核心指標 20230825
SLA: 99.99936%
SLA數量: 12
慢響應數量: 1394
慢響應比率: 12.36095‰
白屏1秒內比率: 90.18%
首屏1秒內比率: 72.81%
~~~
  每次要看細節還得打開看板后臺,在手機都不好操作,還得開啟電腦。
  于是做了一輪優化,將 SLA 分為對內的后臺,對外的國內業務和海外業務,慢響應增加國內和海外的業務數量占比。
  白屏也區分了國內和海外,因為公司對海外用戶的體驗也比較重視。經過優化后,就能了解每天的趨勢,只有在必要時,才去打開后臺查看指標細節。
~~~
Web核心指標 20231031
整體SLA: 99.99953%
整體SLA數量: 7
整體后臺SLA: 99.99804%
整體后臺SLA數量: 3
整體業務SLA: 99.99967%
整體業務SLA數量: 4
海外SLA: 100.00000%
海外SLA數量: 0
整體慢響應數量: 1567
整體慢響應比率: 18.43965‰
整體業務慢響應比率: 1.59287‰
海外慢響應比率: 2.86041‰
白屏1秒內比率: 90.95%
首屏1秒內比率: 78.87%
白屏時間>2秒數量: 1950
海外白屏時間>2秒數量: 412
~~~
**4)服務遷移**
  在使用時發現監控日志的服務比較占用CPU和內存,于是將其單獨抽取做來,獨立部署。
  經過這波操作后,整體的504錯誤,從 800 多漸漸降到了 100 左右。其中有一半的請求是埋點通信,業務請求降到了有史以來的最低點。
  但CPU和內存并沒有按預期下降,這部分涉及到了一次詳細的內存泄漏的摸查過程,在[下文](https://www.cnblogs.com/strick/p/14754867.html)會詳細分析。
**5)飛書告警**
  首先需要注冊[飛書開放平臺](https://open.feishu.cn/),并創建應用。
  然后在開放平臺找到憑證,記錄 app\_id 和 app\_secret。
:-: 
  接著在應用功能中,啟用機器人,由機器人來發送告警。
:-: 
  再獲取[tenant\_access\_token](https://open.feishu.cn/document/ukTMukTMukTM/uIjNz4iM2MjLyYzM)(需要 app\_id 和 app\_secret),最后通過另一個接口[發送消息](https://open.feishu.cn/document/ukTMukTMukTM/uUjNz4SN2MjL1YzM)。
  2023-07-12 增加告警的定時任務,第一期先只監控運行時、圖像和白屏三類錯誤。
  在分析過往發生事故的日志后,得出每半小時,在一個頁面中有上述一類的錯誤數量超過 10 個就發送告警。
  ctime 配置開始和結束時間,message\_type 聲明錯誤類型,message\_path 是頁面路徑。
~~~
SELECT `message`, `message_path`, `author`, count(*) AS `count` FROM `web_monitor`
WHERE (`ctime` >= '2023-07-12 06:00:00' AND `ctime` <= '2023-07-12 06:30:00') AND
`category` = 'error' AND `message_type` = 'runtime' GROUP BY `message_path` HAVING `count` >= 10
~~~
  2023-09-08 在開啟告警后,發現很多都是 image 類型的告警,但打開圖像地址,又都能正常,很多時候與網絡和代理有關,導致無法訪問圖像。
  所以我們的告警會收到大量的“誤報”(誤報率在 46% 左右),雖然的確是發生了錯誤,但這些告警對于我們監控頁面的健康趨勢,價值并不大。
  有時候還會干擾我們的判斷,因為有些圖像地址看著是正常的,但實際上是無法訪問的。
  對圖像的告警規則做了一次優化,借助 Node.js 自帶的[https](https://nodejs.org/docs/latest-v16.x/api/https.html)庫,訪問圖像的地址,包一層 Promise,方便使用 async/await 語法。
~~~
/**
* 讀取圖像請求狀態碼,判斷圖像是否存在
*/
async requestImg(src) {
return new Promise((resolve) => {
https.get(src, (response) => {
resolve(response.statusCode);
});
});
}
~~~
  而對于 HTTP 的圖像請求,以及 webp 的圖像錯誤都會放行,不會過濾掉,這類會判斷為異常。
~~~
const filterList = [];
for (const item of list) {
const message = JSON.parse(item.message);
const src = message.desc.src;
// 對于http請求、webp圖像、html地址,直接作為錯誤處理
if (src.indexOf("https:") === -1 ||
src.indexOf(".webp") >= 0 ||
src.indexOf('.html') >=0
) {
filterList.push(item);
continue;
}
const code = await this.requestImg(src);
// 過濾掉正常的圖像請求
if (code === 200) continue;
filterList.push(item);
}
list = filterList;
~~~
## 三、錯誤分析
  2023-05-19 在調研了市面上的幾款監控系統的功能后,決定在已有的頁面中對錯誤展開進一步的分析。
  新增的三個功能,都采用[抽屜式的交互](https://4x.ant.design/components/drawer-cn/),從屏幕右側邊緣滑出浮層面板。
**1)行為軌跡**
  這是一個成熟的監控系統必備的功能,了解用戶的行為軌跡,可以為解決問題提供眾多的線索。
  在數據庫表中有一個 identity 字段,作為用戶的唯一標識,將其作為查詢條件,就能得到一組日志。
~~~sql
SELECT * FROM `web_monitor` WHERE `identity` = 'xxxx'
~~~
  將得到的日志,根據類型進行不同的渲染,并且提供不同顏色的 icon 便于識別,如下圖所示。
:-: 
  時間軸被分為兩部分,左邊有時間、日志子類型和日志 ID,右邊是日志的關鍵信息。
  起初并沒有做分頁,因為考慮到 H5 頁面以活動為主,所以日志不會很多。
  但是后面發現在管理后臺中,一個用戶會存在巨量的日志,因此在渲染時,直接將頁面搞奔潰了,無響應。
  為此,馬上設計了分頁查詢,默認是一頁 50 條。
**2)錯誤趨勢**
  選取了常用的三類錯誤,運行時、圖像和奔潰,主要是對本周和上周的數據對比,如下圖所示。
:-: 
  觀察兩條曲線,可以更直觀的判斷優化前后是否有效,Sunday、Tuesday 這些單詞都是直接通過[日期庫](https://moment.nodejs.cn/)計算得出的。
  2023-07-14 將兩條曲線轉換成分組柱狀圖,可以更清晰的對兩周的錯誤進行差異對比。
:-: 
  2023-06-20 增加各類錯誤的趨勢圖,因為圖像類錯誤比較多,所以為了避免影響其它錯誤的曲線,沒有將其統計進來。
:-: 
  2023-07-14 將各類錯誤的趨勢圖改成了分組柱狀圖,相比曲線圖,更容易觀察變化。
:-: 
  2023-06-28 增加各種類型的排行榜,可更方便的獲悉哪些頁面的這類錯誤比較多,明確優化對象后,就能優先盡快解決。
:-: 
  對通信也做了一個榜單,便于對請求做優化,例如刪除一個請求、多個請求合并成一個等。
  以及根據身份也做了一個錯誤榜單,分析哪些用戶遇到的錯誤比較多,其實也是在縮小范圍,更準確的進行優化。
**3)錯誤詳情**
  在錯誤詳情中,包含基本的信息,有 IP、瀏覽器、操作系統等信息,以及錯誤的相關信息,包括最近出現、首次出現、Android、iOS 等錯誤數量,如下所示。
:-: 
  注意,這些數量的統計范圍是 2 個月,因為我這邊只保留 2 個月的日志,這些日志存儲在 ElasticSearch 中。
  下面是一條查詢 Android 系統相同錯誤數量的 SQL 語句,我在 MySQL 只保留了 7 天的數據,總數據量在 5000 多萬條 。
~~~sql
SELECT COUNT(*) FROM `web_monitor` WHERE
`message` LIKE "%Uncaught TypeError: Cannot read property 'id' of null at https://www.xxx.me/game/js/operation54.773abd8e.js:1:69175%"
and `category` = 'error' and `message_type` = 'runtime' and `os_name` = 'Android'
and `message_path` = 'game/operation54.html';
~~~
  雖然使用了模糊查詢,但是因為錯誤日志的數據量不大,并且加足了過濾條件,所以查詢速度并不慢。
  另一個分析手段是錯誤覆蓋,包括操作系統和客戶端版本的覆蓋,用餅圖的方式展現,如下圖所示。
:-: 
  依據表中的 os\_name、os\_version、app\_version 三個字段可以計算出操作系統和客戶端版本的數量。
  最后還會用柱狀圖呈現最近 7 天和最近 30 天的錯誤發生的趨勢,如下圖所示。
:-: 
  關于錯誤,有的系統可以將其作為一個任務分配給某個人,然后在解決后更新錯誤狀態。
  2024-03-28 在錯誤詳情中增加錯誤區域,在各個省市自治區中標注錯誤數量,顯示分布情況,顏色越深,錯誤越多。
:-: 
  先下載[中國地圖坐標](http://datav.aliyun.com/portal/school/atlas/area_selector)的 JSON 數據,并修改成模塊化語法,存儲到 geoJson.js 文件中,如下所示。
~~~
export default {
type: 'FeatureCollection',
features: [{
type: 'Feature',
properties: {
adcode: 110000,
name: '北京',
center: [116.405285, 39.904989],
centroid: [116.41995, 40.18994],
childrenNum: 16,
level: 'province',
parent: {
adcode: 100000
},
subFeatureIndex: 0,
acroutes: [100000],
},
...
}]
}
~~~
  省市自治區的名稱可做修改,例如北京市改成北京,然后基于[ECharts 5.X](https://echarts.apache.org/examples/zh/index.html)?版本生成地圖。
  下面是簡化的初始化代碼,option 變量以及其它配置內容都比較多,直接省略了,可參考[此處](https://www.webfunny.cn/blog/post/115)。
~~~
import * as echarts from 'echarts';
// 導入地圖數據
import geoJson from '../data/geoJson.js';
const mapName = 'china';
// 注冊可用地圖
echarts.registerMap(mapName, { geoJSON: geoJson });
// 初始化
const myCharts = echarts.init(document.getElementById('map'), null, { renderer: 'svg' });
myCharts.setOption(option);
~~~
**4)維護人員**
  在后臺中已經搜集到了線上的各類錯誤,但是組內的成員并不會定期去查看各自維護的頁面錯誤情況。
  思考了一段時間后,想到記錄各個頁面的維護人員,將他們的名字保存到日志中。
  然后根據人員統計成錯誤折線圖或錯誤排行榜,這樣就可以用圖表的方式督促維護人員盡快修復現有問題,并且還能觀察錯誤趨勢。
:-: 
  再就是將各個人員的錯誤數每天定時推送到飛書或釘釘上,每天提醒一下大家有空就去優化和修復問題。
  2023-06-20 發現折線圖并不能很好的對比錯誤趨勢,于是改成了分組柱狀圖。
  每種顏色代表某一名維護人員,誰的錯誤上升異常,可以非常明顯的觀察到。
:-: 
  2023-10-17 增加各維護人員的頁面錯誤數量榜單,就是讓維護人員可以看到錯誤數量比較多的頁面。
  對這些頁面做重點優化,以及排查數量比較多的原因,進行針對性的修復。
:-: 
  2024-05-15 發現當團隊成員離職時,原先的維護人員配置就需要修改,而當前是寫在各個頁面中,改動比較繁瑣。
  未來推薦將維護人員與頁面地址的映射關系保存到服務端的接口中,若有人離職,只需要在此處做改動即可。
**參考:**
[利用sourceMap定位錯誤實踐](https://juejin.cn/post/6882265367251517447)
[螞蟻金服基于堆棧映射快速定位問題](https://zhuanlan.zhihu.com/p/158879127)
[貝貝集團多端錯誤監控平臺](https://zhuanlan.zhihu.com/p/158079491)
[MySQL 全文索引實現簡單版搜索引擎](https://www.cnblogs.com/YangJiaXin/p/11153579.html)
*****
> 原文出處:
[博客園-從零開始搞系列](https://www.cnblogs.com/strick/category/1928903.html)
已建立一個微信前端交流群,如要進群,請先加微信號freedom20180706或掃描下面的二維碼,請求中需注明“看云加群”,在通過請求后就會把你拉進來。還搜集整理了一套[面試資料](https://github.com/pwstrick/daily),歡迎閱讀。

推薦一款前端監控腳本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不僅能監控前端的錯誤、通信、打印等行為,還能計算各類性能參數,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、擴展運算符和剩余參數
- 3、解構
- 4、模板字面量
- 5、對象字面量的擴展
- 6、Symbol
- 7、代碼模塊化
- 8、數字
- 9、字符串
- 10、正則表達式
- 11、對象
- 12、數組
- 13、類型化數組
- 14、函數
- 15、箭頭函數和尾調用優化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、類
- 21、類的繼承
- 22、Promise
- 23、Promise的靜態方法和應用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基礎實踐
- 3、WebRTC視頻通話
- 4、Web音視頻基礎
- CSS進階
- 1、CSS基礎拾遺
- 2、偽類和偽元素
- 3、CSS屬性拾遺
- 4、浮動形狀
- 5、漸變
- 6、濾鏡
- 7、合成
- 8、裁剪和遮罩
- 9、網格布局
- 10、CSS方法論
- 11、管理后臺響應式改造
- React
- 1、函數式編程
- 2、JSX
- 3、組件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表單
- 8、樣式
- 9、組件通信
- 10、高階組件
- 11、Redux基礎
- 12、Redux中間件
- 13、React Router
- 14、測試框架
- 15、React Hooks
- 16、React源碼分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基礎
- 4、webpack進階
- 5、Git
- 6、Fiddler
- 7、自制腳手架
- 8、VSCode插件研發
- 9、WebView中的頁面調試方法
- Vue.js
- 1、數據綁定
- 2、指令
- 3、樣式和表單
- 4、組件
- 5、組件通信
- 6、內容分發
- 7、渲染函數和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、數據類型
- 2、接口
- 3、類
- 4、泛型
- 5、類型兼容性
- 6、高級類型
- 7、命名空間
- 8、裝飾器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系統和網絡
- 3、命令行工具
- 4、自建前端監控系統
- 5、定時任務的調試
- 6、自制短鏈系統
- 7、定時任務的進化史
- 8、通用接口
- 9、微前端實踐
- 10、接口日志查詢
- 11、E2E測試
- 12、BFF
- 13、MySQL歸檔
- 14、壓力測試
- 15、活動規則引擎
- 16、活動配置化
- 17、UmiJS版本升級
- 18、半吊子的可視化搭建系統
- 19、KOA源碼分析(上)
- 20、KOA源碼分析(下)
- 21、花10分鐘入門Node.js
- 22、Node環境升級日志
- 23、Worker threads
- 24、低代碼
- 25、Web自動化測試
- 26、接口攔截和頁面回放實驗
- 27、接口管理
- 28、Cypress自動化測試實踐
- 29、基于Electron的開播助手
- Node.js精進
- 1、模塊化
- 2、異步編程
- 3、流
- 4、事件觸發器
- 5、HTTP
- 6、文件
- 7、日志
- 8、錯誤處理
- 9、性能監控(上)
- 10、性能監控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 監控系統
- 1、SDK
- 2、存儲和分析
- 3、性能監控
- 4、內存泄漏
- 5、小程序
- 6、較長的白屏時間
- 7、頁面奔潰
- 8、shin-monitor源碼分析
- 前端性能精進
- 1、優化方法論之測量
- 2、優化方法論之分析
- 3、瀏覽器之圖像
- 4、瀏覽器之呈現
- 5、瀏覽器之JavaScript
- 6、網絡
- 7、構建
- 前端體驗優化
- 1、概述
- 2、基建
- 3、后端
- 4、數據
- 5、后臺
- Web優化
- 1、CSS優化
- 2、JavaScript優化
- 3、圖像和網絡
- 4、用戶體驗和工具
- 5、網站優化
- 6、優化閉環實踐
- 數據結構與算法
- 1、鏈表
- 2、棧、隊列、散列表和位運算
- 3、二叉樹
- 4、二分查找
- 5、回溯算法
- 6、貪心算法
- 7、分治算法
- 8、動態規劃
- 程序員之路
- 大學
- 2011年
- 2012年
- 2013年
- 2014年
- 項目反思
- 前端基礎學習分享
- 2015年
- 再一次項目反思
- 然并卵
- PC網站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端學習之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 2024年
- 日志
- 2020