前端日志上报这事平时真没人拿它当复杂度管理。代码里埋一行 axios.post数据能到服务端就万事大吉。可一旦线上出事故或者用户在地铁里碰上个白屏 bug你打开日志后台想查现场发现关键时段的数据一条都不剩——那一刻你就明白了前端日志上报“不丢包”不是给你锦上添花的是给你救命用的。这篇文章我会完整讲一遍前端日志上报怎么做到可靠不丢包的核心思路、断网弱网时的处理、用户直接关页面时怎么把日志送出去还有一套可以落地到业务里的最小实现代码。全文不装高深按我调日志踩坑多年的经验来讲适合把日志上报做得比较糙、又想快速提升可靠性的团队也适合准备前端面试时想摸清监控上报原理的人。1. 先把问题摊开前端日志为什么会丢1.1 三个最容易丢日志的典型场景先说生活里最常见的三个丢日志场景你会发现它们不约而同都出现在“用户不配合你”的时候。场景一用户直接关掉页面或刷新页面。比如移动端用户看完页面内容顺手一个系统返回手势或者在小程序里右滑切走再比如手机 App 被系统杀掉后台进程。这个瞬间你的日志请求可能还躺在网络栈里浏览器一卸载页面上下文异步请求直接就被取消了。很多团队说“我在 beforeunload 里发请求不就行了”——同步 XHR 确实能发出去但会把页面卡到让人崩溃而且移动端对同步 XHR 的兼容越来越差浏览器明显在收紧这个口子。场景二弱网环境。用户进了电梯、地铁隧道、地下室或者农村 4G 信号就一格网络延迟几百毫秒到几秒。日志请求要么在排队要么超时要么发出去了但服务端没收到。常见表现是开发环境一切正常线上真用户环境日志覆盖低得可怜一到你真正需要日志去定位问题的时候日志就是不来。场景三彻底断网。飞行模式、Wi-Fi 断开、移动数据被关。这时候用户在页面上产生的大量操作日志、错误日志你发出的请求必死无疑。问题是断网只是瞬时的用户可能在 30 秒后重新连上网但这时候页面还在不在页面在你之前的日志早就丢了页面不在连新日志都没机会发了。这三类场景叠加起来基本上就是“日志不丢包”的全部战场。如果你只是追求日志在理想网络上到达率 99%那很简单难就难在要在这些恶劣条件下让尽可能多的日志最终到达服务端。1.2 传统上报方式的三个隐藏短板早年很多前端日志方案写成这样fetch(/log, { method: POST, body: JSON.stringify({ msg: error xxx, time: Date.now() }) });代码看起来没毛病但它有三个隐藏短板。第一fetch 和 XHR 这类常规请求完全依赖页面上下文。页面关闭的瞬间浏览器会取消未完成的网络请求。这个行为是浏览器层面的安全策略你没有办法通过“换个姿势发请求”来绕过除非用下面要讲的特殊 API。第二localStorage 虽然能存日志但它有两大致命伤容量只有 5MB 左右存满之后 setItem 直接抛异常而且它是同步 API往里面写数据会在主线程上造成卡顿。日志一多用户就能明显感觉到页面掉帧这对日志方案本身来讲就是不可接受的副作用。第三也是最重要的——没有重试机制。一次请求失败了管都不管就丢了。服务端超时、网络抖动、接口发布重启任何一个环节闪一下日志就没了。日志系统最怕的不是某一次失败而是失败之后没有人负责“重新送一次”。这三点一叠加你会发现传统方案在理想网络下都在碰运气更别说断网弱网了。明白了为什么丢下面两个核心方案就不难理解了一个是针对“页面关闭瞬间”的 sendBeacon一个是针对“断网弱网阶段”的本地缓存加队列重试。这两者组合才叫完整的不丢包方案。2. 核心方案一用 sendBeacon 把“关页”这几秒抢回来2.1 sendBeacon 到底怎么“不怕”页面关闭先纠正一个常见认知不是所有网络请求在页面关闭时都会丢。浏览器其实有一个特殊通道专门用于页面销毁前向服务端发送少量数据这个通道就是navigator.sendBeacon。sendBeacon 的设计初衷就是解决埋点数据在页面关闭时丢失的问题。它有几个关键特性异步发送不阻塞页面卸载流程。不依赖页面 JS 运行环境持续存活请求由浏览器内核单独调度。请求会在页面关闭之后继续由浏览器尝试发送直到成功或超时。不需要服务端返回响应你也不知道它到底成没成功除非它返回 false。它的使用方式极其简单window.addEventListener(pagehide, () { if (navigator.sendBeacon) { const data JSON.stringify({ type: page_close, ts: Date.now() }); navigator.sendBeacon(/api/log, data); } });你可能会问为什么不直接在 beforeunload 里用同步 XHR 发送答案很简单同步 XHR 是可用的但用户体验极差页面切走时会有明显卡顿而且浏览器在移动端上对同步 XHR 的时长限制越来越严用户一按 Home 键请求照样发不出去。sendBeacon 走的是一条独立的、高优先级的浏览器通道虽然它不是瞬时到达但在绝大多数情况下都能成功送达。还有个细节页面关闭时的“标准事件”这几年有变化。beforeunload 在不少浏览器里已经不保证触发unload 也有类似问题现在比较靠谱的是pagehide它兼容桌面和移动端。如果是要监听页面进入后台、但页面还活着可以配一个visibilitychange判断document.visibilityState hidden来兜底。放一段我在生产环境用的组合监听function reportOnPageHide(logs) { if (!logs.length) return; const send (payload) { if (navigator.sendBeacon) { const blob new Blob([payload], { type: application/json }); return navigator.sendBeacon(BEACON_URL, blob); } return false; }; // pagehide 是现在兼容性最好的卸载信号 window.addEventListener(pagehide, () { const body JSON.stringify({ logs }); try { send(body); } catch (e) { // 这里要静默页面关闭阶段任何异常都可能触发新的上报循环 } }); }2.2 什么时候该用 sendBeacon什么时候别死磕sendBeacon 不是万能的它有明确的边界。第一数据量有限。Chrome 在实现上把单次 sendBeacon 的大小限制在 64KB 左右超过限制会返回 false请求直接不发。Safari 和 Firefox 的实现限制更小。所以 sendBeacon 适合存“最后的紧急日志”不适合在页面卸载时硬塞几百 KB 的完整日志包。第二Content-Type 受限。如果你直接传字符串浏览器会以text/plain发送如果你传 Blob可以自己指定类型但服务端解析时如果默认按 form 表单解析就会出问题。我建议用 Blob 并明确指定application/json这样跟普通 JSON 接口保持一致。第三它没有超时机制也没有回调。你只知道返回 true/falsetrue 只代表“请求已提交给浏览器”不代表服务端一定收到了false 代表“发送失败”但你没法拿到具体失败原因。所以 sendBeacon 适合做“尽力而为”的最后一次尝试不适合作为完整的可靠性方案。所以我的判断是sendBeacon 只是整个不丢包方案的最后一道防线核心路径仍然是本地缓存加队列重试。不要想着只用 sendBeacon 就能搞定所有问题那样遇到弱网断网很容易翻车。3. 核心方案二IndexedDB 本地缓存 队列重试3.1 为什么是 IndexedDB而不是 localStorage把日志先存到本地、等网络恢复后再补发这个思路几乎都能想到。但存哪里就有说头了。localStorage 的优点是使用简单同步 API 一把梭。但它的缺点前面已经提到容量小、同步阻塞。对日志这种高频写入的场景localStorage 根本扛不住。你想想用户刷个列表页一秒钟可能产生十几条埋点日志每条几 KB5MB 容量几分钟就满了而且每次 setItem 都阻塞主线程页面必定卡。IndexedDB 就不一样了容量大大多数浏览器下限是 50MB 起步移动端也能存下大量日志对象。异步 API写操作不会阻塞主线程渲染。原生支持结构化存储直接存 JavaScript 对象不用序列化成字符串。支持索引和游标方便按时间、类型筛选。IndexedDB 的问题也明显API 实在太啰嗦了。一个最简单的增删改查要写一长串 transaction 回调。这也是很多团队嫌麻烦所以放弃它的原因。我的做法是把它封装成一个只有add、getAll、delete、clear四个方法的类剩下的交给上层队列逻辑去用。对比一下存储方案容量异步存储结构适合场景localStorage约 5MB否字符串键值对少量配置、用户偏好sessionStorage约 5MB否字符串键值对单个会话临时数据IndexedDB50MB是结构化对象高频日志缓存、离线数据Memory 对象不限但易丢是任意短期队列缓冲3.2 队列消费模型从内存缓冲到持久化兜底日志上报要分两层内存层和持久层。内存层是一个 JS 数组负责快速接收日志。日志产生时先推入数组不用担心性能问题。达到一定数量比如 10 条或者每隔几秒就触发一次发送尝试。这里的缓冲是有意义的——把多条日志合并成一个请求能大幅减少 HTTP 请求数量对服务端也更友好。持久层才是断网弱网的重武器。当发送请求失败时把失败的那批日志写进 IndexedDB然后停止尝试发送等网络恢复或下一个定时周期再补发。这样即使断网十分钟日志也都躺在 IndexedDB 里不会丢。设计成两级队列还有一个好处内存队列可以保证高频场景下不至于频繁操作 IndexedDB毕竟 IndexedDB 虽然是异步的但频繁读写还是会有性能开销。而 IndexedDB 里的日志就相当于积压的“待发送货物”只要网络一恢复就继续往服务端搬。这里要注意一个关键点消费队列的时候一定要有并发保护。如果上一次发送还没结束下一次定时器又触发了两个请求就会交叉日志顺序全乱还可能重复发送。我用一个flushing标志位来保证同一时间只有一个发送任务在跑async flush() { if (this.flushing) return; const logs this.buffer.splice(0, this.batchSize); if (!logs.length) return; this.flushing true; try { await this.sender.send(logs); await this.purgeStored(logs); this.retryCount 0; } catch (e) { // 发送失败回滚到 buffer 头部并持久化 this.buffer.unshift(...logs); await this.store.add(logs); this.scheduleRetry(); } finally { this.flushing false; } }3.3 指数退避重试和弱网适配重试逻辑不能是“死循环重试”也不能是“固定间隔重试”。死循环重试会把你服务端打爆固定间隔重试在弱网环境里只会继续失败然后继续浪费带宽。业界标准做法是指数退避第一次失败等 1 秒第二次等 2 秒第三次等 4 秒第四次等 8 秒以此类推直到达到上限比如 30 秒就不再增加超过最大重试次数后本次日志重新回到 IndexedDB等待下一次全局触发器。具体实现我是这样组织的scheduleRetry() { if (this.retryTimer) return; const delay Math.min( 1000 * Math.pow(2, this.retryCount), 30 * 1000 ); this.retryCount 1; this.retryTimer setTimeout(() { this.retryTimer null; this.flush(); }, delay); }配合另一个重要逻辑监听浏览器的网络状态事件。window.addEventListener(online, () { this.retryCount 0; this.flush(); });断网期间本来就不应该白费力气去发请求。用navigator.onLine在最前面拦一道断网直接不进发送逻辑等 online 事件触发后再把积压队列一次性发出去效率最高。弱网还有个细节很多时候navigator.onLine是 true但网络质量差到请求根本发不出去。这种情况靠在线状态拦不住只能靠请求超时和重试来兜。我建议给发送逻辑加上超时控制前端 fetch 可以结合 AbortController 实现sendBeacon 没法控制超时所以只用于最后兜底。4. 实操完整实现把兜底方案落到代码上4.1 模块划分与职责我把整个日志上报拆成四个模块职责边界很清晰Logger对外暴露的 API业务方只跟它打交道。LogStore封装 IndexedDB 读写负责持久化缓存。LogSender负责真正发出 HTTP 请求内部做 sendBeacon / fetch 降级。LogScheduler核心调度器维护内存队列控制发送时机处理失败重试。调用链长这样业务代码调logger.info()日志进入 Scheduler 的内存队列Scheduler 批量把数据交给 SenderSender 发 HTTP 请求。如果失败Scheduler 把日志写入 Store 并排重试如果网络断开了Store 里一直躺着数据等网络恢复后再继续。4.2 核心代码实现先看 LogStore一个够用就行的 IndexedDB 封装// log-store.js const DB_NAME fe-log-db; const STORE_NAME logs; const DB_VERSION 1; export class LogStore { constructor() { this.db null; } open() { return new Promise((resolve, reject) { if (this.db) { resolve(this.db); return; } const request indexedDB.open(DB_NAME, DB_VERSION); request.onupgradeneeded () { const db request.result; if (!db.objectStoreNames.contains(STORE_NAME)) { db.createObjectStore(STORE_NAME, { keyPath: id }); } }; request.onsuccess () { this.db request.result; resolve(this.db); }; request.onerror (e) reject(e.target.error); }); } async add(logs) { const db await this.open(); return new Promise((resolve, reject) { const tx db.transaction(STORE_NAME, readwrite); const store tx.objectStore(STORE_NAME); logs.forEach((log) store.add(log)); tx.oncomplete () resolve(); tx.onerror (e) reject(e.target.error); }); } async getBatch(limit 100) { const db await this.open(); return new Promise((resolve, reject) { const tx db.transaction(STORE_NAME, readonly); const store tx.objectStore(STORE_NAME); const request store.getAll(); request.onsuccess () resolve(request.result.slice(0, limit)); request.onerror (e) reject(e.target.error); }); } async delete(ids) { const db await this.open(); return new Promise((resolve, reject) { const tx db.transaction(STORE_NAME, readwrite); const store tx.objectStore(STORE_NAME); ids.forEach((id) store.delete(id)); tx.oncomplete () resolve(); tx.onerror (e) reject(e.target.error); }); } }这个封装只暴露了add、getBatch、delete三个方法够用。注意getAll()可能把大量数据一次性读进内存所以我在外面做 slice 限流防止 IndexedDB 里积压了几千条时直接把内存打爆。再看 LogSender它负责发出请求并做降级// log-sender.js const ENDPOINT /api/log/batch; function fetchWithTimeout(url, options, timeout 8000) { const controller new AbortController(); const timer setTimeout(() controller.abort(), timeout); return fetch(url, { ...options, signal: controller.signal }).finally(() { clearTimeout(timer); }); } export function sendLogs(logs) { const body JSON.stringify({ logs }); // 如果是为了页面关闭场景优先用 sendBeacon if (typeof navigator.sendBeacon function) { const blob new Blob([body], { type: application/json }); const ok navigator.sendBeacon(ENDPOINT, blob); if (ok) return Promise.resolve(); } // 页面存活且网络正常时用带超时的 fetch return fetchWithTimeout(ENDPOINT, { method: POST, headers: { Content-Type: application/json }, body, }).then((res) { if (!res.ok) { throw new Error(log upload failed: ${res.status}); } }); }这里有个取舍sendBeacon 放在第一位是因为它在页面接近关闭时也能发送但正常场景下我更希望用带超时的 fetch因为它能让我感知到失败并重试。所以严格来说sendLogs内部应该区分是谁在调它。页面关闭场景单独走sendBeaconLoop常规场景走 fetch。分开写更清晰export function sendLogs(logs) { const body JSON.stringify({ logs }); return fetchWithTimeout(ENDPOINT, { method: POST, headers: { Content-Type: application/json }, body, }).then((res) { if (!res.ok) throw new Error(log upload failed: ${res.status}); }); } export function sendLogsOnPageHide(logs) { if (!navigator.sendBeacon) return Promise.resolve(false); const blob new Blob([JSON.stringify({ logs })], { type: application/json }); const ok navigator.sendBeacon(ENDPOINT, blob); return Promise.resolve(ok); }然后是核心调度器 LogScheduler// log-scheduler.js import { LogStore } from ./log-store; import { sendLogs } from ./log-sender; export class LogScheduler { constructor({ batchSize 10, maxRetry 5, maxBufferSize 2000, flushInterval 5000, } {}) { this.store new LogStore(); this.batchSize batchSize; this.maxRetry maxRetry; this.maxBufferSize maxBufferSize; this.flushInterval flushInterval; this.buffer []; this.flushing false; this.retryCount 0; this.retryTimer null; this.flushTimer null; this._init(); } _init() { window.addEventListener(online, () { this.retryCount 0; this.flush(); }); this.flushTimer setInterval(() this.flush(), this.flushInterval); } push(log) { this.buffer.push(log); // 内存缓冲不能无限膨胀 if (this.buffer.length this.maxBufferSize) { this.buffer.shift(); } if (this.buffer.length this.batchSize) { this.flush(); } } async flush() { if (this.flushing) return; // 先把内存队列和 IndexedDB 里的积压数据合并起来发 const memoryLogs this.buffer.splice(0, this.batchSize); const storedLogs await this.store.getBatch(this.batchSize - memoryLogs.length); const logs [...memoryLogs, ...storedLogs]; if (!logs.length) return; this.flushing true; try { await sendLogs(logs); if (storedLogs.length) { await this.store.delete(storedLogs.map((l) l.id)); } this.retryCount 0; } catch (e) { // 失败的重新放回内存并持久化一部分防止只有内存导致刷新丢失 this.buffer.unshift(...memoryLogs); if (storedLogs.length) { await this.store.add(storedLogs); } this.scheduleRetry(); } finally { this.flushing false; } } scheduleRetry() { if (this.retryTimer) return; const delay Math.min(1000 * Math.pow(2, this.retryCount), 30000); this.retryCount 1; this.retryTimer setTimeout(() { this.retryTimer null; this.flush(); }, delay); } }最后是 Logger 入口// logger.js import { LogScheduler } from ./log-scheduler; import { sendLogsOnPageHide } from ./log-sender; const scheduler new LogScheduler({ batchSize: 10, maxRetry: 5, maxBufferSize: 2000 }); function generateId() { return ${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}; } function createLog(type, data) { return { id: generateId(), type, data: data || null, ts: Date.now(), page: location.href, ua: navigator.userAgent, }; } export const logger { info(data) { scheduler.push(createLog(info, data)); }, warn(data) { scheduler.push(createLog(warn, data)); }, error(data) { scheduler.push(createLog(error, data)); }, // 页面关闭前直接用 sendBeacon 把内存队列里剩下的日志全发掉 flushOnPageHide() { const pending scheduler.buffer.splice(0, scheduler.batchSize); if (pending.length) { sendLogsOnPageHide(pending); } }, }; window.addEventListener(pagehide, () { logger.flushOnPageHide(); });这套代码放到业务里直接就能跑。核心逻辑不依赖任何第三方库也没有复杂框架依赖新老项目都能接。4.3 关键参数的确定依据几个参数看起来像随手写的其实都有依据batchSize 10Nginx 默认请求体大小、后端接口吞吐、日志平台批量接口规格都不一样。单批 10 条是我在多数项目里折中出来的请求体大概几 KB 到几十 KB不会触发服务端限制也够扛日常埋点频率。maxRetry 5指数退避排下来是 1、2、4、8、16、32 秒最多等 32 秒。超过 5 次还在失败说明网络或服务端短时间恢复不了再重试只会浪费资源不如安静地躺在 IndexedDB 里等 online 事件。maxBufferSize 2000内存队列最多存 2000 条再多就丢最老的。这么做是为了防止极端情况下比如服务端挂了很久内存被日志占爆。IndexedDB 的容量还更大但也要给它设置上限我一般在调用方按天清理或按条数清理。flushInterval 5000每 5 秒兜底 flush 一次避免低日志频率下数据长时间堆积在本地查问题不及时。这些参数不是死的日志量小的业务可以把 batchSize 调小、flushInterval 调大日志量极大的业务则要反过来甚至要引入采样和压缩来降低量级。5. 工程化细节压缩、幂等、采样和降级5.1 批量上报与压缩日志量大时单条日志几十字节、上百字节攒 10 条请求体也不大但日志内容如果有完整的用户操作路径、DOM 状态、接口返回体批量加起来可能上百 KB。这种情况建议做压缩。浏览器端现代方案是用CompressionStreamChrome 80 支持gzip 压缩文本数据async function compressText(text) { const stream new Blob([text]).stream().pipeThrough(new CompressionStream(gzip)); const compressedBlob await new Response(stream).blob(); return compressedBlob; }然后把压缩后的二进制直接 POST 给服务端服务端按 gzip 解压再解析 JSON。压缩率对重复字段很多的日志数据往往能达到 80% 以上效果非常明显。不过要注意CompressionStream 在某些旧浏览器里不支持。我一般做个特性判断支持就用压缩不支持就明文 JSON保证兼容。5.2 幂等去重日志在重试机制下有可能会被重复投递。比如请求已经到服务端了但响应在网络中丢失前端认为失败就重新发了一遍。这个时候服务端不做去重就会出现日志重复统计的问题。所以每条日志我都生成了一个唯一 id用时间戳加随机数拼起来function generateId() { return ${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}; }服务端拿到批量日志后按 id 做去重。这个去重可以用 Redis 的 set也可以建消息表用唯一索引。简单说前端保证“尽量不重发”服务端兜底保证“即使重发也能去重”。5.3 采样策略日志全量上报是不现实的。线上 PV 几百万的时候全量日志能把你服务端写穿。所以可靠性和成本之间要做平衡。我的经验是分级采样错误日志全量上报错误是最需要关注的数据。用户关键行为登录、下单、支付全量上报这些操作频率不高但业务价值极高。普通页面浏览、接口调用按 10% 或 1% 采样随机丢弃一部分。采样率可以在 Logger 初始化时配置甚至可以从服务端拉取动态配置这样线上要临时提高采样率排查问题时不用发版改代码。5.4 降级方案最后说一下降级链。整套上报链路我按优先级排了一条链navigator.sendBeacon页面关闭场景能发就发。fetch keepaliveChrome 支持keepalive选项也能在页面卸载时尽量发但注意它同样对体积有限制且不是所有浏览器都支持。fetch带超时页面存活时的主要通道。XMLHttpRequest同步发送最无奈的后备方案只有前面全失效才用而且只用来发最关键的一条恢复上报。写代码时不需要把每条链都写进去做到 sendBeacon fetch 超时这两级就够了剩下的看浏览器支持情况再补。6. 常见问题与排查技巧实录6.1 典型问题速查表现象可能原因排查方向sendBeacon 返回 false数据量超过浏览器限制页面内存异常检查日志包大小压缩或者拆分批次断网重连后没有立即补发忘了监听 online 事件重试定时器被清了检查 Scheduler 的 online 监听逻辑日志重复上报请求成功但响应丢失触发重试服务端按 id 去重检查重试计数逻辑页面关闭时日志根本没发pagehide 未触发sendBeacon 浏览器不支持改用 visibilitychange 兜底检查兼容性IndexedDB 数据积压越来越多服务端一直失败重试次数上限后没有持续触发设置队列容量上限加全局 flush 任务批量请求过大触发服务端 413batchSize 或者单条日志内容过重调小批次、压缩请求体、裁剪 data 字段6.2 排查方法调试日志上报最讨厌的一点是生产环境问题复现难弱网断网又很难模拟。我常用的方法有三套。第一套是 Chrome DevTools 的网络面板。Network 面板里可以切换Offline模式直接模拟断网。配合Network Throttling里的Slow 3G就能模拟弱网环境。这个观察请求是否按预期进入缓存、重试、补发流程。第二套是在服务端临时打印日志接收时序。我会给每条日志打上ts前端埋点时间戳和服务端接收时间server_ts两个一对比就能算出网络延迟和排队情况。这也方便判断“日志到达率”到底是多少。第三套是本地加日志断点。在 Scheduler 的flush的 try 和 catch 里打 console.log观察内存队列与 IndexedDB 的交互顺序是否正确。页面关闭场景可以在pagehide里断点但要注意断点会打断页面销毁流程结果不完全准确不过看代码逻辑够用了。另外我强烈建议做一次“断网埋点自检”页面加载后正常埋点立刻断网。在页面里多操作几次产生几十条日志。恢复网络观察日志是否在 1 到 5 秒内陆续到达。再试一次断网后不清除缓存直接关页面再重新打开页面看 IndexedDB 里老日志有没有被新页面的调度器捞起来补发。我在多个项目里跑这几步每轮都能发现新问题比如 online 事件没触发、IndexedDB 读写失败、批次数据顺序错乱等等比自己拍脑袋验证要靠谱得多。个人经验小结最后聊点实际体会。我到后期做日志上报项目时目标从来不是“100% 不丢”。前端日志能到达 99%在没有可靠传输协议的普通 HTTP 上报体系里已经非常好了。与其追求极致的到达率不如先把链路做清晰正常情况下快速上报异常情况下缓存重试页面关闭时用 sendBeacon 兜底服务端做幂等去重。这一整套跑顺下来你遇到的基本很少再“丢关键日志”。还有一个小建议日志上报的代码虽然本身不复杂但它属于“平时没人关注、出事全靠它”的基础设施。建议接入时就把采样率、批次大小、队列上限做成可配置项并把上报成功率做成一个独立监控指标。这样日志系统本身出了问题你也能第一时间发现而不是等到查事故的时候才察觉“日志一直是断的”。如果后续日志量继续涨可以考虑把数据接入专门的日志服务平台前端上报思路和本文一致后端存储和检索直接托管能省下不少运维精力。
