为了显示基于调用链的跟踪信息我们在本地安装了Jaeger。为了收集和展示性能指标我们使用了Prometheus和Grafana。我们采用最简单的方式通过在本地创建相映的Docker容器来搭建这些服务。如果希望在Windows上执行相应的命令将换行符从\改为^即可。Jageerdocker run -d --name jaeger \ -p 16686:16686 \ -p 4317:4317 \ jaegertracing/all-in-one:latestPrometheusdocker run -d --name prometheus \ -p 9090:9090 \ -v /c/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml \ prom/prometheus:latest其中c:\prometheus\prometheus.yml的内容如下global: scrape_interval: 5s scrape_configs: - job_name: prometheus static_configs: - targets: [localhost:9090] - job_name: csharp_console_app static_configs: - targets: [192.168.1.166:9464]192.168.1.166是本地的IP地址9464是接下来创建的应用暴露的端口用于输出性能指标信息。Grafanadocker run -d --name grafana \ -p 3000:3000 \ grafana/grafana:latest然后添加针对Prometheushttp://192.168.1.166:9090的连接。我针对OpenTelemetryChatClient输出的指标创建了一个简单Dashboard可以通过这里下载并导入。2. 构建一个简单的Agent应用我们创建一个简单的Console应用并添加针对OpenTelemetry.NET相关的NuGet包OpenTelemetryOpenTelemetry.Exporter.ConsoleOpenTelemetry.Exporter.OpenTelemetryProtocolOpenTelemetry.Exporter.Prometheus.HttpListenerOpenTelemetry.Extensions.Hosting如下所示的是完整的演示程序。最外层的两个using块分别创建了TracerProvider和MeterProvider前者用于链路跟踪后者用于性能指标的收集两者设置了相同的服务名称AIApp和版本1.0.0。对于Trace我们添加了Console和OTLP两种Exporter后者将数据发送到Jaeger。对于Metrics我们添加了Console和PrometheusHttpListener两种Exporter后者在http://192.168.1.166:9464/暴露性能指标供Prometheus收集。using Azure; using dotenv.net; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using OpenAI; using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; using System.Diagnostics; DotEnv.Load(); var model Environment.GetEnvironmentVariable(MODEL)!; var apiKey Environment.GetEnvironmentVariable(API_KEY)!; var endpoint Environment.GetEnvironmentVariable(OPENAI_URL)!; var serviceName AIApp; var servceVersion 1.0.0; using (Sdk.CreateTracerProviderBuilder() .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(serviceName, serviceVersion: servceVersion)) .AddSource(serviceName) .AddConsoleExporter() .AddOtlpExporter(options { options.Endpoint new Uri(http://localhost:4317); options.Protocol OpenTelemetry.Exporter.OtlpExportProtocol.Grpc; }) .Build()) using (Sdk.CreateMeterProviderBuilder() .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(serviceName, serviceVersion: servceVersion)) .AddMeter(serviceName) .AddConsoleExporter() .AddPrometheusHttpListener(options { options.UriPrefixes [http://192.168.1.166:9464/]; }).Build()) { var chatClient new OpenAIClient( credential: new AzureKeyCredential(apiKey), options: new OpenAIClientOptions { Endpoint new Uri(endpoint) }) .GetChatClient(model: model) .AsIChatClient() .AsBuilder() .UseOpenTelemetry(sourceName: serviceName) .Build(); string[] queries [ What is the capital of France?, Who won the FIFA World Cup in 2018?, What is the largest mammal on Earth? ]; var random new Random(); var source new ActivitySource(serviceName); for (int i 0; i 30; i) { using (source.StartActivity(Agent-Server, kind: ActivityKind.Server, parentContext: default)) { await Task.Delay(random.Next(100, 1000)); using (source.StartActivity(Foo)) { await Task.Delay(random.Next(100, 1000)); using (source.StartActivity(Bar)) { await Task.Delay(random.Next(100, 1000)); await chatClient.GetResponseAsync(queries[random.Next(queries.Length)]); } } } await Task.Delay(random.Next(3000, 5000)); } Console.ReadLine(); }在using块中我们创建了用来调用LLM的IChatClient对象。具体来说我们首先创建了一个OpenAIClient并通过GetChatClient方法获取了一个针对聊天模型的客户端。然后我们将其转换为IChatClient并使用AsBuilder方法创建了一个可配置的构建器。在构建器上我们调用UseOpenTelemetry方法指定了与TracerProvider和MeterProvider相同的sourceNameAIApp以启用链路跟踪和性能指标的收集。最后我们调用Build方法构建了最终的IChatClient对象。为了模拟一段持续的调用我们在一个循环中随机选择了三个问题并调用了GetResponseAsync方法。为了模拟一段完整的调用链我们利用创建的ActivitySource将服务名称作为sourceName手动创建了三个不同层级的Activity分别命名为Agent-Server、Foo和Bar它们表示LLM调用外层的操作。3. 结果展示运行程序之后我们可以在控制台上看到链路跟踪和性能指标的输出。同时在Jaeger的UI界面http://localhost:16686/上我们可以看到针对Agent-Server操作的调用链信息如下图所示打开Grafana的Dashboard我们可以看到针对LLM调用的性能指标其中包括请求和响应Token的消耗、调用LLM的延时、成功调用的比例和错误分布等。4. OpenTelemetryChatClient和我们演示的程序一样OpenTelemetryChatClient也是使用ActivitySource创建的Activity来表示针对LLM的调用。创建这个ActivitySource指定的名称来源于OpenTelemetryChatClient构造函数中的sourceName参数在OpenTelemetry的语境中将它视为服务名称。如果没有显示指定sourceNameOpenTelemetryChatClient会使用默认的名称Experimental.Microsoft.Extensions.AI。public sealed partial class OpenTelemetryChatClient : DelegatingChatClient { public OpenTelemetryChatClient( IChatClient innerClient, ILogger? logger null, string? sourceName null); public JsonSerializerOptions JsonSerializerOptions { get; set; } public bool EnableSensitiveData { get; set; } TelemetryHelpers.EnableSensitiveDataDefault; public override async TaskChatResponse GetResponseAsync( IEnumerableChatMessage messages, ChatOptions? options null, CancellationToken cancellationToken default); public override async IAsyncEnumerableChatResponseUpdate GetStreamingResponseAsync( IEnumerableChatMessage messages, ChatOptions? options null, CancellationToken cancellationToken default); }EnableSensitiveData属性用于控制是否允许在Trace数据中包含一些敏感数据这个属性的默认值来源于针对环境变量OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT的设置。如果两者均为设置默认不捕获敏感数据。如果这个属性被设置为true调用LLM的请求和响应消息会被序列化并作为操作的标签进行输出JsonSerializerOptions属性就是用来控制这个序列化过程的行为的。在重写的GetResponseAsync和GetStreamingResponseAsync方法中OpenTelemetryChatClient会创建一个新的Activity来表示针对IChatClient的调用。如果能够从ChatOptions中提取名称的名称对应ModelId属性此操作被命名为“chat {model-name}”否则被命名为“chat”。创建的Activity会被设置一系列丰富的标签来描述此次调用。对于我们前面的演示程序OpenTelemetryChatClient创建的跟踪操作包含的标签体现在如下这张针对Jaeger的截图上。
构建基础设施
发布时间:2026/6/29 23:19:10
为了显示基于调用链的跟踪信息我们在本地安装了Jaeger。为了收集和展示性能指标我们使用了Prometheus和Grafana。我们采用最简单的方式通过在本地创建相映的Docker容器来搭建这些服务。如果希望在Windows上执行相应的命令将换行符从\改为^即可。Jageerdocker run -d --name jaeger \ -p 16686:16686 \ -p 4317:4317 \ jaegertracing/all-in-one:latestPrometheusdocker run -d --name prometheus \ -p 9090:9090 \ -v /c/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml \ prom/prometheus:latest其中c:\prometheus\prometheus.yml的内容如下global: scrape_interval: 5s scrape_configs: - job_name: prometheus static_configs: - targets: [localhost:9090] - job_name: csharp_console_app static_configs: - targets: [192.168.1.166:9464]192.168.1.166是本地的IP地址9464是接下来创建的应用暴露的端口用于输出性能指标信息。Grafanadocker run -d --name grafana \ -p 3000:3000 \ grafana/grafana:latest然后添加针对Prometheushttp://192.168.1.166:9090的连接。我针对OpenTelemetryChatClient输出的指标创建了一个简单Dashboard可以通过这里下载并导入。2. 构建一个简单的Agent应用我们创建一个简单的Console应用并添加针对OpenTelemetry.NET相关的NuGet包OpenTelemetryOpenTelemetry.Exporter.ConsoleOpenTelemetry.Exporter.OpenTelemetryProtocolOpenTelemetry.Exporter.Prometheus.HttpListenerOpenTelemetry.Extensions.Hosting如下所示的是完整的演示程序。最外层的两个using块分别创建了TracerProvider和MeterProvider前者用于链路跟踪后者用于性能指标的收集两者设置了相同的服务名称AIApp和版本1.0.0。对于Trace我们添加了Console和OTLP两种Exporter后者将数据发送到Jaeger。对于Metrics我们添加了Console和PrometheusHttpListener两种Exporter后者在http://192.168.1.166:9464/暴露性能指标供Prometheus收集。using Azure; using dotenv.net; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using OpenAI; using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; using System.Diagnostics; DotEnv.Load(); var model Environment.GetEnvironmentVariable(MODEL)!; var apiKey Environment.GetEnvironmentVariable(API_KEY)!; var endpoint Environment.GetEnvironmentVariable(OPENAI_URL)!; var serviceName AIApp; var servceVersion 1.0.0; using (Sdk.CreateTracerProviderBuilder() .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(serviceName, serviceVersion: servceVersion)) .AddSource(serviceName) .AddConsoleExporter() .AddOtlpExporter(options { options.Endpoint new Uri(http://localhost:4317); options.Protocol OpenTelemetry.Exporter.OtlpExportProtocol.Grpc; }) .Build()) using (Sdk.CreateMeterProviderBuilder() .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(serviceName, serviceVersion: servceVersion)) .AddMeter(serviceName) .AddConsoleExporter() .AddPrometheusHttpListener(options { options.UriPrefixes [http://192.168.1.166:9464/]; }).Build()) { var chatClient new OpenAIClient( credential: new AzureKeyCredential(apiKey), options: new OpenAIClientOptions { Endpoint new Uri(endpoint) }) .GetChatClient(model: model) .AsIChatClient() .AsBuilder() .UseOpenTelemetry(sourceName: serviceName) .Build(); string[] queries [ What is the capital of France?, Who won the FIFA World Cup in 2018?, What is the largest mammal on Earth? ]; var random new Random(); var source new ActivitySource(serviceName); for (int i 0; i 30; i) { using (source.StartActivity(Agent-Server, kind: ActivityKind.Server, parentContext: default)) { await Task.Delay(random.Next(100, 1000)); using (source.StartActivity(Foo)) { await Task.Delay(random.Next(100, 1000)); using (source.StartActivity(Bar)) { await Task.Delay(random.Next(100, 1000)); await chatClient.GetResponseAsync(queries[random.Next(queries.Length)]); } } } await Task.Delay(random.Next(3000, 5000)); } Console.ReadLine(); }在using块中我们创建了用来调用LLM的IChatClient对象。具体来说我们首先创建了一个OpenAIClient并通过GetChatClient方法获取了一个针对聊天模型的客户端。然后我们将其转换为IChatClient并使用AsBuilder方法创建了一个可配置的构建器。在构建器上我们调用UseOpenTelemetry方法指定了与TracerProvider和MeterProvider相同的sourceNameAIApp以启用链路跟踪和性能指标的收集。最后我们调用Build方法构建了最终的IChatClient对象。为了模拟一段持续的调用我们在一个循环中随机选择了三个问题并调用了GetResponseAsync方法。为了模拟一段完整的调用链我们利用创建的ActivitySource将服务名称作为sourceName手动创建了三个不同层级的Activity分别命名为Agent-Server、Foo和Bar它们表示LLM调用外层的操作。3. 结果展示运行程序之后我们可以在控制台上看到链路跟踪和性能指标的输出。同时在Jaeger的UI界面http://localhost:16686/上我们可以看到针对Agent-Server操作的调用链信息如下图所示打开Grafana的Dashboard我们可以看到针对LLM调用的性能指标其中包括请求和响应Token的消耗、调用LLM的延时、成功调用的比例和错误分布等。4. OpenTelemetryChatClient和我们演示的程序一样OpenTelemetryChatClient也是使用ActivitySource创建的Activity来表示针对LLM的调用。创建这个ActivitySource指定的名称来源于OpenTelemetryChatClient构造函数中的sourceName参数在OpenTelemetry的语境中将它视为服务名称。如果没有显示指定sourceNameOpenTelemetryChatClient会使用默认的名称Experimental.Microsoft.Extensions.AI。public sealed partial class OpenTelemetryChatClient : DelegatingChatClient { public OpenTelemetryChatClient( IChatClient innerClient, ILogger? logger null, string? sourceName null); public JsonSerializerOptions JsonSerializerOptions { get; set; } public bool EnableSensitiveData { get; set; } TelemetryHelpers.EnableSensitiveDataDefault; public override async TaskChatResponse GetResponseAsync( IEnumerableChatMessage messages, ChatOptions? options null, CancellationToken cancellationToken default); public override async IAsyncEnumerableChatResponseUpdate GetStreamingResponseAsync( IEnumerableChatMessage messages, ChatOptions? options null, CancellationToken cancellationToken default); }EnableSensitiveData属性用于控制是否允许在Trace数据中包含一些敏感数据这个属性的默认值来源于针对环境变量OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT的设置。如果两者均为设置默认不捕获敏感数据。如果这个属性被设置为true调用LLM的请求和响应消息会被序列化并作为操作的标签进行输出JsonSerializerOptions属性就是用来控制这个序列化过程的行为的。在重写的GetResponseAsync和GetStreamingResponseAsync方法中OpenTelemetryChatClient会创建一个新的Activity来表示针对IChatClient的调用。如果能够从ChatOptions中提取名称的名称对应ModelId属性此操作被命名为“chat {model-name}”否则被命名为“chat”。创建的Activity会被设置一系列丰富的标签来描述此次调用。对于我们前面的演示程序OpenTelemetryChatClient创建的跟踪操作包含的标签体现在如下这张针对Jaeger的截图上。