|
| 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 | +} |
0 commit comments