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

namespace NewLife.Log;

/// <summary>OpenTelemetry 采集器。把进程内所有 活动(Activity) 转成 NewLife 埋点,上报到 <see cref="ITracer"/></summary>
/// <remarks>
/// <para>很多开源组件原生输出 OpenTelemetry 埋点(<see cref="ActivitySource"/>)。本采集器通过全局
/// <see cref="ActivityListener"/> 订阅这些活动,转换成 <see cref="ISpan"/> 后交给 <see cref="Tracer"/>,
/// 从而在接入星尘监控的应用里也能看到第三方组件的调用链。</para>
/// <para>与 <see cref="DefaultSpan"/> 的聚合模型一致,采集到的活动按“源名:操作名”聚合统计,
/// 只有少量样本保留完整链路标识,因此不会把海量活动逐条上网。</para>
/// <para>为避免重复采集,<see cref="ExcludeSources"/> 默认排除已经由 DiagnosticListener 或 NewLife 原生埋点
/// 覆盖的源(HttpClient、SqlClient、EF Core、gRPC 等)。只采集 OpenTelemetry 埋点的场景,
/// 可以把 <see cref="IncludeSources"/> 设为白名单。</para>
/// </remarks>
/// <example>
/// <code>
/// var star = new StarTracer("http://star:5000");
/// var collector = new OpenTelemetryCollector { Tracer = star }.Attach();
///
/// // 只采集指定源
/// var collector2 = new OpenTelemetryCollector
/// {
///     Tracer = star,
///     ExcludeSources = null,
///     IncludeSources = ["StackExchange.Redis", "Npgsql"],
/// }.Attach();
/// </code>
/// </example>
public sealed class OpenTelemetryCollector : DisposeBase
{
    #region 属性
    /// <summary>目标跟踪器。采集到的活动将转成埋点交给它</summary>
    public ITracer? Tracer { get; set; }

    /// <summary>不采集的源名。支持星号通配,默认排除已被 DiagnosticListener 或原生生埋点覆盖的源;设为空表示不排除</summary>
    /// <remarks>这些来源在 .NET 中已经由 <c>DiagnosticListener</c> 事件或 NewLife 原生埋点覆盖,
    /// 若此处重复采集,会导致星尘平台上的埋点计数翻倍。</remarks>
    public IList<String>? ExcludeSources { get; set; } = [
        "System.Net.Http",
        "System.Net.Sockets",
        "System.Net.NameResolution",
        "Experimental.System.Net.*",
        "System.Data.SqlClient",
        "Microsoft.Data.SqlClient",
        "Microsoft.EntityFrameworkCore*",
        "Grpc.Net.Client",
        "MongoDB.Driver",
    ];

    /// <summary>只采集的源名白名单。支持星号通配,为空表示不限</summary>
    public IList<String>? IncludeSources { get; set; }

    private ActivityListener? _listener;
    #endregion

    #region 方法
    /// <summary>挂载全局活动监听</summary>
    /// <returns></returns>
    public OpenTelemetryCollector Attach()
    {
        if (_listener != null) return this;

        var listener = new ActivityListener
        {
            ShouldListenTo = ShouldListenTo,
            Sample = Sample,
            ActivityStopped = OnActivityStopped,
        };
        ActivitySource.AddActivityListener(listener);

        _listener = listener;

        return this;
    }

    /// <summary>是否监听该活动源</summary>
    /// <param name="source">活动源</param>
    /// <returns></returns>
    public Boolean ShouldListenTo(ActivitySource source)
    {
        var name = source?.Name;
        if (name.IsNullOrEmpty()) return false;

        var includes = IncludeSources;
        if (includes != null && includes.Count > 0)
        {
            foreach (var item in includes)
            {
                // IsMatch 的接收者是表达式,入参才是待匹配的字符串
                if (!item.IsNullOrEmpty() && item.IsMatch(name, StringComparison.OrdinalIgnoreCase)) return true;
            }

            return false;
        }

        var excludes = ExcludeSources;
        if (excludes != null)
        {
            foreach (var item in excludes)
            {
                if (!item.IsNullOrEmpty() && item.IsMatch(name, StringComparison.OrdinalIgnoreCase)) return false;
            }
        }

        return true;
    }

    private static ActivitySamplingResult Sample(ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.AllDataAndRecorded;

    private void OnActivityStopped(Activity activity)
    {
        var tracer = Tracer;
        if (tracer == null || activity == null) return;

        // 有源名才纳入,无源活动多来自 DiagnosticListener 路径
        var source = activity.Source?.Name;
        if (source.IsNullOrEmpty()) return;

        var name = $"{source}:{activity.DisplayName}";

        var span = new DefaultSpan(tracer)
        {
            // 活动采用 W3C 标识,直接沿用,保证与 OpenTelemetry 链路一致
            Name = name,
            Id = activity.SpanId.ToHexString(),
            TraceId = activity.TraceId.ToHexString(),
            ParentId = activity.ParentSpanId.ToHexString(),
            StartTime = (Int64)(activity.StartTimeUtc - DateTime.UnixEpoch).TotalMilliseconds,
            EndTime = (Int64)(activity.StartTimeUtc - DateTime.UnixEpoch).TotalMilliseconds + (Int64)activity.Duration.TotalMilliseconds,

            // 强制采样,确保链路样本被保留,从而可以在星尘平台还原调用链
            TraceFlag = 1,
        };

        if (activity.GetTagItem("newlife.value") is Object value && value is IConvertible)
        {
            try
            {
                span.Value = value.ToInt();
            }
            catch { }
        }

        if (activity.Status == ActivityStatusCode.Error)
        {
            span.Error = activity.StatusDescription;
            if (span.Error.IsNullOrEmpty()) span.Error = "Error";
        }

        span.Tag = BuildTag(activity, tracer.MaxTagLength);

        // 不走 Start/Dispose 生命周期,避免污染 DefaultSpan.Current
        tracer.BuildSpan(name).Finish(span);
    }

    /// <summary>把活动属性拼装成数据标签</summary>
    /// <param name="activity">活动</param>
    /// <param name="maxLength">最大长度</param>
    /// <returns></returns>
    private static String? BuildTag(Activity activity, Int32 maxLength)
    {
        if (maxLength <= 0) return null;

        var sb = Pool.StringBuilder.Get();
        foreach (var item in activity.TagObjects)
        {
            if (sb.Length > 0) sb.Append("\r\n");
            sb.Append(item.Key).Append(" = ").Append(item.Value);
        }

        var str = sb.Return(true);

        // 异常事件单独补充,便于追溯错误
        var events = activity.Events;
        if (events != null)
        {
            foreach (var item in events)
            {
                if (item.Name != "exception") continue;

                str += "\r\n" + item.Name;

                var tags = item.Tags;
                if (tags == null) continue;

                foreach (var tag in tags) str += $"\r\n  {tag.Key} = {tag.Value}";
            }
        }

        return str.IsNullOrEmpty() ? null : str.Cut(maxLength);
    }
    #endregion

    #region 构造
    /// <summary>销毁,注销全局活动监听</summary>
    /// <param name="disposing"></param>
    protected override void Dispose(Boolean disposing)
    {
        base.Dispose(disposing);

        _listener?.Dispose();
        _listener = null;
    }
    #endregion
}
#endif