Skip to content

Commit d6cc65a

Browse files
authored
OpenTelemetry: context propagation and semconv update (#262)
1 parent f2fd204 commit d6cc65a

File tree

11 files changed

+398
-89
lines changed

11 files changed

+398
-89
lines changed

Diff for: Directory.Packages.props

+4
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@
5858
<PackageVersion Include="Moq" Version="4.20.72" />
5959
<PackageVersion Include="OpenTelemetry" Version="1.11.2" />
6060
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.11.2" />
61+
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.11.2" />
62+
<PackageVersion Include="OpenTelemetry.Instrumentation.Http " Version="1.11.0" />
63+
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.11.2" />
64+
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.11.1" />
6165
<PackageVersion Include="Serilog.Extensions.Hosting" Version="9.0.0" />
6266
<PackageVersion Include="Serilog.Extensions.Logging" Version="9.0.0" />
6367
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />

Diff for: samples/AspNetCoreSseServer/AspNetCoreSseServer.csproj

+7
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,11 @@
1212
<ProjectReference Include="..\..\src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj" />
1313
</ItemGroup>
1414

15+
<ItemGroup>
16+
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
17+
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
18+
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
19+
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
20+
</ItemGroup>
21+
1522
</Project>

Diff for: samples/AspNetCoreSseServer/Program.cs

+13
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
using TestServerWithHosting.Tools;
2+
using OpenTelemetry.Metrics;
3+
using OpenTelemetry.Trace;
4+
using OpenTelemetry;
25

36
var builder = WebApplication.CreateBuilder(args);
47
builder.Services.AddMcpServer()
58
.WithTools<EchoTool>()
69
.WithTools<SampleLlmTool>();
710

11+
builder.Services.AddOpenTelemetry()
12+
.WithTracing(b => b.AddSource("*")
13+
.AddAspNetCoreInstrumentation()
14+
.AddHttpClientInstrumentation())
15+
.WithMetrics(b => b.AddMeter("*")
16+
.AddAspNetCoreInstrumentation()
17+
.AddHttpClientInstrumentation())
18+
.WithLogging()
19+
.UseOtlpExporter();
20+
821
var app = builder.Build();
922

1023
app.MapMcp();

Diff for: samples/AspNetCoreSseServer/Properties/launchSettings.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@
66
"dotnetRunMessages": true,
77
"applicationUrl": "http://localhost:3001",
88
"environmentVariables": {
9-
"ASPNETCORE_ENVIRONMENT": "Development"
9+
"ASPNETCORE_ENVIRONMENT": "Development",
10+
"OTEL_SERVICE_NAME": "sse-server",
1011
}
1112
},
1213
"https": {
1314
"commandName": "Project",
1415
"dotnetRunMessages": true,
1516
"applicationUrl": "https://localhost:7133;http://localhost:3001",
1617
"environmentVariables": {
17-
"ASPNETCORE_ENVIRONMENT": "Development"
18+
"ASPNETCORE_ENVIRONMENT": "Development",
19+
"OTEL_SERVICE_NAME": "sse-server",
1820
}
1921
}
2022
}

Diff for: samples/ChatWithTools/ChatWithTools.csproj

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
<PackageReference Include="Microsoft.Extensions.AI" />
1616
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
1717
<PackageReference Include="Anthropic.SDK" />
18+
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
19+
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
1820
</ItemGroup>
1921

2022
<ItemGroup>

Diff for: samples/ChatWithTools/Program.cs

+42-6
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,49 @@
33
using Microsoft.Extensions.AI;
44
using OpenAI;
55

6+
using OpenTelemetry;
7+
using OpenTelemetry.Trace;
8+
using Microsoft.Extensions.Logging;
9+
using OpenTelemetry.Logs;
10+
using OpenTelemetry.Metrics;
11+
12+
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
13+
.AddHttpClientInstrumentation()
14+
.AddSource("*")
15+
.AddOtlpExporter()
16+
.Build();
17+
using var metricsProvider = Sdk.CreateMeterProviderBuilder()
18+
.AddHttpClientInstrumentation()
19+
.AddMeter("*")
20+
.AddOtlpExporter()
21+
.Build();
22+
using var loggerFactory = LoggerFactory.Create(builder => builder.AddOpenTelemetry(opt => opt.AddOtlpExporter()));
23+
624
// Connect to an MCP server
725
Console.WriteLine("Connecting client to MCP 'everything' server");
26+
27+
// Create OpenAI client (or any other compatible with IChatClient)
28+
// Provide your own OPENAI_API_KEY via an environment variable.
29+
var openAIClient = new OpenAIClient(Environment.GetEnvironmentVariable("OPENAI_API_KEY")).GetChatClient("gpt-4o-mini");
30+
31+
// Create a sampling client.
32+
using IChatClient samplingClient = openAIClient.AsIChatClient()
33+
.AsBuilder()
34+
.UseOpenTelemetry(loggerFactory: loggerFactory, configure: o => o.EnableSensitiveData = true)
35+
.Build();
36+
837
var mcpClient = await McpClientFactory.CreateAsync(
938
new StdioClientTransport(new()
1039
{
1140
Command = "npx",
1241
Arguments = ["-y", "--verbose", "@modelcontextprotocol/server-everything"],
1342
Name = "Everything",
14-
}));
43+
}),
44+
clientOptions: new()
45+
{
46+
Capabilities = new() { Sampling = new() { SamplingHandler = samplingClient.CreateSamplingHandler() } },
47+
},
48+
loggerFactory: loggerFactory);
1549

1650
// Get all available tools
1751
Console.WriteLine("Tools available:");
@@ -20,13 +54,15 @@
2054
{
2155
Console.WriteLine($" {tool}");
2256
}
57+
2358
Console.WriteLine();
2459

25-
// Create an IChatClient. (This shows using OpenAIClient, but it could be any other IChatClient implementation.)
26-
// Provide your own OPENAI_API_KEY via an environment variable.
27-
using IChatClient chatClient =
28-
new OpenAIClient(Environment.GetEnvironmentVariable("OPENAI_API_KEY")).GetChatClient("gpt-4o-mini").AsIChatClient()
29-
.AsBuilder().UseFunctionInvocation().Build();
60+
// Create an IChatClient that can use the tools.
61+
using IChatClient chatClient = openAIClient.AsIChatClient()
62+
.AsBuilder()
63+
.UseFunctionInvocation()
64+
.UseOpenTelemetry(loggerFactory: loggerFactory, configure: o => o.EnableSensitiveData = true)
65+
.Build();
3066

3167
// Have a conversation, making all tools available to the LLM.
3268
List<ChatMessage> messages = [];

Diff for: samples/EverythingServer/EverythingServer.csproj

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010
<ItemGroup>
1111
<PackageReference Include="Microsoft.Extensions.Hosting" />
12+
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
13+
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
14+
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
1215
</ItemGroup>
1316

1417
<ItemGroup>

Diff for: samples/EverythingServer/Program.cs

+12
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
using ModelContextProtocol;
99
using ModelContextProtocol.Protocol.Types;
1010
using ModelContextProtocol.Server;
11+
using OpenTelemetry;
12+
using OpenTelemetry.Logs;
13+
using OpenTelemetry.Metrics;
14+
using OpenTelemetry.Resources;
15+
using OpenTelemetry.Trace;
1116

1217
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
1318

@@ -186,6 +191,13 @@ await ctx.Server.RequestSamplingAsync([
186191
return new EmptyResult();
187192
});
188193

194+
ResourceBuilder resource = ResourceBuilder.CreateDefault().AddService("everything-server");
195+
builder.Services.AddOpenTelemetry()
196+
.WithTracing(b => b.AddSource("*").AddHttpClientInstrumentation().SetResourceBuilder(resource))
197+
.WithMetrics(b => b.AddMeter("*").AddHttpClientInstrumentation().SetResourceBuilder(resource))
198+
.WithLogging(b => b.SetResourceBuilder(resource))
199+
.UseOtlpExporter();
200+
189201
builder.Services.AddSingleton(subscriptions);
190202
builder.Services.AddHostedService<SubscriptionMessageSender>();
191203
builder.Services.AddHostedService<LoggingUpdateMessageSender>();

Diff for: src/ModelContextProtocol/Diagnostics.cs

+76
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
using System.Diagnostics;
22
using System.Diagnostics.Metrics;
3+
using System.Text.Json;
4+
using System.Text.Json.Nodes;
5+
using ModelContextProtocol.Protocol.Messages;
36

47
namespace ModelContextProtocol;
58

@@ -34,4 +37,77 @@ internal static Histogram<double> CreateDurationHistogram(string name, string de
3437
HistogramBucketBoundaries = [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60, 120, 300],
3538
};
3639
#endif
40+
41+
internal static ActivityContext ExtractActivityContext(this DistributedContextPropagator propagator, IJsonRpcMessage message)
42+
{
43+
propagator.ExtractTraceIdAndState(message, ExtractContext, out var traceparent, out var tracestate);
44+
ActivityContext.TryParse(traceparent, tracestate, true, out var activityContext);
45+
return activityContext;
46+
}
47+
48+
private static void ExtractContext(object? message, string fieldName, out string? fieldValue, out IEnumerable<string>? fieldValues)
49+
{
50+
fieldValues = null;
51+
fieldValue = null;
52+
53+
JsonNode? parameters = null;
54+
switch (message)
55+
{
56+
case JsonRpcRequest request:
57+
parameters = request.Params;
58+
break;
59+
60+
case JsonRpcNotification notification:
61+
parameters = notification.Params;
62+
break;
63+
64+
default:
65+
break;
66+
}
67+
68+
if (parameters?[fieldName] is JsonValue value && value.GetValueKind() == JsonValueKind.String)
69+
{
70+
fieldValue = value.GetValue<string>();
71+
}
72+
}
73+
74+
internal static void InjectActivityContext(this DistributedContextPropagator propagator, Activity? activity, IJsonRpcMessage message)
75+
{
76+
// noop if activity is null
77+
propagator.Inject(activity, message, InjectContext);
78+
}
79+
80+
private static void InjectContext(object? message, string key, string value)
81+
{
82+
JsonNode? parameters = null;
83+
switch (message)
84+
{
85+
case JsonRpcRequest request:
86+
parameters = request.Params;
87+
break;
88+
89+
case JsonRpcNotification notification:
90+
parameters = notification.Params;
91+
break;
92+
93+
default:
94+
break;
95+
}
96+
97+
if (parameters is JsonObject jsonObject && jsonObject[key] == null)
98+
{
99+
jsonObject[key] = value;
100+
}
101+
}
102+
103+
internal static bool ShouldInstrumentMessage(IJsonRpcMessage message) =>
104+
ActivitySource.HasListeners() &&
105+
message switch
106+
{
107+
JsonRpcRequest => true,
108+
JsonRpcNotification notification => notification.Method != NotificationMethods.LoggingMessageNotification,
109+
_ => false
110+
};
111+
112+
internal static ActivityLink[] ActivityLinkFromCurrent() => Activity.Current is null ? [] : [new ActivityLink(Activity.Current.Context)];
37113
}

0 commit comments

Comments
 (0)