@qiniu/miku-delivery-mp-ks
Version:
Kuaishou Mini Program SDK for Miku Delivery
200 lines (199 loc) • 7.68 kB
JavaScript
/**
* @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;
}