解决MySql布尔型新旧版本兼容问题,采用枚举来表示布尔型的数据表。由正向工程赋值
大石头 authored at 2018-05-15 21:21:05
6.90 KiB
X
using System.Net;
using System.Text;

namespace NewLife.Log;

/// <summary>追踪器解析器</summary>
public interface ITracerResolver
{
    /// <summary>从Uri中解析出埋点名称</summary>
    /// <param name="uri"></param>
    /// <param name="userState"></param>
    /// <returns></returns>
    String? ResolveName(Uri uri, Object? userState);

    /// <summary>解析埋点名称</summary>
    /// <param name="name"></param>
    /// <param name="userState"></param>
    /// <returns></returns>
    String? ResolveName(String name, Object? userState);

    /// <summary>创建Http请求埋点</summary>
    ISpan? CreateSpan(ITracer tracer, Uri uri, Object? userState);
}

/// <summary>默认追踪器解析器</summary>
/// <remarks>
/// 解析器给予用户自定义创建Http请求埋点的机会,可以根据需要自定义埋点名称和标签。
/// 在星尘扩展中,再次扩展解析,支持自定义WebApi接口的埋点名称和标签。
/// </remarks>
public class DefaultTracerResolver : ITracerResolver
{
    /// <summary>请求内容是否作为数据标签。默认true</summary>
    /// <remarks>丰富的数据标签可以辅助分析问题,关闭后提升性能</remarks>
    public Boolean RequestContentAsTag { get; set; } = true;

    /// <summary>支持作为标签数据的内容类型</summary>
    public String[] TagTypes { get; set; } = [
        "text/plain", "text/xml", "application/json", "application/xml", "application/x-www-form-urlencoded"
    ];

    /// <summary>标签数据中要排除的头部</summary>
    public String[] ExcludeHeaders { get; set; } = ["traceparent", "Cookie"];

    /// <summary>从Uri中解析出埋点名称</summary>
    /// <param name="uri"></param>
    /// <param name="userState"></param>
    /// <returns></returns>
    public virtual String? ResolveName(Uri uri, Object? userState)
    {
        String name;
        if (uri.IsAbsoluteUri)
        {
            // 太长的Url分段,不适合作为埋点名称
            var segments = uri.Segments.Skip(1).TakeWhile(e => e.Length <= 16).ToArray();
            name = segments.Length > 0
               ? $"{uri.Scheme}://{uri.Authority}/{String.Concat(segments)}"
               : $"{uri.Scheme}://{uri.Authority}";
        }
        else
        {
            name = uri.ToString();
            var p = name.IndexOf('?');
            if (p > 0) name = name[..p];
        }

        return ResolveName(name, userState);
    }

    /// <summary>解析埋点名称</summary>
    /// <param name="name"></param>
    /// <param name="userState"></param>
    /// <returns></returns>
    public virtual String? ResolveName(String name, Object? userState) => name;

    #region 辅助

    /// <summary>读取Http内容的前缀字符串,用于埋点标签</summary>
    /// <param name="content">Http内容</param>
    /// <param name="maxLength">最大字符数</param>
    /// <returns>前缀字符串,失败时返回null</returns>
    private static String? ReadContentPrefix(ByteArrayContent content, Int32 maxLength)
    {
        if (content == null) return null;

        try
        {
            using var stream = content.ReadAsStreamAsync().ConfigureAwait(false).GetAwaiter().GetResult();
            if (stream == null) return null;

            // 从ContentType获取编码,回退到UTF-8
            var encoding = Encoding.UTF8;
            var charset = content.Headers.ContentType?.CharSet;
            if (!charset.IsNullOrEmpty())
            {
                try
                {
                    encoding = Encoding.GetEncoding(charset);
                }
                catch
                {
                }
            }

            using var reader = new StreamReader(stream, encoding);
            var chars = new Char[maxLength];
            var read = reader.Read(chars, 0, maxLength);
            if (read > 0) return new String(chars, 0, read);

            return null;
        }
        catch
        {
            return null;
        }
    }

    /// <summary>从多部分内容中查找第一个文本子内容</summary>
    /// <remarks>
    /// 遍历 MultipartContent 的子部分,返回第一个既是 ByteArrayContent 且 ContentType 匹配 TagTypes 的子内容。
    /// StringContent 和 FormUrlEncodedContent 均继承自 ByteArrayContent,因此可被正确处理。
    /// StreamContent 不是 ByteArrayContent,自动跳过,不会消耗文件流。
    /// </remarks>
    /// <param name="content">可能是多部分的内容</param>
    /// <returns>第一个文本子内容,未找到时返回null</returns>
    private ByteArrayContent? FindFirstTextContent(HttpContent? content)
    {
        if (content is not IEnumerable<HttpContent> parts) return null;

        foreach (var sub in parts)
        {
            if (sub is ByteArrayContent bac &&
                (bac.Headers.ContentType == null ||
                 bac.Headers.ContentType.MediaType == null ||
                 bac.Headers.ContentType.MediaType.StartsWithIgnoreCase(TagTypes)))
            {
                return bac;
            }
        }

        return null;
    }

    #endregion

    /// <summary>创建Http请求埋点</summary>
    public virtual ISpan? CreateSpan(ITracer tracer, Uri uri, Object? userState)
    {
        var name = tracer.Resolver.ResolveName(uri, userState);
        if (name.IsNullOrEmpty()) return null;

        var span = tracer.NewSpan(name);

        var request = userState as HttpRequestMessage;
        var method = request?.Method.Method ?? (userState as WebRequest)?.Method ?? "GET";
        var tag = $"{method} {uri}";

        if (RequestContentAsTag && tag.Length < tracer.MaxTagLength &&
            span is DefaultSpan ds && ds.TraceFlag > 0 && request != null)
        {
            var maxLength = ds.Tracer?.MaxTagLength ?? 1024;
            // 读取请求体前缀作为埋点标签。优先直接读取,否则尝试 multipart 子部分
            if (request.Content is ByteArrayContent bc &&
                (bc.Headers.ContentType == null ||
                 bc.Headers.ContentType.MediaType == null ||
                 bc.Headers.ContentType.MediaType.StartsWithIgnoreCase(TagTypes)))
            {
                var prefix = ReadContentPrefix(bc, maxLength);
                if (!prefix.IsNullOrEmpty()) tag += "\r\n" + prefix;
            }
            else
            {
                var child = FindFirstTextContent(request.Content);
                if (child != null)
                {
                    var prefix = ReadContentPrefix(child, maxLength);
                    if (!prefix.IsNullOrEmpty()) tag += "\r\n" + prefix;
                }
            }

            if (tag.Length < 500)
            {
                var vs = request.Headers.Where(e => !e.Key.EqualIgnoreCase(ExcludeHeaders)).ToDictionary(e => e.Key, e => e.Value.Join(";"));
                tag += "\r\n" + vs.Join("\r\n", e => $"{e.Key}: {e.Value}");
            }
        }
        span.SetTag(tag);

        return span;
    }
}