[feat]调用链页面,增加日志详情页,方便查找调用链中的日志
大石头 authored at 2024-08-02 16:10:50
11.82 KiB
Stardust
using Microsoft.AspNetCore.Mvc;
using NewLife;
using NewLife.Cube;
using NewLife.Web;
using Stardust.Data.Monitors;
using Stardust.Web.Models;
using XCode.Membership;

namespace Stardust.Web.Controllers;

public class TraceController : ControllerBaseX
{
    [Route("[controller]")]
    public ActionResult Index(String id, Pager pager)
    {
        if (id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(id));

        // id可能不是traceId,而是traceParent
        var ss = id.Split('-');
        if (ss.Length == 4) id = ss[1];

        var layout = pager["layout"];

        var list = Search(id, pager);

        if (list.Count > 0)
        {
            var appName = list[0].AppName;
            if (appName.IsNullOrEmpty()) ViewBag.Title = $"{appName}调用链";
        }

        PageSetting.EnableNavbar = false;

        var model = new TraceViewModel
        {
            Page = pager,
            Data = list
        };

        if (layout.EqualIgnoreCase("detail"))
            return View("Detail", model);
        else
            return View("Index", model);
    }

    [Route("[action]")]
    public ActionResult Graph(String id, Pager pager)
    {
        if (id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(id));

        // id可能不是traceId,而是traceParent
        var ss = id.Split('-');
        if (ss.Length == 4) id = ss[1];

        var list = Search(id, pager);

        // 每个应用的第一个节点,必须以App分类展示,其它节点才可以定制化
        var appids = new List<Int32>();

        // 解析得到关系数据
        var cats = new List<String>();
        //var cats = new List<String> { "App", "http", "db", "redis" };
        var nodes = new List<GraphNode>();
        var links = new Dictionary<String, GraphLink>();
        foreach (var item in list)
        {
            var cat = "App";
            var name = item.AppName;
            var ti = item.TraceItem;
            if (appids.Contains(item.AppId) && ti != null && ti.Kind.EqualIgnoreCase("http", "db", "redis", "mq", "mqtt", "modbus", "biz", "other"))
            {
                cat = ti.Kind;
                name = ti.DisplayName ?? ti.Name;
                var ns = name.Split(':');

                // 特殊处理
                switch (ti.Kind)
                {
                    case "http":
                        var p = name.IndexOf('/', "https://".Length);
                        name = p > 0 ? name[..p] : name;
                        break;
                    case "db":
                        if (ns.Length >= 2) name = ns[1];
                        break;
                    case "mq":
                        if (ns.Length >= 2) name = $"{ns[0]}:{ns[1]}";
                        if (ns.Length >= 3 && ns[0] == "redismq" && ns[1] == "Add")
                            name = $"{ns[0]}:{ns[2]}";
                        else if (ns.Length >= 4 && ns[0] == "redismq" && ns[2] == "Add")
                            name = $"{ns[0]}:{ns[3]}";
                        break;
                    case "modbus":
                        name = ns.Length >= 3 ? ns[1] : "IoTDevice";
                        break;
                    default:
                        if (ns.Length >= 2) name = $"{ns[0]}:{ns[1]}";
                        break;
                }
            }
            if (!appids.Contains(item.AppId)) appids.Add(item.AppId);

            name ??= item.AppName;
            item["node_name"] = name;

            // 分类
            var idx = cats.IndexOf(cat);
            if (idx < 0)
            {
                cats.Add(cat);
                idx = cats.Count - 1;
            }

            // 节点
            var node = nodes.FirstOrDefault(e => e.Name == name);
            if (node == null)
            {
                node = new GraphNode
                {
                    Id = item.Id + "",
                    Name = name,
                    Value = item.Cost,
                    Category = idx,

                    SymbolSize = item.Cost,
                    //X = Rand.Next(20, 100),
                    //Y = Rand.Next(20, 100),
                };
                nodes.Add(node);
            }
            else
            {
                node.Value += item.Cost;
                node.SymbolSize += item.Cost;
            }
            item["node_id"] = node.Id;

            // 关系
            var parent = list.FirstOrDefault(e => e.SpanId == item.ParentId);
            if (parent != null)
            {
                var src = parent["node_id"] + "";
                var dst = item["node_id"] + "";
                var key = $"{src}-{dst}";
                if (src != dst && !links.ContainsKey(key)) links.Add(key, new GraphLink { Source = src, Target = dst });
            }
        }

        // 处理图标大小
        var maxCost = nodes.Count == 0 ? 0 : nodes.Max(e => e.Value);
        var minCost = nodes.Count == 0 ? 0 : nodes.Min(e => e.Value);
        var len = maxCost - minCost;
        if (len <= 0) len = 1;
        foreach (var node in nodes)
        {
            var cost = (Int32)Math.Round(100 * (Double)(node.Value - minCost) / (maxCost - minCost));
            node.SymbolSize = cost < 40 ? 40 : cost;
        }

        // 分类图标
        var cts = new List<GraphCategory>();
        foreach (var item in cats)
        {
            var cat = new GraphCategory
            {
                Name = item,
            };
            cat.Symbol = item switch
            {
                "App" => "image:///icons/app.svg",
                "http" => "image:///icons/http.svg",
                "db" => "image:///icons/db.svg",
                "redis" => "image:///icons/redis.svg",
                "mq" => "image:///icons/mq.svg",
                "mqtt" => "image:///icons/mqtt.svg",
                "modbus" => "image:///icons/modbus.svg",
                _ => "circle",
            };
            cts.Add(cat);
        }

        var model = new GraphViewModel
        {
            TraceId = id,
            Title = "关系图",
            Layout = pager["layout"],
            Categories = cts.ToArray(),
            Links = links.Values.ToArray(),
            Nodes = nodes.ToArray(),
        };
        if (model.Layout.IsNullOrEmpty()) model.Layout = "force";

        if (list.Count > 0)
        {
            var appName = list[0].AppName;
            if (appName.IsNullOrEmpty()) model.Title = $"{appName}关系图";
        }

        PageSetting.EnableNavbar = false;

        return View(model);
    }

    private IList<SampleData> Search(String traceId, Pager p)
    {
        // 指定追踪标识后,分页500
        if (!traceId.IsNullOrEmpty())
        {
            if (p.PageSize == 20) p.PageSize = 10_000;
        }
        //if (p.Sort.IsNullOrEmpty()) p.OrderBy = SampleData._.Id.Desc();

        var traceIds = traceId.Split(',').Distinct().ToArray();

        var start = DateTime.Today.AddDays(-30);
        var end = DateTime.Today;
        var list = SampleData.Search(-1, traceIds, null, start, end, p);
        if (list.Count == 0)
        {
            // 如果是查看调用链,去备份表查一下
            var list2 = SampleData2.Search(traceIds, null, p);
            if (list2.Count > 0)
            {
                foreach (var item in list2)
                {
                    var entity = new SampleData();
                    entity.CopyFrom(item);

                    list.Add(entity);
                }
            }

            if (list.Count == 0) return list;
        }
        else
        {
            // 为临近边界的数据,查前一天
            var first = list.OrderBy(e => e.Id).First().CreateTime;
            if (first.Year > 2000 && first.Hour == 0 && first.Minute == 0 && first.Second <= 5)
            {
                var date = first.Date.AddDays(-1);
                var list2 = SampleData.Search(-1, traceIds, null, date, date, p);
                if (list2.Count > 0) (list as List<SampleData>).AddRange(list2);
            }

            if (list.Count < p.PageSize)
            {
                var user = ManageProvider.User;
                foreach (var item in traceIds)
                {
                    // 备份调用链,用于将来查询
                    SampleData2.Backup(item, list, user?.ID ?? 0, user + "");
                }
            }
        }

        // 记录最后一次访问,避免TraceId过多导致字段溢出
        if (list.Count > 0)
        {
            var ti = list[0].TraceItem;
            if (ti != null)
            {
                ti.TraceId = traceIds.Take(5).Join(",");
                ti.Update();
            }
        }

        // 如果有traceId,则按照要求排序,深度搜索算法
        if (list.Count > 0)
        {
            var rs = new List<SampleData>();
            var stack = new Stack<SampleData>();

            // 整理排序,为了配合栈计算,采用反向顺序
            var list2 = list.OrderByDescending(e => e.StartTime).ThenByDescending(e => e.SpanId).ToList();

            // 找到所有parentId,包括空字符串,首次出现的parentId优先
            var pids = list2.Select(e => e.ParentId + "").Distinct().ToArray();
            // 找到顶级parentId,它所对应的span不存在,包括空字符串
            foreach (var item in pids.Where(e => !list2.Any(y => y.SpanId == e)))
            {
                // 这些parentId的子级,按照时间降序后入栈,它们作为一级树
                foreach (var elm in list2.Where(e => e.ParentId + "" == item))
                {
                    stack.Push(elm);
                }
            }
            foreach (var item in stack)
            {
                list2.Remove(item);
            }

            ////!!! 判断每个应用的第一次出现,根据父子关系,整体调整应用的时间偏移
            //// 每个应用的时间偏移量
            //var dic = new Dictionary<Int32, Int32>();

            // 依次弹出parentId,深度搜索
            var parent = stack.Pop();
            rs.Add(parent);
            var pid = parent.SpanId;
            while (true)
            {
                // 当前span的下级,按时间降序入栈
                var ps = list2.Where(e => e.ParentId + "" == pid).ToList();
                foreach (var item in ps)
                {
                    stack.Push(item);
                    list2.Remove(item);

                    // 深度
                    item.Level = parent.Level + 1;

                    //// 如果子级时间小于父级,可能是跨应用时间差,强行调整
                    //if (parent.AppId != item.AppId)
                    //{
                    //    if (!dic.TryGetValue(parent.AppId, out var parentTs)) parentTs = 0;

                    //    // 负数合理,正数不合理
                    //    var ts = (Int32)(parent.StartTime - item.StartTime);
                    //    if (ts > 0)
                    //    {
                    //        if (!dic.TryGetValue(item.AppId, out var itemTs) || ts > itemTs)
                    //            dic[item.AppId] = itemTs = ts + parentTs;
                    //    }
                    //}
                }

                // 没有数据,跳出
                if (stack.Count == 0) break;

                // 出栈,加入结果,处理它的下级
                if (stack.TryPop(out parent))
                {
                    rs.Add(parent);
                    pid = parent.SpanId;
                }
            }

            // 残留的异常数据
            rs.AddRange(list2);

            //// 各应用整体后移
            //foreach (var item in rs)
            //{
            //    if (dic.TryGetValue(item.AppId, out var ts))
            //    {
            //        item.StartTime += ts;
            //        item.EndTime += ts;
            //    }
            //}

            list = rs;
        }

        return list;
    }
}