Skip to content

Commit af77873

Browse files
committed
Collect metrics for session count and query count
1 parent e64f32e commit af77873

File tree

3 files changed

+285
-0
lines changed

3 files changed

+285
-0
lines changed

shell/AIShell.Kernel/AIShell.Kernel.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<ItemGroup>
99
<PackageReference Include="Spectre.Console.Json" Version="0.50.0" />
1010
<PackageReference Include="ModelContextProtocol.Core" Version="0.2.0-preview.3" />
11+
<PackageReference Include="Microsoft.ApplicationInsights" Version="2.23.0" />
1112
</ItemGroup>
1213

1314
<ItemGroup>

shell/AIShell.Kernel/Shell.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ internal Shell(bool interactive, ShellArgs args)
144144
{
145145
ShowLandingPage();
146146
}
147+
148+
Telemetry.TrackSession();
147149
}
148150

149151
internal void ShowBanner()
@@ -678,6 +680,8 @@ internal async Task RunREPLAsync()
678680
.MarkupWarningLine($"[[{Utils.AppName}]]: Agent self-check failed. Resolve the issue as instructed and try again.")
679681
.MarkupWarningLine($"[[{Utils.AppName}]]: Run {Formatter.Command($"/agent config {agent.Impl.Name}")} to edit the settings for the agent.");
680682
}
683+
684+
Telemetry.TrackQuery(agent.Impl.Name);
681685
}
682686
catch (Exception ex)
683687
{
@@ -741,6 +745,8 @@ internal async Task RunOnceAsync(string prompt)
741745
{
742746
await _activeAgent.Impl.RefreshChatAsync(this, force: false);
743747
await _activeAgent.Impl.ChatAsync(prompt, this);
748+
749+
Telemetry.TrackQuery(_activeAgent.Impl.Name);
744750
}
745751
catch (OperationCanceledException)
746752
{
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
using Microsoft.ApplicationInsights;
2+
using Microsoft.ApplicationInsights.Channel;
3+
using Microsoft.ApplicationInsights.Extensibility;
4+
using Microsoft.ApplicationInsights.Extensibility.Implementation;
5+
using Microsoft.ApplicationInsights.Metrics;
6+
7+
namespace AIShell.Kernel;
8+
9+
internal class Telemetry
10+
{
11+
private const string TelemetryFailure = "TELEMETRY_FAILURE";
12+
private const string DefaultUUID = "a586d96e-f941-406c-b87d-5b67e8bc2fcb";
13+
private const string MetricNamespace = "aishell.telemetry";
14+
15+
private static readonly TelemetryClient s_client;
16+
private static readonly string s_os, s_uniqueId;
17+
private static readonly MetricIdentifier s_sessionCount, s_queryCount;
18+
private static readonly HashSet<string> s_knownAgents;
19+
20+
private static bool s_enabled = false;
21+
22+
static Telemetry()
23+
{
24+
s_enabled = !GetEnvironmentVariableAsBool(
25+
name: "AISHELL_TELEMETRY_OPTOUT",
26+
defaultValue: false);
27+
28+
if (s_enabled)
29+
{
30+
var config = TelemetryConfiguration.CreateDefault();
31+
config.ConnectionString = "InstrumentationKey=b273044e-f4af-4a1d-bb8a-ad1fe7ac4cad;IngestionEndpoint=https://centralus-2.in.applicationinsights.azure.com/;LiveEndpoint=https://centralus.livediagnostics.monitor.azure.com/;ApplicationId=1cccb480-3eff-41a0-baad-906cca2cfadb";
32+
config.TelemetryChannel.DeveloperMode = false;
33+
config.TelemetryInitializers.Add(new NameObscurerTelemetryInitializer());
34+
35+
s_client = new TelemetryClient(config);
36+
s_uniqueId = GetUniqueIdentifier().ToString();
37+
s_os = OperatingSystem.IsWindows()
38+
? "Windows"
39+
: OperatingSystem.IsMacOS() ? "macOS" : "Linux";
40+
41+
s_sessionCount = new MetricIdentifier(MetricNamespace, "SessionCount", "uuid", "os");
42+
s_queryCount = new MetricIdentifier(MetricNamespace, "QueryCount", "uuid", "agent");
43+
s_knownAgents = ["openai-gpt", "azure", "interpreter", "ollama", "PhiSilica"];
44+
}
45+
}
46+
47+
/// <summary>
48+
/// Retrieve the unique identifier from the persisted file, if it doesn't exist create it.
49+
/// Generate a guid which will be used as the UUID.
50+
/// </summary>
51+
/// <returns>A guid which represents the unique identifier.</returns>
52+
private static Guid GetUniqueIdentifier()
53+
{
54+
// Try to get the unique id.
55+
// If this returns false, we'll create/recreate the 'aishell.uuid' file.
56+
string uuidPath = Path.Join(Utils.AppCacheDir, "aishell.uuid");
57+
if (TryGetIdentifier(uuidPath, out Guid id))
58+
{
59+
return id;
60+
}
61+
62+
try
63+
{
64+
// Multiple AIShell processes may (unlikely though) start simultaneously so we need
65+
// a system-wide way to control access to the file in that rare case.
66+
using var m = new Mutex(true, "AIShell_CreateUniqueUserId");
67+
m.WaitOne();
68+
try
69+
{
70+
return CreateUniqueIdAndFile(uuidPath);
71+
}
72+
finally
73+
{
74+
m.ReleaseMutex();
75+
}
76+
}
77+
catch (Exception)
78+
{
79+
// The method 'CreateUniqueIdAndFile' shouldn't throw, but the mutex might.
80+
// Any problem in generating a uuid will result in no telemetry being sent.
81+
// Try to send the failure in telemetry without the unique id.
82+
s_client.GetMetric(TelemetryFailure, "detail").TrackValue(1, "mutex");
83+
}
84+
85+
// Something bad happened, turn off telemetry since the unique id wasn't set.
86+
s_enabled = false;
87+
return id;
88+
}
89+
90+
/// <summary>
91+
/// Try to read the file and collect the guid.
92+
/// </summary>
93+
/// <param name="telemetryFilePath">The path to the telemetry file.</param>
94+
/// <param name="id">The newly created id.</param>
95+
/// <returns>The method returns a bool indicating success or failure of creating the id.</returns>
96+
private static bool TryGetIdentifier(string telemetryFilePath, out Guid id)
97+
{
98+
if (File.Exists(telemetryFilePath))
99+
{
100+
// attempt to read the persisted identifier
101+
const int GuidSize = 16;
102+
byte[] buffer = new byte[GuidSize];
103+
try
104+
{
105+
using FileStream fs = new(telemetryFilePath, FileMode.Open, FileAccess.Read);
106+
107+
// If the read is invalid, or wrong size, we return it
108+
int n = fs.Read(buffer, 0, GuidSize);
109+
if (n is GuidSize)
110+
{
111+
id = new Guid(buffer);
112+
if (id != Guid.Empty)
113+
{
114+
return true;
115+
}
116+
}
117+
}
118+
catch
119+
{
120+
// something went wrong, the file may not exist or not have enough bytes, so return false
121+
}
122+
}
123+
124+
id = Guid.Empty;
125+
return false;
126+
}
127+
128+
/// <summary>
129+
/// Try to create a unique identifier and persist it to the telemetry.uuid file.
130+
/// </summary>
131+
/// <param name="telemetryFilePath">The path to the persisted telemetry.uuid file.</param>
132+
/// <returns>The method node id.</returns>
133+
private static Guid CreateUniqueIdAndFile(string telemetryFilePath)
134+
{
135+
// One last attempt to retrieve before creating incase we have a lot of simultaneous entry into the mutex.
136+
if (TryGetIdentifier(telemetryFilePath, out Guid id))
137+
{
138+
return id;
139+
}
140+
141+
// The directory may not exist, so attempt to create it
142+
// CreateDirectory will simply return the directory if exists
143+
bool attemptFileCreation = true;
144+
try
145+
{
146+
Directory.CreateDirectory(Path.GetDirectoryName(telemetryFilePath));
147+
}
148+
catch
149+
{
150+
// There was a problem in creating the directory for the file, do not attempt to create the file.
151+
// We don't send telemetry here because there are valid reasons for the directory to not exist
152+
// and not be able to be created.
153+
attemptFileCreation = false;
154+
}
155+
156+
// If we were able to create the directory, try to create the file,
157+
// if this fails we will send telemetry to indicate this and then use the default identifier.
158+
if (attemptFileCreation)
159+
{
160+
try
161+
{
162+
id = Guid.NewGuid();
163+
File.WriteAllBytes(telemetryFilePath, id.ToByteArray());
164+
return id;
165+
}
166+
catch
167+
{
168+
// another bit of telemetry to notify us about a problem with saving the unique id.
169+
s_client.GetMetric(TelemetryFailure, "detail").TrackValue(1, "saveuuid");
170+
}
171+
}
172+
173+
// all attempts to create an identifier have failed, so use the default node id.
174+
id = new Guid(DefaultUUID);
175+
return id;
176+
}
177+
178+
/// <summary>
179+
/// Determine whether the environment variable is set and how.
180+
/// </summary>
181+
/// <param name="name">The name of the environment variable.</param>
182+
/// <param name="defaultValue">If the environment variable is not set, use this as the default value.</param>
183+
/// <returns>A boolean representing the value of the environment variable.</returns>
184+
private static bool GetEnvironmentVariableAsBool(string name, bool defaultValue)
185+
{
186+
var str = Environment.GetEnvironmentVariable(name);
187+
if (string.IsNullOrEmpty(str))
188+
{
189+
return defaultValue;
190+
}
191+
192+
var boolStr = str.AsSpan();
193+
if (boolStr.Length == 1)
194+
{
195+
if (boolStr[0] == '1')
196+
{
197+
return true;
198+
}
199+
200+
if (boolStr[0] == '0')
201+
{
202+
return false;
203+
}
204+
}
205+
206+
if (boolStr.Length == 3 &&
207+
(boolStr[0] == 'y' || boolStr[0] == 'Y') &&
208+
(boolStr[1] == 'e' || boolStr[1] == 'E') &&
209+
(boolStr[2] == 's' || boolStr[2] == 'S'))
210+
{
211+
return true;
212+
}
213+
214+
if (boolStr.Length == 2 &&
215+
(boolStr[0] == 'n' || boolStr[0] == 'N') &&
216+
(boolStr[1] == 'o' || boolStr[1] == 'O'))
217+
{
218+
return false;
219+
}
220+
221+
if (boolStr.Length == 4 &&
222+
(boolStr[0] == 't' || boolStr[0] == 'T') &&
223+
(boolStr[1] == 'r' || boolStr[1] == 'R') &&
224+
(boolStr[2] == 'u' || boolStr[2] == 'U') &&
225+
(boolStr[3] == 'e' || boolStr[3] == 'E'))
226+
{
227+
return true;
228+
}
229+
230+
if (boolStr.Length == 5 &&
231+
(boolStr[0] == 'f' || boolStr[0] == 'F') &&
232+
(boolStr[1] == 'a' || boolStr[1] == 'A') &&
233+
(boolStr[2] == 'l' || boolStr[2] == 'L') &&
234+
(boolStr[3] == 's' || boolStr[3] == 'S') &&
235+
(boolStr[4] == 'e' || boolStr[4] == 'E'))
236+
{
237+
return false;
238+
}
239+
240+
return defaultValue;
241+
}
242+
243+
internal static void TrackSession()
244+
{
245+
if (s_enabled)
246+
{
247+
s_client.GetMetric(s_sessionCount).TrackValue(1.0, s_uniqueId, s_os);
248+
}
249+
}
250+
251+
internal static void TrackQuery(string agentName)
252+
{
253+
if (s_enabled && s_knownAgents.Contains(agentName))
254+
{
255+
s_client.GetMetric(s_queryCount).TrackValue(1.0, s_uniqueId, agentName);
256+
}
257+
}
258+
}
259+
260+
/// <summary>
261+
/// Set up the telemetry initializer to mask the platform specific names.
262+
/// </summary>
263+
internal class NameObscurerTelemetryInitializer : ITelemetryInitializer
264+
{
265+
// Report the platform name information as "na".
266+
private const string NotAvailable = "na";
267+
268+
/// <summary>
269+
/// Initialize properties we are obscuring to "na".
270+
/// </summary>
271+
/// <param name="telemetry">The instance of our telemetry.</param>
272+
public void Initialize(ITelemetry telemetry)
273+
{
274+
telemetry.Context.Cloud.RoleName = NotAvailable;
275+
telemetry.Context.GetInternalContext().NodeName = NotAvailable;
276+
telemetry.Context.Cloud.RoleInstance = NotAvailable;
277+
}
278+
}

0 commit comments

Comments
 (0)