Skip to content

Commit f97c7a9

Browse files
authored
Add two telemetry metrics to AIShell to get an idea of the usage (#416)
Add 2 telemetry metrics to AIShell, which can be opted out by setting the environment var `AISHELL_TELEMETRY_OPTOUT` to `1` or `true`. - `SessionCount`: tracking the number of sessions get started. It sends custom dimensions `uuid`, `os`, and `standalone`, so we will know the unique users, the platforms, and whether a session is standalone. - `QueryCount`: tracking the number of queries get asked. It sends custom dimensions `uuid`, `agent`, and `remote`, so we will now the distribution of the queries based on unique users, agents, and whether it's a remote query.
1 parent e64f32e commit f97c7a9

File tree

4 files changed

+291
-1
lines changed

4 files changed

+291
-1
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: 8 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(standalone: Channel is null);
147149
}
148150

149151
internal void ShowBanner()
@@ -593,6 +595,7 @@ internal async Task RunREPLAsync()
593595
while (!Exit)
594596
{
595597
string input = null;
598+
bool isRemoteQuery = false;
596599
LLMAgent agent = _activeAgent;
597600

598601
try
@@ -612,6 +615,7 @@ internal async Task RunREPLAsync()
612615
// Write out the remote query, in the same style as user typing.
613616
Host.Markup($"\n>> Remote Query Received:\n");
614617
Host.MarkupLine($"[teal]{input.EscapeMarkup()}[/]");
618+
isRemoteQuery = true;
615619
}
616620
else
617621
{
@@ -678,6 +682,8 @@ internal async Task RunREPLAsync()
678682
.MarkupWarningLine($"[[{Utils.AppName}]]: Agent self-check failed. Resolve the issue as instructed and try again.")
679683
.MarkupWarningLine($"[[{Utils.AppName}]]: Run {Formatter.Command($"/agent config {agent.Impl.Name}")} to edit the settings for the agent.");
680684
}
685+
686+
Telemetry.TrackQuery(agent.Impl.Name, isRemoteQuery);
681687
}
682688
catch (Exception ex)
683689
{
@@ -741,6 +747,8 @@ internal async Task RunOnceAsync(string prompt)
741747
{
742748
await _activeAgent.Impl.RefreshChatAsync(this, force: false);
743749
await _activeAgent.Impl.ChatAsync(prompt, this);
750+
751+
Telemetry.TrackQuery(_activeAgent.Impl.Name, isRemote: false);
744752
}
745753
catch (OperationCanceledException)
746754
{
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", "standalone");
42+
s_queryCount = new MetricIdentifier(MetricNamespace, "QueryCount", "uuid", "agent", "remote");
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(bool standalone)
244+
{
245+
if (s_enabled)
246+
{
247+
s_client.GetMetric(s_sessionCount).TrackValue(1.0, s_uniqueId, s_os, standalone ? "true" : "false");
248+
}
249+
}
250+
251+
internal static void TrackQuery(string agentName, bool isRemote)
252+
{
253+
if (s_enabled && s_knownAgents.Contains(agentName))
254+
{
255+
s_client.GetMetric(s_queryCount).TrackValue(1.0, s_uniqueId, agentName, isRemote ? "true" : "false");
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+
}

shell/agents/Microsoft.Azure.Agent/Microsoft.Azure.Agent.csproj

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@
2424

2525
<ItemGroup>
2626
<PackageReference Include="Azure.Identity" Version="1.14.2" />
27-
<PackageReference Include="Microsoft.ApplicationInsights" Version="2.23.0" />
2827
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
2928
<PackageReference Include="Serilog.Sinks.Async" Version="2.0.0" />
29+
<PackageReference Include="Microsoft.ApplicationInsights" Version="2.23.0">
30+
<ExcludeAssets>contentFiles</ExcludeAssets>
31+
<PrivateAssets>All</PrivateAssets>
32+
</PackageReference>
3033
<PackageReference Include="System.Management.Automation" Version="7.4.7">
3134
<ExcludeAssets>contentFiles</ExcludeAssets>
3235
<PrivateAssets>All</PrivateAssets>

0 commit comments

Comments
 (0)