Stardust/Stardust.Extensions/TracerMiddleware.cs

272 lines
11 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using NewLife;
using NewLife.Collections;
using NewLife.Log;
using NewLife.Web;
using Stardust.Monitors;
using HttpContext = Microsoft.AspNetCore.Http.HttpContext;
namespace Stardust.Extensions;
/// <summary>性能跟踪中间件</summary>
public class TracerMiddleware
{
private readonly RequestDelegate _next;
/// <summary>跟踪器</summary>
public static ITracer? Tracer { get; set; }
/// <summary>实例化</summary>
/// <param name="next"></param>
public TracerMiddleware(RequestDelegate next) => _next = next ?? throw new ArgumentNullException(nameof(next));
/// <summary>调用</summary>
/// <param name="ctx"></param>
/// <returns></returns>
public async Task Invoke(HttpContext ctx)
{
//!! 以下代码不能封装为独立方法因为有异步存在代码被拆分为状态机导致这里建立的埋点span无法关联页面接口内的下级埋点
ISpan? span = null;
var action = "";
var resolver = Tracer?.Resolver as StarTracerResolver;
if (Tracer != null && !ctx.WebSockets.IsWebSocketRequest)
{
action = GetAction(ctx);
if (!action.IsNullOrEmpty())
{
// 请求主体作为强制采样的数据标签,便于分析链路
var req = ctx.Request;
span = Tracer.NewSpan(action);
span.Tag = $"{ctx.GetUserHost()} {req.Method} {req.GetRawUrl()}";
span.Detach(req.Headers);
span.Value = req.ContentLength ?? 0;
if (span is DefaultSpan ds && ds.TraceFlag > 0)
{
var flag = false;
var size = resolver?.RequestTagLength ?? 1024;
if (resolver != null && resolver.RequestContentAsTag &&
req.ContentLength != null &&
req.ContentLength < size &&
req.ContentType != null &&
req.ContentType.StartsWithIgnoreCase(TagTypes))
{
var buf = Pool.Shared.Rent(size);
try
{
req.EnableBuffering();
var count = await req.Body.ReadAsync(buf, 0, size).ConfigureAwait(false);
if (count > 0)
{
span.AppendTag("\r\n<=\r\n" + buf.ToStr(null, 0, count));
flag = true;
}
req.Body.Position = 0;
}
catch (Exception ex)
{
XTrace.Log.Error("[{0}]读取请求主体失败:{1}", action, ex.Message);
}
finally
{
Pool.Shared.Return(buf);
}
}
if (span.Tag.Length < 500)
{
if (!flag) span.AppendTag("\r\n<=");
var vs = req.Headers.Where(e => !e.Key.EqualIgnoreCase(ExcludeHeaders)).ToDictionary(e => e.Key, e => e.Value + "");
span.AppendTag("\r\n" + vs.Join(Environment.NewLine, e => $"{e.Key}:{e.Value}"));
}
else if (!flag)
{
span.AppendTag("\r\n<=\r\n");
span.AppendTag($"ContentLength: {req.ContentLength}\r\n");
span.AppendTag($"ContentType: {req.ContentType}");
}
}
}
}
try
{
await _next.Invoke(ctx).ConfigureAwait(false);
// 自动记录用户访问主机地址
SaveServiceAddress(ctx);
// 根据状态码识别异常
if (span != null)
{
var res = ctx.Response;
span.Value += res.ContentLength ?? 0;
var code = res.StatusCode;
if (code == 400 || code > 404)
span.SetError(new HttpRequestException($"Http Error {code} {(HttpStatusCode)code}"), null);
else if (code == 200)
{
if (span is DefaultSpan ds && ds.TraceFlag > 0 && (span.Tag == null || span.Tag.Length < 500))
{
var flag = false;
var size = resolver?.RequestTagLength ?? 1024;
if (resolver != null && resolver.RequestContentAsTag &&
res.ContentLength != null &&
res.ContentLength < size &&
res.Body.CanSeek &&
res.ContentType != null &&
res.ContentType.StartsWithIgnoreCase(TagTypes))
{
var buf = Pool.Shared.Rent(size);
try
{
var p = res.Body.Position;
var count = await res.Body.ReadAsync(buf, 0, size).ConfigureAwait(false);
if (count > 0)
{
span.AppendTag("\r\n=>\r\n" + buf.ToStr(null, 0, count));
flag = true;
}
res.Body.Position = p;
}
catch (Exception ex)
{
XTrace.Log.Error("[{0}]读取响应主体失败:{1}", action, ex.Message);
}
finally
{
Pool.Shared.Return(buf);
}
}
if (span.Tag == null || span.Tag.Length < 500)
{
if (!flag) span.AppendTag("\r\n=>");
var vs = res.Headers.Where(e => !e.Key.EqualIgnoreCase(ExcludeHeaders)).ToDictionary(e => e.Key, e => e.Value + "");
span.AppendTag("\r\n" + vs.Join(Environment.NewLine, e => $"{e.Key}:{e.Value}"));
}
else if (!flag)
{
span.AppendTag("\r\n=>\r\n");
span.AppendTag($"ContentLength: {res.ContentLength}\r\n");
span.AppendTag($"ContentType: {res.ContentType}");
}
}
}
else if (code == 404)
{
// 取消404找不到路径的埋点避免TraceItem过多
span?.Abandon();
}
}
}
catch (Exception ex)
{
//if (span != null)
//{
// // 接口抛出ApiException时认为是正常业务行为埋点不算异常
// if (ex is ApiException)
// span.Tag ??= ex.Message;
// else
// span.SetError(ex, null);
//}
// 捕获所有未处理异常即使是ApiException也应该在接口层包装而不是继续向外抛出异常
span?.SetError(ex, null);
throw;
}
finally
{
span?.Dispose();
}
}
/// <summary>支持作为标签数据的内容类型</summary>
public static String[] TagTypes { get; set; } = [
"text/plain", "text/xml", "application/json", "application/xml", "application/x-www-form-urlencoded"
];
/// <summary>忽略的头部</summary>
public static String[] ExcludeHeaders { get; set; } = ["traceparent", "Authorization", "Cookie"];
/// <summary>忽略的后缀</summary>
public static String[] ExcludeSuffixes { get; set; } = [
".html", ".htm", ".js", ".css", ".map", ".png", ".jpg", ".gif", ".ico", // 脚本样式图片
".woff", ".woff2", ".svg", ".ttf", ".otf", ".eot" // 字体
];
private static readonly String[] CubeActions = ["index", "detail", "add", "edit", "delete", "deleteSelect", "deleteAll", "ExportCsv", "Info", "SetEnable", "EnableSelect", "DisableSelect", "DeleteSelect"];
private static String? GetAction(HttpContext ctx)
{
var p = ctx.Request.Path + "";
if (p.EndsWithIgnoreCase(ExcludeSuffixes)) return null;
var ss = p.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (ss.Length == 0) return p;
// 如果是魔方格式保留3段其它webapi接口只留2段
if (ss.Length >= 3 && ss[2].EqualIgnoreCase(CubeActions))
p = "/" + ss.Take(3).Join("/");
else
p = "/" + ss.Take(2).Join("/");
return p;
}
/// <summary>自动记录用户访问主机地址</summary>
/// <param name="ctx"></param>
public static void SaveServiceAddress(HttpContext ctx)
{
var uri = ctx.Request.GetRawUrl();
if (uri == null) return;
var host = uri.Authority;
if (host.IsNullOrEmpty()) return;
// 排除本机地址
var p = host.LastIndexOf(':');
if (p >= 0) host = host[..p];
if (host.EqualIgnoreCase("127.0.0.1", "localhost", "[::1]")) return;
if (host.StartsWith("127.0.")) return;
var baseAddress = $"{uri.Scheme}://{host}";
if (uri.Port > 0)
{
if (uri.Scheme == "http" && uri.Port != 80)
baseAddress += ":" + uri.Port;
else if (uri.Scheme == "https" && uri.Port != 443)
baseAddress += ":" + uri.Port;
}
var set = NewLife.Setting.Current;
var ss = set.ServiceAddress?.Split(",").ToList() ?? [];
if (!ss.Contains(baseAddress))
{
var newAddrs = new List<String> { baseAddress };
// 过滤掉本机地址
foreach (var item in ss)
{
var addr = item;
if (!Uri.TryCreate(addr, UriKind.Absolute, out var u)) continue;
if (u.Host.EqualIgnoreCase("127.0.0.1", "localhost", "[::1]")) continue;
if (u.Host.StartsWith("127.0.")) continue;
if (u.Port == 0) continue;
addr = u.ToString();
newAddrs.Add(addr);
}
set.ServiceAddress = newAddrs.Take(5).Join(",");
set.Save();
}
}
}