Skip to content

OpenTelemetry: context propagation and semconv update #262

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="OpenTelemetry" Version="1.11.2" />
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.11.2" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.11.2" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http " Version="1.11.0" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.11.2" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.11.1" />
<PackageVersion Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageVersion Include="Serilog.Extensions.Logging" Version="9.0.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
Expand Down
7 changes: 7 additions & 0 deletions samples/AspNetCoreSseServer/AspNetCoreSseServer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,11 @@
<ProjectReference Include="..\..\src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
</ItemGroup>

</Project>
13 changes: 13 additions & 0 deletions samples/AspNetCoreSseServer/Program.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
using TestServerWithHosting.Tools;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
using OpenTelemetry;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMcpServer()
.WithTools<EchoTool>()
.WithTools<SampleLlmTool>();

builder.Services.AddOpenTelemetry()
.WithTracing(b => b.AddSource("*")
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation())
.WithMetrics(b => b.AddMeter("*")
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation())
.WithLogging()
.UseOtlpExporter();

var app = builder.Build();

app.MapMcp();
Expand Down
6 changes: 4 additions & 2 deletions samples/AspNetCoreSseServer/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:3001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
"ASPNETCORE_ENVIRONMENT": "Development",
"OTEL_SERVICE_NAME": "sse-server",
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:7133;http://localhost:3001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
"ASPNETCORE_ENVIRONMENT": "Development",
"OTEL_SERVICE_NAME": "sse-server",
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions samples/ChatWithTools/ChatWithTools.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
<PackageReference Include="Microsoft.Extensions.AI" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Anthropic.SDK" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
</ItemGroup>

<ItemGroup>
Expand Down
48 changes: 42 additions & 6 deletions samples/ChatWithTools/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,49 @@
using Microsoft.Extensions.AI;
using OpenAI;

using OpenTelemetry;
using OpenTelemetry.Trace;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;

using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddHttpClientInstrumentation()
.AddSource("*")
.AddOtlpExporter()
.Build();
using var metricsProvider = Sdk.CreateMeterProviderBuilder()
.AddHttpClientInstrumentation()
.AddMeter("*")
.AddOtlpExporter()
.Build();
using var loggerFactory = LoggerFactory.Create(builder => builder.AddOpenTelemetry(opt => opt.AddOtlpExporter()));

// Connect to an MCP server
Console.WriteLine("Connecting client to MCP 'everything' server");

// Create OpenAI client (or any other compatible with IChatClient)
// Provide your own OPENAI_API_KEY via an environment variable.
var openAIClient = new OpenAIClient(Environment.GetEnvironmentVariable("OPENAI_API_KEY")).GetChatClient("gpt-4o-mini");

// Create a sampling client.
using IChatClient samplingClient = openAIClient.AsIChatClient()
.AsBuilder()
.UseOpenTelemetry(loggerFactory: loggerFactory, configure: o => o.EnableSensitiveData = true)
.Build();

var mcpClient = await McpClientFactory.CreateAsync(
new StdioClientTransport(new()
{
Command = "npx",
Arguments = ["-y", "--verbose", "@modelcontextprotocol/server-everything"],
Name = "Everything",
}));
}),
clientOptions: new()
{
Capabilities = new() { Sampling = new() { SamplingHandler = samplingClient.CreateSamplingHandler() } },
},
loggerFactory: loggerFactory);

// Get all available tools
Console.WriteLine("Tools available:");
Expand All @@ -20,13 +54,15 @@
{
Console.WriteLine($" {tool}");
}

Console.WriteLine();

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

// Have a conversation, making all tools available to the LLM.
List<ChatMessage> messages = [];
Expand Down
3 changes: 3 additions & 0 deletions samples/EverythingServer/EverythingServer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
</ItemGroup>

<ItemGroup>
Expand Down
12 changes: 12 additions & 0 deletions samples/EverythingServer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
using ModelContextProtocol;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Server;
using OpenTelemetry;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

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

Expand Down Expand Up @@ -186,6 +191,13 @@ await ctx.Server.RequestSamplingAsync([
return new EmptyResult();
});

ResourceBuilder resource = ResourceBuilder.CreateDefault().AddService("everything-server");
builder.Services.AddOpenTelemetry()
.WithTracing(b => b.AddSource("*").AddHttpClientInstrumentation().SetResourceBuilder(resource))
.WithMetrics(b => b.AddMeter("*").AddHttpClientInstrumentation().SetResourceBuilder(resource))
.WithLogging(b => b.SetResourceBuilder(resource))
.UseOtlpExporter();

builder.Services.AddSingleton(subscriptions);
builder.Services.AddHostedService<SubscriptionMessageSender>();
builder.Services.AddHostedService<LoggingUpdateMessageSender>();
Expand Down
76 changes: 76 additions & 0 deletions src/ModelContextProtocol/Diagnostics.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Text.Json;
using System.Text.Json.Nodes;
using ModelContextProtocol.Protocol.Messages;

namespace ModelContextProtocol;

Expand Down Expand Up @@ -34,4 +37,77 @@ internal static Histogram<double> CreateDurationHistogram(string name, string de
HistogramBucketBoundaries = [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60, 120, 300],
};
#endif

internal static ActivityContext ExtractActivityContext(this DistributedContextPropagator propagator, IJsonRpcMessage message)
{
propagator.ExtractTraceIdAndState(message, ExtractContext, out var traceparent, out var tracestate);
ActivityContext.TryParse(traceparent, tracestate, true, out var activityContext);
return activityContext;
}

private static void ExtractContext(object? message, string fieldName, out string? fieldValue, out IEnumerable<string>? fieldValues)
{
fieldValues = null;
fieldValue = null;

JsonNode? parameters = null;
switch (message)
{
case JsonRpcRequest request:
parameters = request.Params;
break;

case JsonRpcNotification notification:
parameters = notification.Params;
break;

default:
break;
}

if (parameters?[fieldName] is JsonValue value && value.GetValueKind() == JsonValueKind.String)
{
fieldValue = value.GetValue<string>();
}
}

internal static void InjectActivityContext(this DistributedContextPropagator propagator, Activity? activity, IJsonRpcMessage message)
{
// noop if activity is null
propagator.Inject(activity, message, InjectContext);
}

private static void InjectContext(object? message, string key, string value)
{
JsonNode? parameters = null;
switch (message)
{
case JsonRpcRequest request:
parameters = request.Params;
break;

case JsonRpcNotification notification:
parameters = notification.Params;
break;

default:
break;
}

if (parameters is JsonObject jsonObject && jsonObject[key] == null)
{
jsonObject[key] = value;
}
}

internal static bool ShouldInstrumentMessage(IJsonRpcMessage message) =>
ActivitySource.HasListeners() &&
message switch
{
JsonRpcRequest => true,
JsonRpcNotification notification => notification.Method != NotificationMethods.LoggingMessageNotification,
_ => false
};

internal static ActivityLink[] ActivityLinkFromCurrent() => Activity.Current is null ? [] : [new ActivityLink(Activity.Current.Context)];
}
Loading