节点在线、应用在线、配置在线使用令牌查询
大石头 authored at 2021-12-16 19:49:30
4.78 KiB
Stardust
/**
 * Express.js 中间件:自动追踪 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);
}

/**
 * 取路由模板路径,优先使用 Express 匹配到的路由。
 * @param {Object} req 请求
 * @param {String} fallback 回退路径
 * @returns {String} 路径
 */
function getRoutePath(req, fallback) {
    try {
        const route = req && req.route ? req.route.path : null;
        let path = null;
        if (Array.isArray(route) && route.length > 0) path = route[0];
        else if (typeof route === 'string') path = route;
        if (!path || typeof path !== 'string') return fallback;

        if (path.charAt(0) !== '/') path = '/' + path;
        const baseUrl = req.baseUrl ? String(req.baseUrl).replace(/\/+$/, '') : '';
        if (baseUrl && path.indexOf(baseUrl + '/') !== 0) path = baseUrl + path;
        return path;
    } catch (err) {
        return fallback;
    }
}

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

        const method = req && req.method ? String(req.method).toUpperCase() : 'GET';
        const tag = method + ' ' + ((req && (req.originalUrl || req.url)) || path);
        let span = null;

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

        let finished = false;
        const finish = function () {
            if (finished) return;
            finished = true;
            if (!span) return;

            try {
                const routePath = getRoutePath(req, path);
                span.name = method + ' ' + routePath;
                if (res && res.statusCode >= 400) span.setError('HTTP ' + res.statusCode);
                span.finish();
            } catch (err) {
                // 监控异常不得影响业务
            }
        };

        try {
            if (res && typeof res.end === 'function') {
                const originalEnd = res.end;
                res.end = function () {
                    finish();
                    return originalEnd.apply(this, arguments);
                };
            }
            if (res && typeof res.once === 'function') {
                res.once('finish', finish);
                res.once('close', finish);
            }
        } catch (err) {
            // 监控异常不得影响业务
        }

        if (!span) return next();

        try {
            return runWithSpan(span, function () {
                return next();
            });
        } catch (err) {
            try {
                span.setError(err);
            } catch (ignore) {
                // 忽略
            }
            finish();
            throw err;
        }
    };
}

module.exports = createExpressMiddleware;
module.exports.isIgnoredPath = isIgnoredPath;
module.exports.getRoutePath = getRoutePath;