/** * 日志上报模块 */ const _ = require('lodash'); const axios = require('axios'); const jsonwebtoken = require('jsonwebtoken'); const opentracing = require('opentracing'); const jaeger = require('jaeger-client'); const log = require('log4js').getLogger(); const httpLog = require('log4js').getLogger('http'); /** * 业务异常 */ class ApiError extends Error { constructor(message, code) { super(message) this.code = code; this.timestamp = Date.now(); } toJSON() { return { timestamp: this.timestamp, code: this.code, message: this.message } } } const cmdArgs = process.argv let isLocal = false cmdArgs.forEach((arg) => { if(arg == 'local'){ isLocal = true } }) const services = { "pay": "http://api-yitong-com-pay-v1.default/" } /** * YiTong SDK HTTP */ const http = axios.create({ method: 'POST', headers: { 'content-type': 'application/json;charset=utf-8' }, timeout: 60 * 1000, // 60秒超时 maxContentLength: 100 * 1024 * 1024, httpAgent: new require('http').Agent({ keepAlive: true }), httpsAgent: new require('https').Agent({ keepAlive: true }) }); // koa opentracing // https://github.com/fapspirit/axios-opentracing http.interceptors.request.use(function (config) { if(!isLocal) { let regExp = new RegExp(/(http|https):\/\/api.(dev.|test.|)yitong.com\/[\D]*\//) if(regExp.test(config.url)){ let service = config.url.match(regExp)[0].split('/')[3] let k8sService = services[service] if(k8sService) { config.url = config.url.replace(regExp, k8sService) } } } if (config.context && config.context.span) { let tracer = opentracing.globalTracer(); let span = tracer.startSpan(`${config.method}: ${config.url}`, { childOf: config.context.span }); config.span = span; span.setTag(opentracing.Tags.COMPONENT, 'axios'); span.setTag(opentracing.Tags.SPAN_KIND, opentracing.Tags.SPAN_KIND_RPC_CLIENT); span.setTag(opentracing.Tags.HTTP_METHOD, config.method); span.setTag(opentracing.Tags.HTTP_URL, config.url); tracer.inject(span, opentracing.FORMAT_HTTP_HEADERS, config.headers); } return config; }, function (error) { if (error.config && error.config.span) { let { span } = error.config; try { span.setTag(opentracing.Tags.ERROR, true); span.setTag(opentracing.Tags.HTTP_STATUS_CODE, error.code); span.log({ 'event': 'error', 'error.object': error.message }); span.finish(); } catch (e) { log.error(e); } } return Promise.reject(error); }); http.interceptors.response.use(function (response) { if (response.config && response.config.span) { let { span } = response.config; try { span.setTag(opentracing.Tags.HTTP_STATUS_CODE, response.status); span.finish(); } catch (e) { log.error(e); } } return response; }, function (error) { if (error.config && error.config.span) { let { span } = error.config; try { span.setTag(opentracing.Tags.ERROR, true); span.setTag(opentracing.Tags.HTTP_STATUS_CODE, error.code); span.log({ 'event': 'error', 'error.object': error.message }); span.finish(); } catch (e) { log.error(e); } } return Promise.reject(error); }); // YiTong JSON-RPC http.interceptors.request.use(function (config) { if (config.context && config.context.headers) { // 有上下文环境,透传HTTP请求头 let { headers } = config.context; Object.keys(headers).forEach(key => { let key2 = key.toLowerCase(); if (config.headers[key2] === undefined) { // 仅透传authorization及x-yt-自定义请求头 if (key2 === 'authorization' || key2.startsWith('x-yt-')) { config.headers[key2] = headers[key]; } } }); } return config; }); http.interceptors.response.use(function (response) { if (response.data) { if (response.data.code === 1 || response.data.code === 0) { return response.data.result; } } return Promise.reject(response); }); function sleep(ms) { return new Promise((resolve, reject) => { setTimeout(resolve, ms) }); } /** * JSON RPC 返回值 */ function ok(result, message) { return { timestamp: Date.now(), code: 1, message: message || '操作成功', result: result }; } /** * JSON RPC 异常 */ function error(e) { if (e) { return new ApiError(e.message || '出错了', e.code || 9); } else { return new ApiError('出错了', 9); } } /** * JSON RPC 异常转换 */ function transformError(e) { // axios异常 if (e && e.request && e.status && e.data) { e = e.data; } // 业务异常 if (e && e.timestamp && e.code && e.message) { return e; } else { return error(); } } /** * 异常处理中间件 */ function errorHandler() { return async function (ctx, next) { try { await next(); } catch (e) { ctx.status = 200; ctx.body = transformError(e); log.error(e); } }; } /** * 调试中间件 */ function debug() { return async function (ctx, next) { httpLog.debug(ctx.request.body); await next(); // httpLog.debug(ctx.body); }; } /** * opentracing链路跟踪中间件 * https://github.com/opentracing-contrib/javascript-express */ function tracing(opts) { opentracing.initGlobalTracer(jaeger.initTracer(opts.tracing)); const tracer = opentracing.globalTracer(); return async function (ctx, next) { let wireCtx = tracer.extract(opentracing.FORMAT_HTTP_HEADERS, ctx.headers); let name = `${ctx.method}:${ctx.path}`; let span = tracer.startSpan(name, { childOf: wireCtx }); ctx.span = span; span.setTag(opentracing.Tags.COMPONENT, 'koa'); span.setTag(opentracing.Tags.SPAN_KIND, opentracing.Tags.SPAN_KIND_RPC_SERVER); span.setTag(opentracing.Tags.HTTP_METHOD, ctx.method); span.setTag(opentracing.Tags.HTTP_URL, ctx.url); if (ctx.headers['user-agent']) { span.setTag('http.headers.user_agent', ctx.headers['user-agent']); } if (ctx.headers['referer']) { span.setTag('http.headers.referer', ctx.headers['referer']); } // 返回traceId,便于排查问题 let responseHeaders = {}; tracer.inject(span, opentracing.FORMAT_TEXT_MAP, responseHeaders); Object.entries(responseHeaders).forEach(keyValue => { let [key, value] = keyValue; ctx.set(key, value); }); try { await next(); } catch (e) { span.setTag(opentracing.Tags.ERROR, true); span.setTag(opentracing.Tags.SAMPLING_PRIORITY, 1); span.log({ 'event': 'error', 'error.object': e.message }); throw e; } finally { try { span.setTag(opentracing.Tags.HTTP_STATUS_CODE, ctx.status); if (ctx.status >= 500) { span.setTag(opentracing.Tags.ERROR, true); span.setTag(opentracing.Tags.SAMPLING_PRIORITY, 1); } span.finish(); } catch (e) { log.error(e); } } }; } /** * YiTong 专用头部中间件 */ function headerx() { return async function (ctx, next) { /** * Authorization中token */ ctx.getAuthorizationToken = function () { let authorization = /Bearer\s+(\S*)/i.exec(ctx.headers.authorization) if (authorization && authorization[1]) { return authorization[1]; } // 兼容 return ctx.query.token || ""; }; /** * 客户端Key */ ctx.getAppKey = function () { return ctx.headers['x-yt-appkey']; }; /** * 客户端版本号 */ ctx.getAppVersion = function () { let appVersion = ctx.headers['x-yt-appversion']; if (appVersion) { return appVersion; } let userAgent = ctx.headers['user-agent']; if (userAgent) { let index = userAgent.lastIndexOf('/'); if (index > 0) { return userAgent.substr(index + 1); } } return undefined; }; await next(); } } /** * Token解析中间件 */ function token(opts) { // token公钥,缓存 const publicKey = axios(`${opts.apiBase}/auth/oauth2/public-key.pem`); return async function (ctx, next) { let authorizationToken = ctx.getAuthorizationToken(); if (authorizationToken) { try { let { data } = await publicKey; // token解密,缓存 let token = jsonwebtoken.verify(authorizationToken, data); ctx.token = token; if (ctx.span) { ctx.span.setTag('token.provider', token.iss); ctx.span.setTag('token.app_key', token.aud); ctx.span.setTag('token.subject', token.sub); } } catch (e) { ctx.body = error({ code: 100, message: '请重新登录' }); if (ctx.span) { ctx.span.setTag(opentracing.Tags.ERROR, true); ctx.span.setTag(opentracing.Tags.SAMPLING_PRIORITY, 1); ctx.span.log({ 'event': 'error', 'error.object': e.message }); } return; } } /** * 当前请求token */ ctx.getToken = function () { return ctx.token; }; await next(); }; } /** * 鉴权中间件 */ function auth(opts) { return async function (ctx, next) { /** * 当前用户 */ ctx.getUser = function () { if (ctx.token) { return http({ context: ctx, url: `${opts.apiBase}/auth/v3/user/getUser`, data: { appKey: ctx.getAppKey() } }); } else { return null; } }; /** * 应用级token */ ctx.getApplicationToken = function () { return http({ context: ctx, url: `${opts.apiBase}/auth/v3/application/getTokenByApplication`, data: { appKey: opts.appKey, appSecret: opts.appSecret } }); }; await next(); }; } /** * 用户行为跟踪上报中间件 */ function track(opts) { return async function (ctx, next) { ctx.track = true; // 全部接口收集 let t = Date.now(); ctx.trackEvent = { __referer__: `${opts.serviceName}:${ctx.path}`, t: t, app_key: ctx.getAppKey() || _.get(ctx.token, 'aud') || opts.appKey, event: { key: 'api' }, segmentation: {}, session_id: ctx.ip, user_details: { user_id: _.get(ctx.token, 'sub') } } try { await next(); } finally { if (ctx.track && ctx.trackEvent.user_details.user_id) { let { trackEvent } = ctx; trackEvent.event.sum = Number(((Date.now() - t) / 1000.0).toFixed(3)); let trackQuery = Object.entries(trackEvent).map(keyValue => { let [key, value] = keyValue; if (Object.prototype.toString.call(value) === '[object String]') { return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; } else { return `${encodeURIComponent(key)}=${encodeURIComponent(JSON.stringify(value))}`; } }).join('&'); axios({ method: 'GET', url: `${opts.trackBase}${trackQuery}`, headers: { 'user-agent': ctx.headers['user-agent'] } }).catch((e) => { log.error(e); }); } } }; } /** * 数据校验工具 */ function validate(b, message) { if (!b) { throw error({ message: message || '参数错误', code: 10 }); } } function equal(value, expected, message) { if (value != expected) { throw error({ message: message || '参数错误', code: 10 }); } } function gt(value, expected, message) { validate(value > expected, message); } function notEmpty(value, message) { validate(!!value, message); } function maxlength(value, expected, message) { if (value) { validate(value.length <= expected, message); } } /** * App扩展 */ function extendApp(app, opts) { app.use(errorHandler()); // 启用异常处理 app.use(debug()); // 启用请求参数调试 app.use(tracing(opts)); // 启用链路跟踪 app.use(headerx()); // 启用亿童请求头提取 app.use(token(opts)); // 启动token解析 app.use(auth(opts)); // 启动鉴权中间件 app.use(track(opts)); // 启动用户行为跟踪 return app; } module.exports = { http, // SDK客户端 sleep, ok, // 正常请求返回封装 error, // 生成业务异常 transformError, // 处理各类异常,转为业务异常 errorHandler, // 异常处理中间件 debug, // 调试中间件 tracing, // 链路跟踪中间件 headerx, // 亿童请求头中间件 token, // token解析中间件 auth, // 鉴权中间件 track, // 用户行为跟踪中间件 validator: { // 数据校验工具 gt, notEmpty, equal, maxlength }, extendApp // koa扩展入口 }; // context扩展 // context.span // 链路跟踪rootSpan // context.token // 当前请求token缓存,对象,已解析,请使用context.getToken()访问 // context.track // 是否跟踪用户行为开关(建议值) // context.trackEvent // 用户行为跟踪事件 // context.getAuthorizationToken() // 获取Authorization请求头中token,字符串 // context.getAppKey() // 客户端Key // context.getAppVersion() // 客户端版本号 // context.getToken() // 当前请求token,对象,已解析 // context.getUser() // 当前请求用户 // context.getApplicationToken() // 获取应用级token