UNPKG

@qiniu/miku-delivery-mp-ks

Version:

Kuaishou Mini Program SDK for Miku Delivery

200 lines (199 loc) 7.68 kB
/** * @file 日志上报处理 * @desc 参考文档 https://cf.qiniu.io/pages/viewpage.action?pageId=107318296 * @todo - 日志持久化存储 */ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { getAccessToken } from '../utils/access-token'; import { getDebug } from '../utils/debug'; import { logApiPrefix } from '../utils/env'; import { fetch as defaultFetch, Headers } from '../utils/fetch/index'; import Mutex from '../utils/mutex'; import { stringifyQuery, wait } from '../utils/index'; import { getEnv } from './env'; export const apiPrefix = logApiPrefix; /** 一次上报请求最多带 200 条 */ const logNumPerCall = 200; const debug = getDebug('log'); export default class Logger { constructor( /** 应用信息 */ appInfo, /** 请求发送实现 */ fetch = defaultFetch, /** 最大累计待上报条数 */ flushNum = 100, /** 最大上报延迟,单位秒 */ flushWait = 30) { Object.defineProperty(this, "appInfo", { enumerable: true, configurable: true, writable: true, value: appInfo }); Object.defineProperty(this, "fetch", { enumerable: true, configurable: true, writable: true, value: fetch }); Object.defineProperty(this, "flushNum", { enumerable: true, configurable: true, writable: true, value: flushNum }); Object.defineProperty(this, "flushWait", { enumerable: true, configurable: true, writable: true, value: flushWait }); Object.defineProperty(this, "env", { enumerable: true, configurable: true, writable: true, value: stringifyQuery(getEnv()) }); /** 上报行为的互斥控制 */ Object.defineProperty(this, "flushMutex", { enumerable: true, configurable: true, writable: true, value: new Mutex() }); /** 累计待上报的日志,从老到新排 */ Object.defineProperty(this, "buffer", { enumerable: true, configurable: true, writable: true, value: [] }); } callApiLog(logs, attempts = 3, retryInterval = 2000) { return __awaiter(this, void 0, void 0, function* () { const schemaLogsArray = groupLogsBySchema(logs); const schemas = schemaLogsArray.map(item => item.schema).join(','); const requestUrl = `${logApiPrefix}/v1/log/${schemas}`; const accessToken = getAccessToken({ appID: this.appInfo.appID, appSalt: this.appInfo.appSalt, path: requestUrl }); const request = { url: requestUrl, method: 'POST', headers: new Headers({ 'Authorization': accessToken, 'Content-Type': 'text/csv', 'X-Env': this.env }), body: getLogBody(schemaLogsArray) }; for (let i = 1; i <= attempts; i++) { try { const resp = yield this.fetch(request); if (resp.ok) return; throw new Error(`Unexpected response status: ${resp.status} ${resp.statusText}`); } catch (e) { const shouldRetry = i < attempts; console.warn('Call log API failed:', e, shouldRetry ? `Retry after ${retryInterval}ms` : ''); if (shouldRetry) yield wait(retryInterval); } } }); } /** 把所有记录下的日志进行上报 */ flush() { return this.flushMutex.runExclusive(() => { const logs = this.buffer.splice(0); if (logs.length === 0) return; const callNum = Math.ceil(logs.length / logNumPerCall); return Promise.all(Array.from({ length: callNum }).map((_, i) => (this.callApiLog(logs.slice(i * logNumPerCall, (i + 1) * logNumPerCall))))); }); } /** * 检查是否要上报,满足以下条件之一则上报: * 1. 累计未上报 $flushNum 条 * 2. $flushWait 秒 */ tryFlush() { return __awaiter(this, void 0, void 0, function* () { const flushOrRetry = yield this.flushMutex.runExclusive(() => { const buffer = this.buffer; if (buffer.length === 0) return false; if (buffer.length >= this.flushNum) { debug('buffer.length >= this.flushNum'); return true; } const waited = Date.now() - buffer[0].log.ts; if (waited >= (this.flushWait * 1000)) { debug('waited >= this.flushWait'); return true; } return (this.flushWait * 1000) - waited; // retry after a while }); if (flushOrRetry === true) return this.flush(); if (typeof flushOrRetry === 'number') { setTimeout(() => this.tryFlush(), flushOrRetry); } }); } /** 记录单条日志 */ log(schemaName, logData) { debug('log', schemaName, logData); this.buffer.push({ schema: schemaName, log: Object.assign({ ts: Date.now() }, logData) }); this.tryFlush(); } } function groupLogsBySchema(logs) { const schemaLogsArray = []; logs.forEach(({ schema, log }) => { let schemaLogs = schemaLogsArray.find(group => group.schema === schema); if (schemaLogs == null) { schemaLogs = { schema, logs: [] }; schemaLogsArray.push(schemaLogs); } schemaLogs.logs.push(log); }); return schemaLogsArray; } /** 组装打点请求的 body,多个 Schema 日志以空行间隔 */ export function getLogBody(logs) { return logs.map(item => item.logs).map(getSchemaLogBody).join('\n\n'); } /** 组装单个 Schema 打点请求的 body,CSV 格式,合并多条日志 */ function getSchemaLogBody(logs) { const fields = Object.keys(logs[0]); // 要求 timestamp 总是第一列 const sortedFields = ['ts', ...fields.filter(f => f !== 'ts')]; const headLine = sortedFields.map(processCSVValue).join(','); const bodyLines = logs.map(log => sortedFields.map(k => log[k]).map(v => v != null ? (v + '') : '').map(processCSVValue).join(',')); return [headLine, ...bodyLines].join('\n'); } // https://www.rfc-editor.org/rfc/rfc4180.html // https://stackoverflow.com/a/24922761 function processCSVValue(value) { let str = value.replace(/"/g, '""'); if (/("|,|\n)/.test(str)) { str = '"' + str + '"'; } return str; }