-
Notifications
You must be signed in to change notification settings - Fork 43
Expand file tree
/
Copy pathMcpToolTelemetry.cs
More file actions
131 lines (111 loc) · 4.68 KB
/
McpToolTelemetry.cs
File metadata and controls
131 lines (111 loc) · 4.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information
using System.Diagnostics;
using System.Reflection;
using Elastic.Documentation.Configuration;
using Microsoft.Extensions.Logging;
namespace Elastic.Documentation.Mcp.Remote.Telemetry;
public static class McpToolTelemetry
{
internal const string McpToolSourceName = "Elastic.Documentation.Api.McpTools";
private static readonly ActivitySource McpActivitySource = new(McpToolSourceName);
private static readonly McpServerProfile ServerProfile = ResolveServerProfile();
private static readonly string? ServerVersion = ResolveServerVersion();
public static string ResolveToolName(string template) =>
template
.Replace("{resource}", ServerProfile.ResourceNoun, StringComparison.Ordinal)
.Replace("{scope}", ServerProfile.ScopePrefix, StringComparison.Ordinal);
public static Activity? StartActivity(string toolName)
{
var activity = McpActivitySource.StartActivity($"mcp.tool.{toolName}", ActivityKind.Internal);
_ = activity?.SetTag("mcp.tool.name", toolName);
_ = activity?.SetTag("mcp.server.profile", ServerProfile.Name);
if (!string.IsNullOrWhiteSpace(ServerVersion))
_ = activity?.SetTag("mcp.server.version", ServerVersion);
return activity;
}
public static PayloadMetadata SetPayloadMetadata(Activity? activity, IReadOnlyDictionary<string, object?> arguments)
{
var argumentKeys = arguments.Keys
.OrderBy(k => k, StringComparer.Ordinal)
.ToArray();
var argKeys = string.Join(",", argumentKeys);
_ = activity?.SetTag("mcp.payload.arg_count", argumentKeys.Length);
_ = activity?.SetTag("mcp.payload.arg_keys", argKeys);
foreach (var kvp in arguments.Where(kvp => kvp.Value is string))
{
if (kvp.Value is string value)
_ = activity?.SetTag($"mcp.payload.{kvp.Key}.length", value.Length);
}
return new PayloadMetadata(argumentKeys.Length, argKeys);
}
public static void MarkSuccess(Activity? activity)
{
_ = activity?.SetTag("mcp.call.success", true);
_ = activity?.SetStatus(ActivityStatusCode.Ok);
}
public static void MarkFailure(Activity? activity, Exception ex)
{
_ = activity?.SetTag("mcp.call.success", false);
_ = activity?.SetTag("mcp.call.error_type", ex.GetType().FullName);
_ = activity?.SetTag("error.message", ex.Message);
_ = activity?.AddException(ex);
_ = activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
FailServerSpan(ex);
}
// Walk up to the ASP.NET Core server (transaction) span and mark it failed so that
// the HTTP transaction appears as a failing transaction in APM even though the HTTP
// response is 200 (correct per MCP/JSON-RPC spec). No-ops when there is no server
// ancestor, e.g. in unit tests.
private static void FailServerSpan(Exception ex)
{
for (var a = Activity.Current; a is not null; a = a.Parent)
{
if (a.Kind != ActivityKind.Server)
continue;
_ = a.SetStatus(ActivityStatusCode.Error, ex.Message);
_ = a.AddException(ex);
return;
}
}
public static void MarkFailure(Activity? activity, string errorType, string message)
{
_ = activity?.SetTag("mcp.call.success", false);
_ = activity?.SetTag("mcp.call.error_type", errorType);
_ = activity?.SetTag("error.message", message);
_ = activity?.SetStatus(ActivityStatusCode.Error, message);
}
public static void MarkCancelled(Activity? activity)
{
_ = activity?.SetTag("mcp.call.success", false);
_ = activity?.SetTag("mcp.call.cancelled", true);
_ = activity?.SetStatus(ActivityStatusCode.Error, "cancelled");
}
public static void LogStart(ILogger logger, string toolName, PayloadMetadata metadata) =>
logger.LogInformation(
"MCP tool call started {ToolName} (profile={Profile}, arg_count={ArgCount}, arg_keys={ArgKeys})",
toolName,
ServerProfile.Name,
metadata.ArgCount,
metadata.ArgKeys);
public static void LogCompletion(ILogger logger, string toolName, long durationMs, string outcome) =>
logger.LogInformation(
"MCP tool call completed {ToolName} (profile={Profile}, duration_ms={DurationMs}, outcome={Outcome})",
toolName,
ServerProfile.Name,
durationMs,
outcome);
private static McpServerProfile ResolveServerProfile()
{
var configuredProfile = SystemEnvironmentVariables.Instance.McpServerProfile;
return McpServerProfile.Resolve(configuredProfile);
}
private static string? ResolveServerVersion()
{
var informationalVersion = Assembly.GetExecutingAssembly()
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
return informationalVersion?.Split(['+', '-'])[0];
}
}
public readonly record struct PayloadMetadata(int ArgCount, string ArgKeys);