Skip to content

Commit f44c23e

Browse files
authored
feat(telemetry): Switch to Application Insights SDK for full Usage analytics (#243)
Replace OpenTelemetry with Application Insights SDK to enable Users, Sessions, Funnels, and User Flows analytics in Azure portal. Changes: - Remove OpenTelemetry packages - Use Microsoft.ApplicationInsights SDK directly (v2.23.0) - Add ExcelMcpTelemetryInitializer for User.Id/Session.Id context - Simplify ExcelMcpTelemetry to use TelemetryClient.TrackEvent() - Convert to static SensitiveDataRedactor - Update .NET SDK to 8.0.416 Fixes #239
1 parent b3ca0db commit f44c23e

File tree

9 files changed

+307
-177
lines changed

9 files changed

+307
-177
lines changed

Directory.Packages.props

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
<PackageVersion Include="Microsoft.AnalysisServices" Version="19.106.1" />
99
<!-- UI Framework -->
1010
<PackageVersion Include="Spectre.Console" Version="0.54.0" />
11-
<PackageVersion Include="Spectre.Console.Cli" Version="0.53.0" />
11+
<PackageVersion Include="Spectre.Console.Cli" Version="0.53.1" />
1212
<!-- Microsoft Extensions for MCP Server -->
1313
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.10" />
1414
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.10" />
1515
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
16-
<!-- OpenTelemetry for Application Insights -->
17-
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.3.0" /> <PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.11.2" /> <PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.11.2" />
16+
<!-- Application Insights SDK for telemetry (Users, Sessions, Funnels, User Flows) -->
17+
<PackageVersion Include="Microsoft.ApplicationInsights" Version="2.23.0" />
1818
<PackageVersion Include="Microsoft.Extensions.Resilience" Version="10.0.0" />
1919
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.10" />
2020
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.10" />
@@ -37,4 +37,4 @@
3737
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.5" />
3838
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.100" />
3939
</ItemGroup>
40-
</Project>
40+
</Project>

global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"sdk": {
3-
"version": "8.0.414",
3+
"version": "8.0.416",
44
"rollForward": "latestFeature"
55
}
66
}

src/ExcelMcp.McpServer/ExcelMcp.McpServer.csproj

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,8 @@
7979
<PackageReference Include="ModelContextProtocol" />
8080
<PackageReference Include="Microsoft.Extensions.Hosting" />
8181
<PackageReference Include="Microsoft.Extensions.Logging" />
82-
<!-- OpenTelemetry for Application Insights telemetry -->
83-
<PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" />
84-
<PackageReference Include="OpenTelemetry.Exporter.Console" />
85-
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
82+
<!-- Application Insights SDK for telemetry (Users, Sessions, Funnels, User Flows) -->
83+
<PackageReference Include="Microsoft.ApplicationInsights" />
8684
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers">
8785
<PrivateAssets>all</PrivateAssets>
8886
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>

src/ExcelMcp.McpServer/Program.cs

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
using Azure.Monitor.OpenTelemetry.Exporter;
1+
using Microsoft.ApplicationInsights;
2+
using Microsoft.ApplicationInsights.Extensibility;
23
using Microsoft.Extensions.DependencyInjection;
34
using Microsoft.Extensions.Hosting;
45
using Microsoft.Extensions.Logging;
5-
using OpenTelemetry.Trace;
66
using Sbroenne.ExcelMcp.McpServer.Telemetry;
77

88
namespace Sbroenne.ExcelMcp.McpServer;
@@ -49,7 +49,8 @@ public static async Task Main(string[] args)
4949
}
5050

5151
/// <summary>
52-
/// Configures OpenTelemetry with Azure Monitor exporter for Application Insights.
52+
/// Configures Application Insights SDK for telemetry.
53+
/// Enables Users/Sessions/Funnels/User Flows analytics in Azure Portal.
5354
/// Respects opt-out via EXCELMCP_TELEMETRY_OPTOUT environment variable.
5455
/// </summary>
5556
private static void ConfigureTelemetry(HostApplicationBuilder builder)
@@ -69,29 +70,34 @@ private static void ConfigureTelemetry(HostApplicationBuilder builder)
6970
return; // No connection string available and not in debug mode
7071
}
7172

72-
// Configure OpenTelemetry
73-
builder.Services.AddOpenTelemetry()
74-
.WithTracing(tracing =>
75-
{
76-
tracing
77-
.AddSource(ExcelMcpTelemetry.ActivitySource.Name)
78-
.AddProcessor(new SensitiveDataRedactingProcessor());
79-
80-
if (isDebugMode)
81-
{
82-
// Debug mode: write to stderr (console) for local testing
83-
tracing.AddConsoleExporter();
84-
Console.Error.WriteLine("[Telemetry] Debug mode enabled - logging to stderr");
85-
}
86-
else
87-
{
88-
// Production: send to Azure Monitor
89-
tracing.AddAzureMonitorTraceExporter(options =>
90-
{
91-
options.ConnectionString = connectionString;
92-
});
93-
}
94-
});
73+
// Configure Application Insights SDK
74+
var aiConfig = TelemetryConfiguration.CreateDefault();
75+
if (!string.IsNullOrEmpty(connectionString))
76+
{
77+
aiConfig.ConnectionString = connectionString;
78+
}
79+
else if (isDebugMode)
80+
{
81+
// Debug mode without connection string - telemetry will be tracked but not sent
82+
// This allows testing the tracking code without Azure resources
83+
Console.Error.WriteLine("[Telemetry] Debug mode enabled - telemetry tracked locally (no Azure connection)");
84+
}
85+
86+
// Add initializer to set User.Id and Session.Id on all telemetry
87+
aiConfig.TelemetryInitializers.Add(new ExcelMcpTelemetryInitializer());
88+
89+
// Register TelemetryClient as singleton for dependency injection
90+
var telemetryClient = new TelemetryClient(aiConfig);
91+
builder.Services.AddSingleton(telemetryClient);
92+
builder.Services.AddSingleton(aiConfig);
93+
94+
// Store reference for static access in ExcelMcpTelemetry
95+
ExcelMcpTelemetry.SetTelemetryClient(telemetryClient);
96+
97+
if (isDebugMode)
98+
{
99+
Console.Error.WriteLine($"[Telemetry] Application Insights configured - User.Id={ExcelMcpTelemetry.UserId}, Session.Id={ExcelMcpTelemetry.SessionId}");
100+
}
95101
}
96102

97103
/// <summary>

src/ExcelMcp.McpServer/Telemetry/ExcelMcpTelemetry.cs

Lines changed: 96 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,17 @@
11
// Copyright (c) Sbroenne. All rights reserved.
22
// Licensed under the MIT License.
33

4-
using System.Diagnostics;
54
using System.Reflection;
5+
using Microsoft.ApplicationInsights;
66

77
namespace Sbroenne.ExcelMcp.McpServer.Telemetry;
88

99
/// <summary>
1010
/// Centralized telemetry helper for ExcelMcp MCP Server.
11-
/// Provides usage tracking and unhandled exception reporting via OpenTelemetry.
11+
/// Provides usage tracking and unhandled exception reporting via Application Insights SDK.
1212
/// </summary>
1313
public static class ExcelMcpTelemetry
1414
{
15-
/// <summary>
16-
/// The ActivitySource for creating traces.
17-
/// </summary>
18-
public static readonly ActivitySource ActivitySource = new("ExcelMcp.McpServer", GetVersion());
19-
2015
/// <summary>
2116
/// Environment variable to opt-out of telemetry.
2217
/// Set to "true" or "1" to disable telemetry.
@@ -40,9 +35,32 @@ public static class ExcelMcpTelemetry
4035

4136
/// <summary>
4237
/// Unique session ID for correlating telemetry within a single MCP server process.
38+
/// Changes each time the MCP server starts.
4339
/// </summary>
4440
public static readonly string SessionId = Guid.NewGuid().ToString("N")[..8];
4541

42+
/// <summary>
43+
/// Stable anonymous user ID based on machine identity.
44+
/// Persists across sessions for the same machine, enabling user-level analytics
45+
/// without collecting personally identifiable information.
46+
/// </summary>
47+
public static readonly string UserId = GenerateAnonymousUserId();
48+
49+
/// <summary>
50+
/// Application Insights TelemetryClient for sending Custom Events.
51+
/// Enables Users/Sessions analytics in Azure Portal.
52+
/// </summary>
53+
private static TelemetryClient? _telemetryClient;
54+
55+
/// <summary>
56+
/// Sets the TelemetryClient instance for sending Custom Events.
57+
/// Called by Program.cs during startup.
58+
/// </summary>
59+
internal static void SetTelemetryClient(TelemetryClient client)
60+
{
61+
_telemetryClient = client;
62+
}
63+
4664
private static bool? _isEnabled;
4765

4866
/// <summary>
@@ -89,6 +107,7 @@ public static bool IsOptedOut()
89107

90108
/// <summary>
91109
/// Tracks a tool invocation with usage metrics.
110+
/// Sends Application Insights Custom Event for Users/Sessions analytics.
92111
/// </summary>
93112
/// <param name="toolName">The MCP tool name (e.g., "excel_range")</param>
94113
/// <param name="action">The action performed (e.g., "get-values")</param>
@@ -98,60 +117,72 @@ public static void TrackToolInvocation(string toolName, string action, long dura
98117
{
99118
if (!IsEnabled) return;
100119

101-
using var activity = ActivitySource.StartActivity("ToolInvocation", ActivityKind.Internal);
102-
if (activity == null) return;
120+
// Debug mode: write to stderr
121+
if (IsDebugMode())
122+
{
123+
Console.Error.WriteLine($"[Telemetry] ToolInvocation: {toolName}.{action} - {(success ? "Success" : "Failed")} ({durationMs}ms)");
124+
}
125+
126+
// Send Application Insights Custom Event (populates customEvents table)
127+
if (_telemetryClient != null)
128+
{
129+
var properties = new Dictionary<string, string>
130+
{
131+
{ "ToolName", toolName },
132+
{ "Action", action },
133+
{ "Success", success.ToString() },
134+
{ "AppVersion", GetVersion() }
135+
};
103136

104-
activity.SetTag("tool.name", toolName);
105-
activity.SetTag("tool.action", action);
106-
activity.SetTag("tool.duration_ms", durationMs);
107-
activity.SetTag("tool.success", success);
108-
activity.SetTag("session.id", SessionId);
109-
activity.SetTag("app.version", GetVersion());
137+
var metrics = new Dictionary<string, double>
138+
{
139+
{ "DurationMs", durationMs }
140+
};
110141

111-
activity.SetStatus(success ? ActivityStatusCode.Ok : ActivityStatusCode.Error);
142+
_telemetryClient.TrackEvent("ToolInvocation", properties, metrics);
143+
}
112144
}
113145

114146
/// <summary>
115147
/// Tracks an unhandled exception.
116148
/// Only call this for exceptions that escape all catch blocks (true bugs/crashes).
149+
/// Sends Application Insights exception and Custom Event.
117150
/// </summary>
118151
/// <param name="exception">The unhandled exception</param>
119152
/// <param name="source">Source of the exception (e.g., "AppDomain.UnhandledException")</param>
120153
public static void TrackUnhandledException(Exception exception, string source)
121154
{
122155
if (!IsEnabled || exception == null) return;
123156

124-
using var activity = ActivitySource.StartActivity("UnhandledException", ActivityKind.Internal);
125-
if (activity == null) return;
126-
127157
// Redact sensitive data from exception
128-
var (type, message, stackTrace) = SensitiveDataRedactingProcessor.RedactException(exception);
129-
130-
activity.SetTag("exception.type", type);
131-
activity.SetTag("exception.message", message);
132-
activity.SetTag("exception.source", source);
133-
activity.SetTag("session.id", SessionId);
134-
activity.SetTag("app.version", GetVersion());
158+
var (type, message, _) = SensitiveDataRedactor.RedactException(exception);
135159

136-
if (stackTrace != null)
160+
// Debug mode: write to stderr
161+
if (IsDebugMode())
137162
{
138-
// Truncate stack trace to avoid exceeding limits
139-
const int maxStackTraceLength = 4096;
140-
if (stackTrace.Length > maxStackTraceLength)
141-
{
142-
stackTrace = stackTrace[..maxStackTraceLength] + "... [truncated]";
143-
}
144-
activity.SetTag("exception.stacktrace", stackTrace);
163+
Console.Error.WriteLine($"[Telemetry] UnhandledException: {type} - {message} (Source: {source})");
145164
}
146165

147-
activity.SetStatus(ActivityStatusCode.Error, message);
148-
149-
// Record as exception event
150-
activity.AddEvent(new ActivityEvent("exception", tags: new ActivityTagsCollection
166+
// Send Application Insights telemetry
167+
if (_telemetryClient != null)
151168
{
152-
{ "exception.type", type },
153-
{ "exception.message", message }
154-
}));
169+
// Track as exception in Application Insights (for Failures blade)
170+
_telemetryClient.TrackException(exception, new Dictionary<string, string>
171+
{
172+
{ "Source", source },
173+
{ "ExceptionType", type },
174+
{ "AppVersion", GetVersion() }
175+
});
176+
177+
// Also track as Custom Event (for Users/Sessions analytics)
178+
_telemetryClient.TrackEvent("UnhandledException", new Dictionary<string, string>
179+
{
180+
{ "Source", source },
181+
{ "ExceptionType", type },
182+
{ "Message", message ?? "Unknown" },
183+
{ "AppVersion", GetVersion() }
184+
});
185+
}
155186
}
156187

157188
/// <summary>
@@ -164,4 +195,28 @@ private static string GetVersion()
164195
?? Assembly.GetExecutingAssembly().GetName().Version?.ToString()
165196
?? "1.0.0";
166197
}
198+
199+
/// <summary>
200+
/// Generates a stable anonymous user ID based on machine identity.
201+
/// Uses a hash of machine name and user profile path to create a consistent
202+
/// identifier that persists across sessions without collecting PII.
203+
/// </summary>
204+
private static string GenerateAnonymousUserId()
205+
{
206+
try
207+
{
208+
// Combine machine-specific values that are stable but not personally identifiable
209+
var machineIdentity = $"{Environment.MachineName}|{Environment.UserName}|{Environment.OSVersion.Platform}";
210+
211+
// Create a SHA256 hash and take the first 16 characters
212+
var bytes = System.Text.Encoding.UTF8.GetBytes(machineIdentity);
213+
var hash = System.Security.Cryptography.SHA256.HashData(bytes);
214+
return Convert.ToHexString(hash)[..16].ToLowerInvariant();
215+
}
216+
catch
217+
{
218+
// Fallback to a random ID if machine identity cannot be determined
219+
return Guid.NewGuid().ToString("N")[..16];
220+
}
221+
}
167222
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) Sbroenne. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.ApplicationInsights.Channel;
5+
using Microsoft.ApplicationInsights.Extensibility;
6+
7+
namespace Sbroenne.ExcelMcp.McpServer.Telemetry;
8+
9+
/// <summary>
10+
/// Telemetry initializer that sets User.Id and Session.Id for Application Insights.
11+
/// This enables the Users and Sessions blades in the Azure Portal.
12+
/// </summary>
13+
public sealed class ExcelMcpTelemetryInitializer : ITelemetryInitializer
14+
{
15+
private readonly string _userId;
16+
private readonly string _sessionId;
17+
18+
/// <summary>
19+
/// Initializes a new instance of the <see cref="ExcelMcpTelemetryInitializer"/> class.
20+
/// </summary>
21+
public ExcelMcpTelemetryInitializer()
22+
{
23+
_userId = ExcelMcpTelemetry.UserId;
24+
_sessionId = ExcelMcpTelemetry.SessionId;
25+
}
26+
27+
/// <summary>
28+
/// Initializes the telemetry item with user and session context.
29+
/// </summary>
30+
/// <param name="telemetry">The telemetry item to initialize.</param>
31+
public void Initialize(ITelemetry telemetry)
32+
{
33+
// Set user context for Users blade
34+
if (string.IsNullOrEmpty(telemetry.Context.User.Id))
35+
{
36+
telemetry.Context.User.Id = _userId;
37+
}
38+
39+
// Set session context for Sessions blade
40+
if (string.IsNullOrEmpty(telemetry.Context.Session.Id))
41+
{
42+
telemetry.Context.Session.Id = _sessionId;
43+
}
44+
45+
// Set cloud role for better grouping in Application Map
46+
if (string.IsNullOrEmpty(telemetry.Context.Cloud.RoleName))
47+
{
48+
telemetry.Context.Cloud.RoleName = "ExcelMcp.McpServer";
49+
}
50+
}
51+
}

0 commit comments

Comments
 (0)