解决MySql布尔型新旧版本兼容问题,采用枚举来表示布尔型的数据表。由正向工程赋值
|
# 链路追踪ITracer
## 概述
å¯è§‚测性是衡é‡çŽ°ä»£åº”ç”¨è´¨é‡çš„æ ¸å¿ƒæŒ‡æ ‡ä¹‹ä¸€ã€‚NewLife.Core æä¾›äº†ä¸€å¥—完整的链路追踪规范:`ITracer`/`ISpan`,用于实现轻é‡çº§ APM(Application Performance Monitoring)。
ä¸Žä¼ ç»Ÿ APM 系统ä¸åŒï¼ŒNewLife 的链路追踪采用**æœ¬åœ°é‡‡æ ·+统计**的方å¼ï¼š
- åœ¨æœ¬åœ°å®Œæˆæ•°æ®é‡‡æ ·å’Œåˆæ¥ç»Ÿè®¡
- 仅上报统计数æ®å’Œå°‘é‡é‡‡æ ·æ ·æœ¬
- æžå¤§èŠ‚çœç½‘ç»œä¼ è¾“å’Œå˜å‚¨æˆæœ¬
NewLife 全系列项目(30+ 组件)å‡å†…置了 `ITracer` 埋点,开å‘者å¯ä»¥ï¼š
1. æ— ä¾µå…¥åœ°èŽ·å–组件è¿è¡ŒæŒ‡æ ‡
2. 接入星尘监控平å°å®žçŽ°åˆ†å¸ƒå¼è¿½è¸ª
3. 基于规范扩展自定义埋点
**Nuget 包**: `NewLife.Core`
**æºç **: [NewLife.Core/Log/ITracer.cs](https://github.com/NewLifeX/X/blob/master/NewLife.Core/Log/ITracer.cs)
---
## 快速入门
### 基础用法
最简å•的埋点示例:
```csharp
using var span = tracer?.NewSpan("æ“作åç§°");
```
使用 `using` 关键å—ç¡®ä¿ span åœ¨ä½œç”¨åŸŸç»“æŸæ—¶è‡ªåŠ¨å®Œæˆï¼Œè®°å½•耗时和次数。
### 完整示例
以下是网络接收数æ®çš„埋点示例:
```csharp
private void Ss_Received(Object? sender, ReceivedEventArgs e)
{
var ns = (this as INetSession).Host;
var tracer = ns?.Tracer;
// 创建埋点,记录接收æ“作
using var span = tracer?.NewSpan($"net:{ns?.Name}:Receive", e.Message);
try
{
OnReceive(e);
}
catch (Exception ex)
{
// æ ‡è®°é”™è¯¯å¹¶è®°å½•å¼‚å¸¸ä¿¡æ¯
span?.SetError(ex, e.Message ?? e.Packet);
throw;
}
}
```
这个示例演示了:
- **创建埋点**:使用动æ€ç”Ÿæˆçš„åç§°
- **æ•°æ®æ ‡ç¾**ï¼šç¬¬äºŒä¸ªå‚æ•° `e.Message` ä½œä¸ºæ•°æ®æ ‡ç¾
- **异常处ç†**:通过 `SetError` æ ‡è®°å¼‚å¸¸åŸ‹ç‚¹
- **自动记录**:`using` è¯å¥è‡ªåŠ¨è®°å½•è€—æ—¶
通过这个埋点,我们å¯ä»¥ï¼š
- 统计接收数æ®åŒ…的次数
- è®°å½•æ¯æ¬¡æŽ¥æ”¶çš„耗时
- 统计异常å‘生次数
- æŸ¥çœ‹é‡‡æ ·æ•°æ®å’Œæ ‡ç¾
---
## æ ¸å¿ƒæŽ¥å£
### ITracer 接å£
性能跟踪器,轻é‡çº§ APM è§„èŒƒçš„æ ¸å¿ƒæŽ¥å£ã€‚
```csharp
public interface ITracer
{
#region 属性
/// <summary>é‡‡æ ·å‘¨æœŸã€‚å•ä½ç§’,默认15ç§’</summary>
Int32 Period { get; set; }
/// <summary>最大æ£å¸¸é‡‡æ ·æ•°ã€‚é‡‡æ ·å‘¨æœŸå†…ï¼Œæœ€å¤šåªè®°å½•指定数é‡çš„æ£å¸¸äº‹ä»¶ï¼Œé»˜è®¤1</summary>
Int32 MaxSamples { get; set; }
/// <summary>æœ€å¤§å¼‚å¸¸é‡‡æ ·æ•°ã€‚é‡‡æ ·å‘¨æœŸå†…ï¼Œæœ€å¤šåªè®°å½•指定数é‡çš„异常事件,默认10</summary>
Int32 MaxErrors { get; set; }
/// <summary>è¶…æ—¶æ—¶é—´ã€‚è¶…è¿‡è¯¥æ—¶é—´æ—¶å¼ºåˆ¶é‡‡æ ·ï¼Œå•使¯«ç§’,默认5000</summary>
Int32 Timeout { get; set; }
/// <summary>æœ€å¤§æ ‡ç¾é•¿åº¦ã€‚超过该长度时将截æ–,默认1024å—符</summary>
Int32 MaxTagLength { get; set; }
/// <summary>å‘http/rpc请求注入TraceIdçš„å‚æ•°åï¼Œä¸ºç©ºè¡¨ç¤ºä¸æ³¨å…¥ï¼Œé»˜è®¤W3Cæ ‡å‡†çš„traceparent</summary>
String? AttachParameter { get; set; }
#endregion
/// <summary>建立Span构建器</summary>
/// <param name="name">æ“作åç§°ï¼Œç”¨äºŽæ ‡è¯†ä¸åŒçš„埋点类型</param>
ISpanBuilder BuildSpan(String name);
/// <summary>开始一个Span</summary>
/// <param name="name">æ“作åç§°ï¼Œç”¨äºŽæ ‡è¯†ä¸åŒçš„埋点类型</param>
ISpan NewSpan(String name);
/// <summary>开始一个Spanï¼ŒæŒ‡å®šæ•°æ®æ ‡ç¾</summary>
/// <param name="name">æ“作åç§°ï¼Œç”¨äºŽæ ‡è¯†ä¸åŒçš„埋点类型</param>
/// <param name="tag">æ•°æ®æ ‡ç¾ï¼Œè®°å½•关键傿•°ä¿¡æ¯</param>
ISpan NewSpan(String name, Object? tag);
/// <summary>æˆªæ–æ‰€æœ‰Span构建器数æ®ï¼Œé‡ç½®é›†åˆ</summary>
ISpanBuilder[] TakeAll();
}
```
#### 关键属性说明
| 属性 | 默认值 | 说明 |
|-----|-------|------|
| `Period` | 15ç§’ | é‡‡æ ·å‘¨æœŸï¼Œå®šæœŸä¸ŠæŠ¥æ•°æ®çš„æ—¶é—´é—´éš” |
| `MaxSamples` | 1 | æ¯ä¸ªå‘¨æœŸå†…记录的æ£å¸¸é‡‡æ ·æ•°ï¼Œç”¨äºŽç»˜åˆ¶è°ƒç”¨ä¾èµ–关系 |
| `MaxErrors` | 10 | æ¯ä¸ªå‘¨æœŸå†…è®°å½•çš„å¼‚å¸¸é‡‡æ ·æ•°ï¼Œç”¨äºŽé—®é¢˜è¯Šæ– |
| `Timeout` | 5000ms | è¶…æ—¶é˜ˆå€¼ï¼Œè¶…è¿‡æ¤æ—¶é—´çš„æ“ä½œä¼šè¢«å¼ºåˆ¶é‡‡æ · |
| `MaxTagLength` | 1024 | æ ‡ç¾æœ€å¤§é•¿åº¦ï¼Œè¶…è¿‡ä¼šè¢«æˆªæ– |
| `AttachParameter` | "traceparent" | HTTP/RPC è¯·æ±‚ä¸æ³¨å…¥ TraceId çš„å‚æ•°å,éµå¾ª W3C æ ‡å‡† |
è¿™äº›å‚æ•°é€šå¸¸ç”±æ˜Ÿå°˜ç›‘控ä¸å¿ƒåЍæ€ä¸‹å‘è°ƒæ•´ï¼Œæ— éœ€æ‰‹åŠ¨é…置。
#### æ ¸å¿ƒæ–¹æ³•
- **`NewSpan(String name)`**: 最常用的方法,创建一个新埋点
- **`NewSpan(String name, Object? tag)`**: åˆ›å»ºåŸ‹ç‚¹å¹¶é™„åŠ æ•°æ®æ ‡ç¾
- **`BuildSpan(String name)`**: èŽ·å–æˆ–创建 SpanBuilder,用于高级场景
### ISpan 接å£
性能跟踪片段,代表一个具体的埋点实例。
```csharp
public interface ISpan : IDisposable
{
/// <summary>å”¯ä¸€æ ‡è¯†ã€‚éšçº¿ç¨‹ä¸Šä¸‹æ–‡ã€Httpã€Rpcä¼ é€’ï¼Œä½œä¸ºå†…éƒ¨ç‰‡æ®µçš„çˆ¶çº§</summary>
String Id { get; set; }
/// <summary>埋点åã€‚ç”¨äºŽæ ‡è¯†ä¸åŒç±»åž‹çš„æ“ä½œ</summary>
String Name { get; set; }
/// <summary>çˆ¶çº§ç‰‡æ®µæ ‡è¯†ã€‚ç”¨äºŽæž„å»ºè°ƒç”¨é“¾</summary>
String? ParentId { get; set; }
/// <summary>è·Ÿè¸ªæ ‡è¯†ã€‚å¯ç”¨äºŽå…³è”多个片段,建立ä¾èµ–关系,éšçº¿ç¨‹ä¸Šä¸‹æ–‡ã€Httpã€Rpcä¼ é€’</summary>
String TraceId { get; set; }
/// <summary>开始时间。Unix毫秒时间戳</summary>
Int64 StartTime { get; set; }
/// <summary>ç»“æŸæ—¶é—´ã€‚Unix毫秒时间戳</summary>
Int64 EndTime { get; set; }
/// <summary>用户数值。记录数å—åž‹æ ‡é‡ï¼Œå¦‚æ¯æ¬¡æ•°æ®åº“æ“ä½œè¡Œæ•°ï¼Œæ˜Ÿå°˜å¹³å°æ±‡æ€»ç»Ÿè®¡</summary>
Int64 Value { get; set; }
/// <summary>æ•°æ®æ ‡ç¾ã€‚è®°å½•ä¸€äº›é™„åŠ æ•°æ®ï¼Œå¦‚è¯·æ±‚å‚æ•°ã€å“应结果ç‰</summary>
String? Tag { get; set; }
/// <summary>错误信æ¯ã€‚记录异常消æ¯</summary>
String? Error { get; set; }
/// <summary>设置错误信æ¯ï¼ŒApiException除外</summary>
void SetError(Exception ex, Object? tag = null);
/// <summary>è®¾ç½®æ•°æ®æ ‡ç¾ã€‚å†…éƒ¨æ ¹æ®æœ€å¤§é•¿åº¦æˆªæ–</summary>
void SetTag(Object tag);
/// <summary>抛弃埋点,ä¸è®¡å…¥é‡‡é›†</summary>
void Abandon();
}
```
#### 关键属性
| 属性 | 类型 | 说明 |
|-----|------|------|
| `Id` | String | Span å”¯ä¸€æ ‡è¯†ï¼Œéµå¾ª W3C æ ‡å‡† |
| `ParentId` | String? | 父级 Span IDï¼Œç”¨äºŽæž„å»ºè°ƒç”¨æ ‘ |
| `TraceId` | String | è·Ÿè¸ªé“¾æ ‡è¯†ï¼ŒåŒä¸€è°ƒç”¨é“¾ä¸çš„æ‰€æœ‰ Span å…±äº«ç›¸åŒ TraceId |
| `StartTime` | Int64 | 开始时间(Unix 毫秒) |
| `EndTime` | Int64 | ç»“æŸæ—¶é—´ï¼ˆUnix 毫秒) |
| `Value` | Int64 | 用户数值,å¯è®°å½•ä¸šåŠ¡æŒ‡æ ‡ï¼ˆå¦‚æ•°æ®åº“行数) |
| `Tag` | String? | æ•°æ®æ ‡ç¾ï¼Œè®°å½•è¯·æ±‚å‚æ•°ã€å“应数æ®ç‰ |
| `Error` | String? | é”™è¯¯ä¿¡æ¯ |
#### æ ¸å¿ƒæ–¹æ³•
- **`SetError(Exception ex, Object? tag)`**: æ ‡è®°å¼‚å¸¸ï¼Œ`ApiException` 类型会被特殊处ç†
- **`SetTag(Object tag)`**: è®¾ç½®æ•°æ®æ ‡ç¾ï¼Œæ”¯æŒå¤šç§ç±»åž‹è‡ªåЍåºåˆ—化
- **`Abandon()`**: 丢弃当å‰åŸ‹ç‚¹ï¼Œå¸¸ç”¨äºŽè¿‡æ»¤æ— 效请求(如404扫æï¼‰ã€‚判定æ¡ä»¶ä¸è¦åªçœ‹çжæ€ç ,业务接å£ä¸»åŠ¨è¿”å›žçš„404有观测价值,应结åˆâ€œæ˜¯å¦å‘½ä¸è·¯ç”±ç»ˆç»“点â€åˆ¤æ–
---
## 使用最佳实践
### 1. 注入 ITracer
在 ASP.NET Core 应用ä¸ï¼Œé€šè¿‡æ˜Ÿå°˜æ‰©å±•注入:
```csharp
using NewLife.Stardust.Extensions;
var builder = WebApplication.CreateBuilder(args);
// æ³¨å…¥æ˜Ÿå°˜ï¼ŒåŒ…å« ITracer 实现
builder.Services.AddStardust();
```
详è§ï¼š[æ˜Ÿå°˜åˆ†å¸ƒå¼æœåС平å°](https://newlifex.com/blood/stardust)
### 2. èŽ·å– ITracer 实例
æœ‰å¤šç§æ–¹å¼èŽ·å– `ITracer`:
```csharp
// æ–¹å¼1ï¼šæž„é€ å‡½æ•°æ³¨å…¥ï¼ˆæŽ¨è)
public class MyService
{
private readonly ITracer _tracer;
public MyService(ITracer tracer)
{
_tracer = tracer;
}
}
// æ–¹å¼2:属性注入
public class MyService
{
public ITracer? Tracer { get; set; }
}
// æ–¹å¼3ï¼šä½¿ç”¨å…¨å±€é™æ€å®žä¾‹
var tracer = DefaultTracer.Instance;
```
### 3. 创建埋点
#### 基础埋点
```csharp
using var span = _tracer?.NewSpan("MyOperation");
// 业务逻辑
```
#### å¸¦æ•°æ®æ ‡ç¾çš„埋点
```csharp
var request = new { UserId = 123, Action = "Query" };
using var span = _tracer?.NewSpan("UserQuery", request);
// 业务逻辑
```
#### 带异常处ç†çš„埋点
```csharp
using var span = _tracer?.NewSpan("DatabaseQuery");
try
{
// 执行数æ®åº“查询
var result = await ExecuteQueryAsync();
}
catch (Exception ex)
{
span?.SetError(ex, "查询失败");
throw;
}
```
#### è®°å½•ä¸šåŠ¡æŒ‡æ ‡
```csharp
using var span = _tracer?.NewSpan("BatchProcess");
var count = ProcessRecords();
span.Value = count; // 记录处ç†çš„记录数
```
### 4. 定时任务埋点示例
```csharp
public class DataRetentionService : IHostedService
{
private readonly ITracer _tracer;
private readonly StarServerSetting _setting;
private TimerX? _timer;
public DataRetentionService(StarServerSetting setting, ITracer tracer)
{
_setting = setting;
_tracer = tracer;
}
public Task StartAsync(CancellationToken cancellationToken)
{
// æ¯600ç§’æ‰§è¡Œä¸€æ¬¡ï¼Œéšæœºå»¶è¿Ÿå¯åЍ
_timer = new TimerX(DoWork, null,
DateTime.Today.AddMinutes(Rand.Next(60)),
600 * 1000)
{
Async = true
};
return Task.CompletedTask;
}
private void DoWork(Object? state)
{
var set = _setting;
if (set.DataRetention <= 0) return;
var time = DateTime.Now.AddDays(-set.DataRetention);
var time2 = DateTime.Now.AddDays(-set.DataRetention2);
var time3 = DateTime.Now.AddDays(-set.DataRetention3);
// 创建埋点,记录清ç†ä»»åŠ¡
using var span = _tracer?.NewSpan("DataRetention", new { time, time2, time3 });
try
{
// åˆ é™¤æ—§æ•°æ®
var rs = AppMinuteStat.DeleteBefore(time);
XTrace.WriteLine("åˆ é™¤[{0}]之å‰çš„AppMinuteStat共:{1:n0}", time, rs);
rs = TraceMinuteStat.DeleteBefore(time);
XTrace.WriteLine("åˆ é™¤[{0}]之å‰çš„TraceMinuteStat共:{1:n0}", time, rs);
}
catch (Exception ex)
{
span?.SetError(ex, null);
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_timer?.TryDispose();
return Task.CompletedTask;
}
}
```
### 5. è¿‡æ»¤æ— æ•ˆè¯·æ±‚
在 Web 应用ä¸ï¼Œç»å¸¸é‡åˆ°å„ç§æ‰«æè¯·æ±‚,å¯ä»¥ä½¿ç”¨ `Abandon()` 丢弃这些埋点:
```csharp
public IActionResult ProcessRequest()
{
using var span = _tracer?.NewSpan("WebRequest");
// æ£€æµ‹åˆ°æ— æ•ˆè¯·æ±‚ï¼ˆå¦‚404扫æï¼‰
if (IsInvalidScan())
{
span?.Abandon(); // 丢弃埋点,ä¸è®¡å…¥ç»Ÿè®¡
return NotFound();
}
// æ£å¸¸å¤„ç†
return Ok();
}
```
> åˆ¤å®šâ€œæ— æ•ˆè¯·æ±‚â€ä¸è¦åªçœ‹çжæ€ç :业务接å£ä¸»åŠ¨è¿”å›žçš„404(如查询的资æºä¸å˜åœ¨ï¼‰æ˜¯æ£å¸¸ä¸šåŠ¡è¡Œä¸ºï¼ŒåŸ‹ç‚¹æœ‰è§‚æµ‹ä»·å€¼ï¼›
> è€Œæ‰«ææŽ¢æµ‹è¯·æ±‚ä¸ä¼šå‘½ä¸ä»»ä½•路由终结点。星尘 SDK çš„ `TracerMiddleware` 峿Œ‰ã€Œçжæ€ç 404 且 `ctx.GetEndpoint() == null`ã€
> 判定,并å¯é€šè¿‡ `TracerMiddleware.ShouldAbandon` 接管默认规则,其它框架å¯å‚照该æ€è·¯ã€‚
`Abandon()` 的作用边界:
- **åªä¸¢å¼ƒå½“å‰ç‰‡æ®µ**:åŒä¸€æ¡é“¾è·¯é‡Œç”±å…¶å®ƒè·Ÿè¸ªå™¨ï¼ˆ`DiagnosticListener`ã€`OpenTelemetryCollector`ã€XCode)产生的片段ä¸å—å½±å“ï¼Œéœ€è¦æ•´é“¾è¿‡æ»¤æ—¶åº”在采集端按 TraceId 处ç†ã€‚
- **åªçœä¸ŠæŠ¥ä¸çœåˆ›å»º**:埋点对象ã€è¯·æ±‚å¤´æ ‡ç¾ç‰å·²ç»äº§ç”Ÿï¼Œæ‰«ææ´ªå³°ä¸‹çš„创建开销ä¾ç„¶å˜åœ¨ï¼›è¦åœ¨åˆ›å»ºå‰æ‹¦æˆªï¼Œå¯è®© `ITracer.Resolver` 对探测路径返回空以跳过埋点。
- **星尘档是è½åœ°èƒ½åŠ›**:`DefaultSpan` 会归还片段上下文;OpenTelemetry 档的活动归 SDK 管ç†ï¼Œä¸ä½œä¸ºæ‰¿è¯ºï¼Œè¯¦è§[æ”¶å£è®°å½•](/NewLife/X/Blob/dev/Doc/#æ”¶å£è®°å½•)。
---
## 分布å¼é“¾è·¯è¿½è¸ª
### è·¨æœåŠ¡ä¼ é€’
ISpan æä¾›äº†æ‰©å±•方法,支æŒé€šè¿‡ HTTP/RPC ä¼ é€’è·Ÿè¸ªä¸Šä¸‹æ–‡ï¼š
```csharp
// HTTP 请求注入
var request = new HttpRequestMessage(HttpMethod.Get, url);
span?.Attach(request); // 注入 traceparent 头
await httpClient.SendAsync(request);
// RPC 调用注入
var args = new { UserId = 123 };
span?.Attach(args); // æ³¨å…¥è·Ÿè¸ªå‚æ•°
await rpcClient.InvokeAsync("Method", args);
```
æœåŠ¡ç«¯æ”¶åˆ°è¯·æ±‚åŽï¼Œä¼šè‡ªåŠ¨è§£æž `traceparent` 傿•°ï¼Œæ¢å¤è·Ÿè¸ªä¸Šä¸‹æ–‡ã€‚
### 调用链构建
åŒä¸€ä¸ª `TraceId` 下的所有 Span 会自动形æˆè°ƒç”¨é“¾ï¼š
```
TraceId: ac1b1e8617342790000015eb0ea2a6
├─ DataRetention (父)
├─ SQL:AppMinuteStat.DeleteBefore (å)
├─ SQL:TraceMinuteStat.DeleteBefore (å)
└─ SQL:TraceHourStat.DeleteBefore (å)
```
在星尘监控平å°å¯ä»¥æŸ¥çœ‹ï¼š
- [调用链ç«ç„°å›¾](https://star.newlifex.com/trace?id=ac1b1e8617342790000015eb0ea2a6)
- [调用链日志视图](https://star.newlifex.com/trace?id=ac1b1e8617342790000015eb0ea2a6&layout=detail)
---
## 监控与分æž
### 查看统计数æ®
在星尘监控平å°å¯ä»¥æŸ¥çœ‹åŸ‹ç‚¹ç»Ÿè®¡ï¼š
- 调用总次数
- 异常次数
- å¹³å‡è€—æ—¶ã€æœ€å¤§è€—æ—¶ã€æœ€å°è€—æ—¶
- QPS(æ¯ç§’请求数)
示例:[DataRetention 埋点统计](https://star.newlifex.com/Monitors/traceDayStat?appId=4&itemId=284)
### é‡‡æ ·ç–ç•¥
ITracer é‡‡ç”¨æ™ºèƒ½é‡‡æ ·ç–略:
1. **æ£å¸¸é‡‡æ ·**:æ¯ä¸ªå‘¨æœŸå†…最多ä¿ç•™ `MaxSamples` 个æ£å¸¸æ ·æœ¬ï¼ˆé»˜è®¤1个)
2. **å¼‚å¸¸é‡‡æ ·**:æ¯ä¸ªå‘¨æœŸå†…最多ä¿ç•™ `MaxErrors` ä¸ªå¼‚å¸¸æ ·æœ¬ï¼ˆé»˜è®¤10个)
3. **è¶…æ—¶é‡‡æ ·**:耗时超过 `Timeout` çš„æ“ä½œå¼ºåˆ¶é‡‡æ ·
4. **å…¨é“¾è·¯é‡‡æ ·**:设置 `TraceFlag` 的调用链全é‡é‡‡æ ·
### 本地统计机制
æ¯ä¸ªåŸ‹ç‚¹å对应一个 `SpanBuilder`ï¼Œç”¨äºŽç´¯åŠ ç»Ÿè®¡ï¼š
- **Total**: 总次数
- **Errors**: 异常次数
- **Cost**: 总耗时
- **MaxCost**: 最大耗时
- **MinCost**: 最å°è€—æ—¶
æ¯ä¸ªé‡‡æ ·å‘¨æœŸç»“æŸåŽï¼Œ`SpanBuilder` æ•°æ®æ‰“包上报,然åŽé‡ç½®è®¡æ•°å™¨ã€‚
---
## 高级特性
### 埋点命å规范
建议使用分层命å,便于分类统计:
```csharp
// 网络层
tracer.NewSpan("net:{åè®®}:Receive");
tracer.NewSpan("net:tcp:Send");
// æ•°æ®åº“层
tracer.NewSpan("db:{表å}:Select");
tracer.NewSpan("db:User:Insert");
// 业务层
tracer.NewSpan("biz:{模å—}:{æ“作}");
tracer.NewSpan("biz:Order:Create");
// 外部调用
tracer.NewSpan("http:{æœåŠ¡}:{方法}");
tracer.NewSpan("http:PaymentApi:Pay");
```
### 动æ€å‚数调整
å¯ä»¥åЍæ€è°ƒæ•´é‡‡æ ·å‚数:
```csharp
var tracer = DefaultTracer.Instance;
tracer.Period = 30; // 改为30秒周期
tracer.MaxSamples = 5; // å¢žåŠ æ£å¸¸é‡‡æ ·æ•°
tracer.Timeout = 10000; // 超时阈值改为10秒
tracer.MaxTagLength = 2048; // æ ‡ç¾é•¿åº¦æ”¹ä¸º2K
```
é€šå¸¸è¿™äº›å‚æ•°ç”±æ˜Ÿå°˜ç›‘控ä¸å¿ƒç»Ÿä¸€ä¸‹å‘。
### 自定义 ITracer 实现
å¯ä»¥å®žçŽ°è‡ªå·±çš„ `ITracer`:
```csharp
public class CustomTracer : ITracer
{
public Int32 Period { get; set; } = 15;
public Int32 MaxSamples { get; set; } = 1;
public Int32 MaxErrors { get; set; } = 10;
// ... å®žçŽ°æŽ¥å£æ–¹æ³•
public ISpan NewSpan(String name)
{
// 自定义埋点创建逻辑
var span = new CustomSpan { Name = name };
span.Start();
return span;
}
}
// 注册自定义实现
DefaultTracer.Instance = new CustomTracer();
```
---
## 性能优化
> **接入开销实测数æ®**:è§[《APM埋点性能测试报告》](/NewLife/X/Blob/dev/Doc/性能/APM埋点性能测试报告.md)——ä¸ç”¨ APM ä¸ºé›¶æˆæœ¬ï¼›ä¸€æ¬¡æ™®é€šåŸ‹ç‚¹çº¦ 0.13 µs / 72 Bï¼›OpenTelemetry 输出空埋点 235 ns / 672 B。
### 对象æ±
`DefaultTracer` å†…ç½®äº†å¯¹è±¡æ± ï¼Œå¤ç”¨ `ISpanBuilder` å’Œ `ISpan` 实例:
```csharp
public IPool<ISpanBuilder> BuilderPool { get; }
public IPool<ISpan> SpanPool { get; }
```
频ç¹åˆ›å»ºçš„åŸ‹ç‚¹å¯¹è±¡ä¼šè‡ªåŠ¨å½’è¿˜å¯¹è±¡æ± ï¼Œå‡å°‘ GC 压力。
### æ ‡ç¾é•¿åº¦æŽ§åˆ¶
å¯¹äºŽå¤§åž‹å¯¹è±¡ï¼Œå»ºè®®æŽ§åˆ¶æ ‡ç¾å†…容:
```csharp
// 䏿ލè:记录整个大对象
span.SetTag(largeObject);
// 推è:åªè®°å½•关键信æ¯
span.SetTag(new { Id = obj.Id, Name = obj.Name });
```
### æ¡ä»¶åŸ‹ç‚¹
对于éžå…³é”®è·¯å¾„,å¯ä»¥æ¡ä»¶æ€§åˆ›å»ºåŸ‹ç‚¹ï¼š
```csharp
ISpan? span = null;
if (_tracer != null && IsImportantOperation())
{
span = _tracer.NewSpan("ImportantOp");
}
try
{
// 业务逻辑
}
finally
{
span?.Dispose();
}
```
---
## 异常处ç†
### ApiException 特殊处ç†
业务异常 `ApiException` ä¸ä¼šè¢«æ ‡è®°ä¸ºé”™è¯¯ï¼š
```csharp
catch (ApiException aex)
{
span?.SetError(aex, request);
// 记录为æ£å¸¸åŸ‹ç‚¹ï¼ŒTagä¸åŒ…å«ä¸šåŠ¡é”™è¯¯ç
}
catch (Exception ex)
{
span?.SetError(ex, request);
// 记录为异常埋点,Errorä¸åŒ…å«å¼‚常消æ¯
}
```
### 异常埋点
æ¯ä¸ªå¼‚常会自动创建独立的异常埋点,便于按异常类型统计:
```csharp
// 原始埋点
using var span = tracer.NewSpan("DatabaseQuery");
try
{
// 抛出异常
throw new TimeoutException("查询超时");
}
catch (Exception ex)
{
span.SetError(ex, query);
// 自动创建 "ex:TimeoutException" 埋点
}
```
---
## 常è§é—®é¢˜
### 1. 埋点数æ®åœ¨æ˜Ÿå°˜å¹³å°çœ‹ä¸åˆ°ï¼Ÿ
æ£€æŸ¥ä»¥ä¸‹å‡ ç‚¹ï¼š
- 确认已注入星尘扩展:`services.AddStardust()`
- 检查网络连接到星尘æœåС噍
- æ£€æŸ¥åº”ç”¨åœ¨æ˜Ÿå°˜å¹³å°æ˜¯å¦å·²æ³¨å†Œ
- 查看本地日志,确认埋点æ£å¸¸åˆ›å»º
### 2. é‡‡æ ·æ•°æ®å¤ªå°‘?
è°ƒæ•´é‡‡æ ·å‚æ•°ï¼š
```csharp
tracer.MaxSamples = 10; // å¢žåŠ æ£å¸¸é‡‡æ ·æ•°
tracer.MaxErrors = 50; // å¢žåŠ å¼‚å¸¸é‡‡æ ·æ•°
tracer.Timeout = 1000; // é™ä½Žè¶…时阈值
```
### 3. 如何关é—埋点?
```csharp
// æ–¹å¼1ï¼šä¸æ³¨å…¥ ITracer
// services.AddStardust(); // 注释掉
// æ–¹å¼2:使用空实现
DefaultTracer.Instance = null;
```
### 4. 如何查看本地埋点数æ®ï¼Ÿ
DefaultTracer 会输出到日志:
```
Tracer[DataRetention] Total=10 Errors=0 Speed=0.02tps Cost=1500ms MaxCost=2000ms MinCost=1000ms
```
### 5. å¹¶å‘场景下 Span 会乱å—?
ä¸ä¼šã€‚`ISpan` 使用 `AsyncLocal<ISpan>` ä¿å˜ä¸Šä¸‹æ–‡ï¼Œæ¯ä¸ªå¼‚æ¥æµç¨‹ç‹¬ç«‹ï¼š
```csharp
public static AsyncLocal<ISpan?> Current { get; }
```
---
## 与 OpenTelemetry çš„åŒå‘适é…
OpenTelemetry 是 CNCF 的跨厂商å¯è§‚æµ‹æ€§æ ‡å‡†ï¼Œ.NET 上直接建立在 `System.Diagnostics.ActivitySource`/`Activity` 之上。NewLife çš„ `ITracer`/`ISpan` 与它是**两套体系**,调用形æ€é«˜åº¦ç›¸ä¼¼ä½†æ•°æ®æ¨¡åž‹ä¸åŒï¼š
| 维度 | NewLife `ITracer`/`ISpan` | OpenTelemetry (.NET) |
|---|---|---|
| 底层类型 | è‡ªç ” `ISpan`/`DefaultSpan` | `ActivitySource`/`Activity` |
| æ•°æ®ç²’度 | 按埋点åèšåˆç»Ÿè®¡ + 少釿 ·æœ¬ | æ¯ä¸ª Span 独立记录,全é‡è°ƒç”¨é“¾ |
| é‡‡æ · | `MaxSamples`/`MaxErrors`/`Timeout` + `TraceFlag` å¼ºåˆ¶é“¾è·¯é‡‡æ · | Head/Tail Sampling + Processor + Exporter |
| 上下文载体 | `DefaultSpan.Current` | `Activity.Current` |
| ä¼ æ’ | `traceparent`ï¼ˆå·²ç¬¦åˆ W3C) | `traceparent`(W3C) |
| 上报 | ç§æœ‰ RPC `Trace/Report` → 星尘 | OTLP → Collector/Jaeger/Tempo |
两者å¯ä»¥äº’相桥接,NewLife.Core æä¾›ä¸¤ä¸ªé€‚é…器(**net6.0 åŠä»¥ä¸Š**ï¼Œæ— æ–°å¢žä¾èµ–)。
### 把 NewLife 埋点输出为 OpenTelemetry
`OpenTelemetryTracer` 实现 `ITracer`,内部用 `ActivitySource` äº§å‡ºæ ‡å‡†æ´»åŠ¨ã€‚NewLife å„ç»„ä»¶æ— éœ€ä»»ä½•æ”¹é€ ï¼Œå®ƒä»¬è°ƒç”¨çš„ `Tracer?.NewSpan()` ä¼šåŽŸæ ·äº§å‡ºæ ‡å‡†æ´»åŠ¨ï¼š
```csharp
// 一行å¯ç”¨ï¼Œæœ¬è¿›ç¨‹æ‰€æœ‰ NewLife 埋点以 OpenTelemetry æ ‡å‡†è¾“å‡º
OpenTelemetryTracer.Enable();
```
`Inner` å±žæ€§å¯æŒ‡å‘底盘跟踪器,实现åŒå†™ã€‚æ¤æ—¶å†…层片段会继承活动的 W3C æ ‡è¯†ï¼Œæ˜Ÿå°˜é“¾è·¯ä¸Ž OpenTelemetry 链路å¯ä»¥äº’相对照:
```csharp
OpenTelemetryTracer.Enable(new OpenTelemetryTracer
{
SourceName = "MyApp",
Inner = new StarTracer("http://star:5000"),
});
```
没有任何活动监å¬è€…且未设置底盘时,`NewSpan` ç«‹å³è¿”回共享的空实现,ä¸äº§ç”Ÿåˆ†é…ï¼Œå› æ¤å¯ä»¥å®‰å…¨åœ°é•¿æœŸæŒ‚ç€ã€‚
> 注æ„:`Abandon()` åŽæ´»åЍä¸å†ç»“æŸï¼Œä¹Ÿå°±ä¸ä¼šé€šçŸ¥ä»»ä½•采集方,该埋点ä¸ä¼šè¢«å¯¼å‡ºï¼ˆå®žæµ‹æŠ›å¼ƒ 10000 次埋点导出 0 æ¡ï¼Œå¯¹ç…§æ£å¸¸åŸ‹ç‚¹ 10000 æ¡å…¨éƒ¨å¯¼å‡ºï¼‰ã€‚但活动ä¸åœæ¢å°±æ— 法归还 `Activity.Current`,åŒä¸€ä¸Šä¸‹æ–‡å†…åŽç»åŸ‹ç‚¹ä»ä¼šä»¥è¿™ä¸ªä¸ä¼šå¯¼å‡ºçš„片段为父级;需è¦å½»åº•过滤埋点的场景,应通过采集端的æºåè¿‡æ»¤æˆ–é‡‡æ ·å¤„ç†å™¨å®žçŽ°ã€‚è¯¦è§æ–‡æœ«[æ”¶å£è®°å½•](/NewLife/X/Blob/dev/Doc/#æ”¶å£è®°å½•)。
### æœåŠ¡ç«¯ç‰‡æ®µç»§æ‰¿ä¸Šæ¸¸é“¾è·¯
æœåŠ¡ç«¯æ”¶åˆ°è¯·æ±‚æ—¶ï¼Œè¦æŠŠä¸Šæ¸¸çš„ `traceparent` 作为父级,æ‰èƒ½ä¸²æˆä¸€æ¡è·¨è¿›ç¨‹è°ƒç”¨é“¾ã€‚星尘ç‰è·Ÿè¸ªå™¨çš„ç‰‡æ®µæ ‡è¯†æ˜¯å¯å˜å—段,「先创建片段ã€å† `Detach(headers)`ã€å³å¯ï¼›è€Œ OpenTelemetry 的活动一旦å¯åŠ¨å°±æ— æ³•å†æŒ‡å®šçˆ¶çº§ï¼Œå¿…é¡»åœ¨åˆ›å»ºæ—¶å°±ä¼ å…¥ä¸Šæ¸¸ä¸Šä¸‹æ–‡ã€‚ä¸é—´ä»¶ç»Ÿä¸€ç”¨ `NewServerSpan` å±è”½è¿™ä¸€å·®å¼‚:
```csharp
using var span = Tracer.NewServerSpan(action, req.Headers);
span.Tag = $"{ctx.GetUserHost()} {req.Method} {req.GetRawUrl()}";
```
OpenTelemetry åªè®¤ W3C çš„ `traceparent`ï¼ˆå« NewLife RPC 使用的 `TraceId` 头,其值åŒä¸º traceparent æ ¼å¼ï¼‰ï¼›`Request-Id`ã€`Eagleeye-Traceid` ç‰åކ岿 ¼å¼ä¸æ”¯æŒè·¨è¿›ç¨‹ç»§æ‰¿ã€‚
### 把 OpenTelemetry 埋点采集进星尘
很多开æºç»„件原生输出 OpenTelemetry 埋点。`OpenTelemetryCollector` 通过全局 `ActivityListener` è®¢é˜…è¿™äº›æ´»åŠ¨ï¼Œè½¬æˆ `ISpan` äº¤ç»™ç›®æ ‡è·Ÿè¸ªå™¨ï¼Œä»Žè€Œåœ¨æ˜Ÿå°˜å¹³å°ä¸Šä¹Ÿèƒ½çœ‹åˆ°ç¬¬ä¸‰æ–¹ç»„件的调用链:
```csharp
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();
```
采集到的活动按“æºå:æ“作åâ€ï¼ˆå¦‚ `StackExchange.Redis:SET`)èšåˆç»Ÿè®¡ï¼Œæ²¿ç”¨æ´»åŠ¨çš„ W3C æ ‡è¯†å¹¶å¼ºåˆ¶é‡‡æ ·ï¼Œå› æ¤ä¸ä¼šæŠŠæµ·é‡æ´»åЍ逿¡ä¸Šç½‘。
**去é‡**:åŒä¸€æ¬¡æ“作å¯èƒ½åŒæ—¶è¢« NewLife 原生埋点ã€`DiagnosticListener`(Stardust å·²å†…å»ºç›‘å¬ HttpClient/EF Core/SqlClient/gRPC/MongoDBï¼‰ä»¥åŠ `ActivityListener` 采集三次。`ExcludeSources` 默认排除这些已覆盖的æºï¼Œé¿å…星尘平å°ä¸Šçš„埋点计数翻å€ï¼›ä¹Ÿå¯æ”¹ç”¨ `IncludeSources` 白åå•åªé‡‡é›†ç¼ºå£éƒ¨åˆ†ã€‚
Stardust 的 `StarTracer` 已默认挂载该采集器,由 `StarSetting.EnableOpenTelemetry` 控制开关。
---
## æ”¶å£è®°å½•
> 已知ä¸å¤„ç†ã€ä¸å†é‡å¤ç™»è®°çš„事项。扫æåŸ‹ç‚¹ä½“ç³»å‰å…ˆè¯»æœ¬èŠ‚ï¼Œé¿å…é‡å¤æŠ¥å‘Šã€‚
| 事项 | ä½ç½® | 处置与ç†ç”± |
|------|------|-----------|
| OpenTelemetry 模å¼ä¸‹ `Abandon()` ä¸å½’还 `Activity.Current` | `OpenTelemetrySpan.Abandon` | **ä¸ä¿®**(2026-10 决ç–)。导出侧本就生效——活动ä¸ç»“æŸå³ä¸é€šçŸ¥é‡‡é›†æ–¹ï¼Œå®žæµ‹æŠ›å¼ƒ 10000 次埋点导出 0 æ¡ï¼Œå¯¹ç…§æ£å¸¸åŸ‹ç‚¹ 10000 æ¡å…¨éƒ¨å¯¼å‡ºï¼Œå› æ¤ä¸å˜åœ¨â€œè¢«æŠ›å¼ƒçš„埋点ä»ä¼šä¸ŠæŠ¥â€ã€‚æ®‹ä½™å½±å“æ˜¯æ´»åЍä¸åœæ¢å°±æ— 法归还 `Activity.Current`:åŒä¸€å¼‚æ¥ä¸Šä¸‹æ–‡å†…åŽç»åŸ‹ç‚¹ä¼šä»¥è¿™ä¸ªä¸ä¼šå¯¼å‡ºçš„ç‰‡æ®µä¸ºçˆ¶çº§ï¼Œè¿žç»æŠ›å¼ƒè¿˜ä¼šè®©è¿™æ¡çˆ¶çº§é“¾ä¸€ç›´è¢«æŒæœ‰ï¼ˆå®žæµ‹ 257 B/次)。Web 场景æ¯ä¸ªè¯·æ±‚å„自一个异æ¥ä¸Šä¸‹æ–‡ã€é“¾é•¿â‰ˆ1,影å“å¯å¿½ç•¥ï¼›æ´»åŠ¨ç”Ÿå‘½å‘¨æœŸå½’ OpenTelemetry SDK 管ç†ï¼Œåº“ä¾§ä¸ä»‹å…¥ã€‚星尘档 `DefaultSpan.Abandon` 已归还上下文(实测驻留由 171 B/次é™è‡³ 0)。 |
| 星尘档 `DefaultSpan.Current` çš„ `AsyncLocal` å†™å…¥åˆ†é… | `DefaultSpan.Start` / `Finish` | **ä¸ä¿®**(2026-10-11 决ç–)。实测å•å±‚åŸ‹ç‚¹çš„åˆ†é… **100%** æ¥è‡ªè¿™ä¸€æ¬¡å†™å…¥ï¼šå¯¹è±¡æ± `Get`/`Return`ã€æž„建器å—典命ä¸ã€**值未å˜åŒ–的写入**å‡ä¸º 0 B,而值æ¯å˜åŒ–ä¸€æ¬¡å³ 72 B(≈40 B çš„ `ExecutionContext` + 32 B çš„å€¼æ˜ å°„èŠ‚ç‚¹ï¼‰ã€‚`ExecutionContext` ä¸å¯å˜ï¼Œå€¼ä¸€å˜åªèƒ½æ–°å»ºï¼Œå±ž .NET 模型下é™ã€‚改 `[ThreadStatic]` ä¼šè®©åŸ‹ç‚¹æ— æ³•è·¨ `await` ä¼ æ’(异æ¥é“¾è·¯æ–裂);改为"å¯å˜çš„ `StrongBox` å°±åœ°æ”¹å—æ®µ"看似 0 B,但并行分支共享åŒä¸€ç›’å会互相覆盖,属é™é»˜ä¸²åŒ…ï¼›`Finish` ä¸è¿˜åŽŸ `Current` 会让åŽç»åŸ‹ç‚¹æŒ‚åˆ°å·²å½’è¿˜å¯¹è±¡æ± çš„ç‰‡æ®µä¸Šã€‚ä¸‰è€…éƒ½æ¯”åˆ†é…问题严é‡ï¼Œæ•…ä¿ç•™ï¼Œ"æ¯æ¬¡ `NewSpan` 零分é…"在本模型下ä¸å¯è¾¾ã€‚两点边界需知:**该值éšå®¿ä¸» `ExecutionContext` ä¸å·²æœ‰çš„ `AsyncLocal` 槽数上å‡**(干净进程 72 Bã€1 æ§½ 160 Bã€3 槽约 224 B);**嵌套æ¯å±‚å† +200 B**(2 次上下文写入 144 B + 父片段 `Id` å—符串 56 B,第 2 层妿œ‰çˆ¶ `TraceId` 88 B)。 |
---
## å‚考资料
- **星尘监控平å°**: https://newlifex.com/blood/stardust
- **W3C Trace Context**: https://www.w3.org/TR/trace-context/
- **æºç 仓库**: https://github.com/NewLifeX/X
- **在线文档**: https://newlifex.com/core/tracer
---
## 更新日志
- **2026-10**: 新增 OpenTelemetry åŒå‘适é…(`OpenTelemetryTracer` 输出ã€`OpenTelemetryCollector` 采集);traceId å‡çº§ä¸º W3C æ ‡å‡†çš„ 32 ä½åå…进制;星尘档 `Abandon()` 归还上下文
- **2024-12-16**: 完善文档,补充最佳实践和示例代ç
- **2024-08**: æ”¯æŒ .NET 9.0
- **2023-11**: å¢žåŠ `Abandon()` 方法
- **2023-06**: ä¼˜åŒ–å¯¹è±¡æ± ï¼Œæå‡æ€§èƒ½
- **2022**: åˆå§‹ç‰ˆæœ¬å‘布
|