节点在线、应用在线、配置在线使用令牌查询
大石头 authored at 2021-12-16 19:49:30
4.17 KiB
Stardust
/**
 * Koa 中间件:自动追踪 HTTP 请求。
 */

'use strict';

const { parseTraceparent, runWithSpan } = require('../trace-context');

/**
 * 是否需要忽略此路径。
 * @param {String} path 请求路径
 * @returns {Boolean} 是否忽略
 */
function isIgnoredPath(path) {
    const text = String(path || '');
    return /^\/(App|Trace)(\/|$)/i.test(text) || text.indexOf('/App/') !== -1 || text.indexOf('/Trace/') !== -1;
}

/**
 * 读取请求头(不区分大小写)。
 * @param {Object} req 请求
 * @param {String} name 头名
 * @returns {String} 头值
 */
function getHeader(req, name) {
    const headers = req && req.headers;
    if (!headers) return '';
    const lower = name.toLowerCase();
    let value = headers[lower];
    if (value === undefined) value = headers[name];
    if (value === undefined) {
        for (const key of Object.keys(headers)) {
            if (key.toLowerCase() === lower) {
                value = headers[key];
                break;
            }
        }
    }
    if (Array.isArray(value)) return value.length > 0 ? String(value[0]) : '';
    return value === undefined || value === null ? '' : String(value);
}

/**
 * 取路由模板路径。
 * @param {Object} ctx Koa 上下文
 * @param {String} fallback 回退路径
 * @returns {String} 路径
 */
function getRoutePath(ctx, fallback) {
    try {
        let route = ctx ? (ctx.routePath || ctx._matchedRoute) : null;
        if (!route && ctx && ctx.route) route = ctx.route.path;
        if (Array.isArray(route) && route.length > 0) route = route[0];
        if (!route || typeof route !== 'string') return fallback;
        return route.charAt(0) === '/' ? route : '/' + route;
    } catch (err) {
        return fallback;
    }
}

/**
 * 创建 Koa 中间件。
 * @param {Object} tracer 追踪器实例
 * @returns {Function} Koa 中间件
 */
function createKoaMiddleware(tracer) {
    return async function stardustMiddleware(ctx, next) {
        let path = '/';
        try {
            path = (ctx && (ctx.path || (ctx.url ? String(ctx.url).split('?')[0] : ''))) || '/';
            if (isIgnoredPath(path)) return next();
        } catch (err) {
            path = '/';
        }

        const method = ctx && ctx.method ? String(ctx.method).toUpperCase() : 'GET';
        const tag = method + ' ' + ((ctx && (ctx.originalUrl || ctx.url)) || path);
        let span = null;
        try {
            // 复用 HTTP 自动入口创建的 Span,避免重复
            span = ctx && ctx.__stardustSpan ? ctx.__stardustSpan : null;
            if (!span && ctx && ctx.req && ctx.req.__stardustSpan) span = ctx.req.__stardustSpan;
            if (span) {
                span.setTag(tag);
            } else {
                const inbound = parseTraceparent(getHeader(ctx && ctx.req, 'traceparent'));
                if (inbound) span = tracer.newSpan(method + ' ' + getRoutePath(ctx, path), { parentId: inbound.spanId, traceId: inbound.traceId });
                else span = tracer.newSpan(method + ' ' + getRoutePath(ctx, path));
                span.setTag(tag);
                if (ctx) ctx.__stardustSpan = span;
            }
        } catch (err) {
            span = null;
        }

        let finished = false;
        try {
            if (span) {
                await runWithSpan(span, function () {
                    return next();
                });
            } else {
                await next();
            }

            if (span && ctx && ctx.status >= 400) span.setError('HTTP ' + ctx.status);
        } catch (err) {
            if (span) {
                try {
                    span.setError(err);
                } catch (ignore) {
                    // 忽略
                }
            }
            throw err;
        } finally {
            if (span && !finished) {
                finished = true;
                try {
                    span.name = method + ' ' + getRoutePath(ctx, path);
                    span.finish();
                } catch (err) {
                    // 监控异常不得影响业务
                }
            }
        }
    };
}

module.exports = createKoaMiddleware;
module.exports.isIgnoredPath = isIgnoredPath;
module.exports.getRoutePath = getRoutePath;