From c0c8b84240f35a53df81c28c9e6c9c24d7ae9a46 Mon Sep 17 00:00:00 2001 From: Maoliang Huang Date: Wed, 28 Oct 2020 16:41:15 -0700 Subject: [PATCH 1/5] Collect user id and mac address. - We hash the user account id and the mac address, and collect those in the telemetry. These are used to find how many users are using this mdoule. - We also send the hashed user account id in the http request header. This is used to enable throttling by user account. --- .../AzPredictorTests.cs | 3 +- .../Az.Tools.Predictor.csproj | 1 + .../Az.Tools.Predictor/AzContext.cs | 177 ++++++++++++++++++ .../Az.Tools.Predictor/AzPredictor.cs | 16 +- .../Az.Tools.Predictor/AzPredictorService.cs | 36 +++- .../AzPredictorTelemetryClient.cs | 19 +- .../Az.Tools.Predictor/IAzContext.cs | 44 +++++ 7 files changed, 284 insertions(+), 12 deletions(-) create mode 100644 tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs create mode 100644 tools/Az.Tools.Predictor/Az.Tools.Predictor/IAzContext.cs diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor.Test/AzPredictorTests.cs b/tools/Az.Tools.Predictor/Az.Tools.Predictor.Test/AzPredictorTests.cs index c1d76ab0aae2..c6c030053933 100644 --- a/tools/Az.Tools.Predictor/Az.Tools.Predictor.Test/AzPredictorTests.cs +++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor.Test/AzPredictorTests.cs @@ -45,7 +45,8 @@ public AzPredictorTests(ModelFixture modelFixture) this._azPredictor = new AzPredictor(this._service, this._telemetryClient, new Settings() { SuggestionCount = 1, - }); + }, + null); } /// diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/Az.Tools.Predictor.csproj b/tools/Az.Tools.Predictor/Az.Tools.Predictor/Az.Tools.Predictor.csproj index caa40859af4d..180c7118811b 100644 --- a/tools/Az.Tools.Predictor/Az.Tools.Predictor/Az.Tools.Predictor.csproj +++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/Az.Tools.Predictor.csproj @@ -33,6 +33,7 @@ + diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs new file mode 100644 index 000000000000..f10e4f78d72b --- /dev/null +++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs @@ -0,0 +1,177 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.Management.Automation; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Net.NetworkInformation; +using System.Security.Cryptography; +using System.Text; + +namespace Microsoft.Azure.PowerShell.Tools.AzPredictor +{ + using PowerShell = System.Management.Automation.PowerShell; + + /// + /// The class for the current Azure PowerShell context. + /// + internal sealed class AzContext : IAzContext + { + /// + public Version AzVersion { get; private set; } = new Version("0.0.0.0"); + + /// + public string HashedUserId { get; private set; } = string.Empty; + + private string _cachedHashedMacAddress; + /// + public string HashedMacAddress + { + get + { + if (_cachedHashedMacAddress == null) + { + _cachedHashedMacAddress = string.Empty; + + var macAddress = GetMACAddress(); + if (!string.IsNullOrWhiteSpace(macAddress)) + { + _cachedHashedMacAddress = GenerateSha256HashString(macAddress)?.Replace("-", string.Empty).ToLowerInvariant(); + } + } + + return _cachedHashedMacAddress; + } + } + + /// + public void UpdateContext() + { + AzVersion = GetAzVersion(); + HashedUserId = GenerateSha256HashString(GetUserAccountId()); + } + + /// + /// Gets the latest version from the loaded Az modules. + /// + private Version GetAzVersion() + { + Version defaultVersion = new Version("0.0.0"); + + Version latestAz = defaultVersion; + + try + { + var outputs = AzContext.ExecuteScript("Get-Module -Name Az -ListAvailable"); + foreach (PSObject obj in outputs) + { + string psVersion = obj.Properties["Version"].Value.ToString(); + int pos = psVersion.IndexOf('-'); + Version currentAz = (pos == -1) ? new Version(psVersion) : new Version(psVersion.Substring(0, pos)); + if (currentAz > latestAz) + { + latestAz = currentAz; + } + } + } + catch (Exception) + { + } + + return latestAz; + } + + /// + /// Gets the user account id if the user logs in, otherwise empty string. + /// + private string GetUserAccountId() + { + try + { + var output = AzContext.ExecuteScript("(Get-AzContext).Account.Id"); + return output.FirstOrDefault() ?? string.Empty; + } + catch (Exception) + { + } + + return string.Empty; + } + + /// + /// Executes the PowerShell cmdlet in the current powershell session. + /// + private static List ExecuteScript(string contents) + { + List output = new List(); + + using (PowerShell powershell = PowerShell.Create(RunspaceMode.NewRunspace)) + { + powershell.AddScript(contents); + Collection result = powershell.Invoke(); + + if (result != null && result.Count > 0) + { + output.AddRange(result); + } + } + + return output; + } + + /// + /// Generate a SHA256 Hash string from the originInput. + /// + /// + /// The Sha256 hash, or empty if the input is only whtespace + private static string GenerateSha256HashString(string originInput) + { + if (string.IsNullOrWhiteSpace(originInput)) + { + return string.Empty; + } + + string result = string.Empty; + try + { + using (var sha256 = new SHA256CryptoServiceProvider()) + { + var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(originInput)); + result = BitConverter.ToString(bytes); + } + } + catch + { + // do not throw if CryptoProvider is not provided + } + + return result; + } + + /// + /// Get the MAC address of the default NIC, or null if none can be found + /// + /// The MAC address of the defautl nic, or null if noen is found + private static string GetMACAddress() + { + return NetworkInterface.GetAllNetworkInterfaces()? + .FirstOrDefault(nic => nic != null && + nic.OperationalStatus == OperationalStatus.Up && + !string.IsNullOrWhiteSpace(nic.GetPhysicalAddress()?.ToString()))? + .GetPhysicalAddress()?.ToString(); + } + } +} diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictor.cs b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictor.cs index 5998d952db8e..6e2615b14f30 100644 --- a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictor.cs +++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictor.cs @@ -57,6 +57,7 @@ internal sealed class AzPredictor : ICommandPredictor private readonly IAzPredictorService _service; private readonly ITelemetryClient _telemetryClient; private readonly Settings _settings; + private readonly IAzContext _azContext; private Queue _lastTwoMaskedCommands = new Queue(AzPredictorConstants.CommandHistoryCountToProcess); @@ -66,16 +67,21 @@ internal sealed class AzPredictor : ICommandPredictor /// The service that provides the suggestion /// The client to collect telemetry /// The settings of the service - public AzPredictor(IAzPredictorService service, ITelemetryClient telemetryClient, Settings settings) + /// The Az context which this module runs with + public AzPredictor(IAzPredictorService service, ITelemetryClient telemetryClient, Settings settings, IAzContext azContext) { this._service = service; this._telemetryClient = telemetryClient; this._settings = settings; + this._azContext = azContext; } /// public void StartEarlyProcessing(IReadOnlyList history) { + // The context only changes when the user executes the corresponding command. + this._azContext?.UpdateContext(); + if (history.Count > 0) { if (_lastTwoMaskedCommands.Any()) @@ -262,9 +268,11 @@ public class PredictorInitializer : IModuleAssemblyInitializer public void OnImport() { var settings = Settings.GetSettings(); - var telemetryClient = new AzPredictorTelemetryClient(); - var azPredictorService = new AzPredictorService(settings.ServiceUri, telemetryClient); - var predictor = new AzPredictor(azPredictorService, telemetryClient, settings); + var azContext = new AzContext(); + azContext.UpdateContext(); + var telemetryClient = new AzPredictorTelemetryClient(azContext); + var azPredictorService = new AzPredictorService(settings.ServiceUri, telemetryClient, azContext); + var predictor = new AzPredictor(azPredictorService, telemetryClient, settings, azContext); SubsystemManager.RegisterSubsystem(predictor); } } diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictorService.cs b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictorService.cs index 26b0a4476dc2..8a022d3280a3 100644 --- a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictorService.cs +++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictorService.cs @@ -19,6 +19,7 @@ using System.Linq; using System.Management.Automation.Language; using System.Net.Http; +using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -38,7 +39,7 @@ public sealed class RequestContext public string CorrelationId { get; set; } = Guid.Empty.ToString(); public string SessionId { get; set; } = Guid.Empty.ToString(); public string SubscriptionId { get; set; } = Guid.Empty.ToString(); - public Version VersionNumber{ get; set; } = new Version(1, 0); + public Version VersionNumber{ get; set; } = new Version(0, 0); } public string History { get; set; } @@ -48,7 +49,8 @@ public sealed class RequestContext public PredictionRequestBody(string command) => this.History = command; }; - private static readonly HttpClient _client = new HttpClient(); + private const string ThrottleByIdHeader = "X-UserId"; + private readonly HttpClient _client; private readonly string _commandsEndpoint; private readonly string _predictionsEndpoint; private volatile Tuple _commandSuggestions; // The command and the prediction for that. @@ -59,6 +61,7 @@ public sealed class RequestContext private ParameterValuePredictor _parameterValuePredictor = new ParameterValuePredictor(); private readonly ITelemetryClient _telemetryClient; + private readonly IAzContext _azContext; /// /// The AzPredictor service interacts with the Aladdin service specified in serviceUri. @@ -66,11 +69,16 @@ public sealed class RequestContext /// /// The URI of the Aladdin service. /// The telemetry client. - public AzPredictorService(string serviceUri, ITelemetryClient telemetryClient) + /// The Az context which this module runs with + public AzPredictorService(string serviceUri, ITelemetryClient telemetryClient, IAzContext azContext) { this._commandsEndpoint = serviceUri + AzPredictorConstants.CommandsEndpoint; this._predictionsEndpoint = serviceUri + AzPredictorConstants.PredictionsEndpoint; this._telemetryClient = telemetryClient; + this._azContext = azContext; + + this._client = new HttpClient(); + this._client.DefaultRequestHeaders?.Add(AzPredictorService.ThrottleByIdHeader, this._azContext.HashedUserId); RequestCommands(); } @@ -163,6 +171,8 @@ public IEnumerable> GetSuggestion(Ast input /// public virtual void RequestPredictions(IEnumerable commands) { + AzPredictorService.ReplaceThrottleUserIdToHeader(this._client?.DefaultRequestHeaders, this._azContext.HashedUserId); + // Even if it's called multiple times, we only need to keep the one for the latest command. this._predictionRequestCancellationSource?.Cancel(); @@ -182,6 +192,7 @@ public virtual void RequestPredictions(IEnumerable commands) { SessionId = this._telemetryClient.SessionId, CorrelationId = this._telemetryClient.CorrelationId, + VersionNumber = this._azContext.AzVersion, }; var requestBody = new PredictionRequestBody(localCommands) { @@ -222,7 +233,7 @@ protected virtual void RequestCommands() // We don't need to block on the task. We send the HTTP request and update commands and predictions list at the background. Task.Run(async () => { - var httpResponseMessage = await AzPredictorService._client.GetAsync(this._commandsEndpoint); + var httpResponseMessage = await this._client.GetAsync(this._commandsEndpoint); var reply = await httpResponseMessage.Content.ReadAsStringAsync(); var commands_reply = JsonConvert.DeserializeObject>(reply); @@ -271,5 +282,22 @@ private static string GetCommandName(string commandLine) { return commandLine.Split(AzPredictorConstants.CommandParameterSeperator).First(); } + + private static void ReplaceThrottleUserIdToHeader(HttpRequestHeaders header, string value) + { + if (header != null) + { + lock (header) + { + header.Remove(AzPredictorService.ThrottleByIdHeader); + + if (!string.IsNullOrWhiteSpace(value)) + { + header.Add(AzPredictorService.ThrottleByIdHeader, value); + } + } + } + + } } } diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictorTelemetryClient.cs b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictorTelemetryClient.cs index 1ddc854f888d..66b90207cb38 100644 --- a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictorTelemetryClient.cs +++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictorTelemetryClient.cs @@ -18,9 +18,7 @@ using Newtonsoft.Json; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; -using System.IO; namespace Microsoft.Azure.PowerShell.Tools.AzPredictor { @@ -38,11 +36,13 @@ sealed class AzPredictorTelemetryClient : ITelemetryClient public string CorrelationId { get; private set; } = Guid.NewGuid().ToString(); private readonly TelemetryClient _telemetryClient; + private readonly IAzContext _azContext; /// /// Constructs a new instance of /// - public AzPredictorTelemetryClient() + /// The Az context which this module runs with + public AzPredictorTelemetryClient(IAzContext azContext) { TelemetryConfiguration configuration = TelemetryConfiguration.CreateDefault(); configuration.InstrumentationKey = "7df6ff70-8353-4672-80d6-568517fed090"; // Use Azuer-PowerShell instrumentation key. see https://github.com/Azure/azure-powershell-common/blob/master/src/Common/AzurePSCmdlet.cs @@ -50,6 +50,7 @@ public AzPredictorTelemetryClient() _telemetryClient.Context.Location.Ip = "0.0.0.0"; _telemetryClient.Context.Cloud.RoleInstance = "placeholderdon'tuse"; _telemetryClient.Context.Cloud.RoleName = "placeholderdon'tuse"; + _azContext = azContext; } /// @@ -65,6 +66,8 @@ public void OnHistory(string historyLine) { "History", historyLine }, { "SessionId", SessionId }, { "CorrelationId", CorrelationId }, + { "UserId", _azContext.HashedUserId }, + { "MacAddress", _azContext.HashedMacAddress }, }; _telemetryClient.TrackEvent($"{AzPredictorTelemetryClient.TelemetryEventPrefix}/CommandHistory", currentLog); @@ -89,6 +92,8 @@ public void OnRequestPrediction(string command) { "Command", command }, { "SessionId", SessionId }, { "CorrelationId", CorrelationId }, + { "UserId", _azContext.HashedUserId }, + { "MacAddress", _azContext.HashedMacAddress }, }; _telemetryClient.TrackEvent($"{AzPredictorTelemetryClient.TelemetryEventPrefix}/RequestPrediction", currentLog); @@ -112,6 +117,8 @@ public void OnRequestPredictionError(string command, Exception e) { "SessionId", SessionId }, { "CorrelationId", CorrelationId }, { "Exception", e.ToString() }, + { "UserId", _azContext.HashedUserId }, + { "MacAddress", _azContext.HashedMacAddress }, }; _telemetryClient.TrackEvent($"{AzPredictorTelemetryClient.TelemetryEventPrefix}/RequestPredictionError", currentLog); @@ -134,6 +141,8 @@ public void OnSuggestionAccepted(string acceptedSuggestion) { "AcceptedSuggestion", acceptedSuggestion }, { "SessionId", SessionId }, { "CorrelationId", CorrelationId }, + { "UserId", _azContext.HashedUserId }, + { "MacAddress", _azContext.HashedMacAddress }, }; _telemetryClient.TrackEvent($"{AzPredictorTelemetryClient.TelemetryEventPrefix}/AcceptSuggestion", properties); @@ -158,6 +167,8 @@ public void OnGetSuggestion(string maskedUserInput, IEnumerable + /// Represents the current Azure PowerShell context. + /// + internal interface IAzContext + { + /// + /// Gets the current Az module version. + /// + public Version AzVersion { get; } + + /// + /// Gets the hashed user account id. A empty string if the user doesn't log in. + /// + public string HashedUserId { get; } + + /// + /// Gets the hashed MAC address. + /// + public string HashedMacAddress { get; } + + /// + /// Updates the Az context. + /// + public void UpdateContext(); + } +} From cce88eba6995ab11ea10d13b4a40dd1d45f16f75 Mon Sep 17 00:00:00 2001 From: Maoliang Huang Date: Thu, 29 Oct 2020 14:45:12 -0700 Subject: [PATCH 2/5] Rename --- .../Az.Tools.Predictor/AzContext.cs | 16 ++++++------- .../AzPredictorTelemetryClient.cs | 24 +++++++++---------- .../Az.Tools.Predictor/IAzContext.cs | 4 ++-- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs index f10e4f78d72b..3060f2866e43 100644 --- a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs +++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs @@ -34,26 +34,26 @@ internal sealed class AzContext : IAzContext public Version AzVersion { get; private set; } = new Version("0.0.0.0"); /// - public string HashedUserId { get; private set; } = string.Empty; + public string HashUserId { get; private set; } = string.Empty; - private string _cachedHashedMacAddress; + private string _cachedHashMacAddress; /// - public string HashedMacAddress + public string HashMacAddress { get { - if (_cachedHashedMacAddress == null) + if (_cachedHashMacAddress == null) { - _cachedHashedMacAddress = string.Empty; + _cachedHashMacAddress = string.Empty; var macAddress = GetMACAddress(); if (!string.IsNullOrWhiteSpace(macAddress)) { - _cachedHashedMacAddress = GenerateSha256HashString(macAddress)?.Replace("-", string.Empty).ToLowerInvariant(); + _cachedHashMacAddress = GenerateSha256HashString(macAddress)?.Replace("-", string.Empty).ToLowerInvariant(); } } - return _cachedHashedMacAddress; + return _cachedHashMacAddress; } } @@ -61,7 +61,7 @@ public string HashedMacAddress public void UpdateContext() { AzVersion = GetAzVersion(); - HashedUserId = GenerateSha256HashString(GetUserAccountId()); + HashUserId = GenerateSha256HashString(GetUserAccountId()); } /// diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictorTelemetryClient.cs b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictorTelemetryClient.cs index 66b90207cb38..c9a773233a32 100644 --- a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictorTelemetryClient.cs +++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictorTelemetryClient.cs @@ -66,8 +66,8 @@ public void OnHistory(string historyLine) { "History", historyLine }, { "SessionId", SessionId }, { "CorrelationId", CorrelationId }, - { "UserId", _azContext.HashedUserId }, - { "MacAddress", _azContext.HashedMacAddress }, + { "UserId", _azContext.HashUserId }, + { "HashMacAddress", _azContext.HashMacAddress }, }; _telemetryClient.TrackEvent($"{AzPredictorTelemetryClient.TelemetryEventPrefix}/CommandHistory", currentLog); @@ -92,8 +92,8 @@ public void OnRequestPrediction(string command) { "Command", command }, { "SessionId", SessionId }, { "CorrelationId", CorrelationId }, - { "UserId", _azContext.HashedUserId }, - { "MacAddress", _azContext.HashedMacAddress }, + { "UserId", _azContext.HashUserId }, + { "HashMacAddress", _azContext.HashMacAddress }, }; _telemetryClient.TrackEvent($"{AzPredictorTelemetryClient.TelemetryEventPrefix}/RequestPrediction", currentLog); @@ -117,8 +117,8 @@ public void OnRequestPredictionError(string command, Exception e) { "SessionId", SessionId }, { "CorrelationId", CorrelationId }, { "Exception", e.ToString() }, - { "UserId", _azContext.HashedUserId }, - { "MacAddress", _azContext.HashedMacAddress }, + { "UserId", _azContext.HashUserId }, + { "HashMacAddress", _azContext.HashMacAddress }, }; _telemetryClient.TrackEvent($"{AzPredictorTelemetryClient.TelemetryEventPrefix}/RequestPredictionError", currentLog); @@ -141,8 +141,8 @@ public void OnSuggestionAccepted(string acceptedSuggestion) { "AcceptedSuggestion", acceptedSuggestion }, { "SessionId", SessionId }, { "CorrelationId", CorrelationId }, - { "UserId", _azContext.HashedUserId }, - { "MacAddress", _azContext.HashedMacAddress }, + { "UserId", _azContext.HashUserId }, + { "HashMacAddress", _azContext.HashMacAddress }, }; _telemetryClient.TrackEvent($"{AzPredictorTelemetryClient.TelemetryEventPrefix}/AcceptSuggestion", properties); @@ -167,8 +167,8 @@ public void OnGetSuggestion(string maskedUserInput, IEnumerable /// Gets the hashed user account id. A empty string if the user doesn't log in. /// - public string HashedUserId { get; } + public string HashUserId { get; } /// /// Gets the hashed MAC address. /// - public string HashedMacAddress { get; } + public string HashMacAddress { get; } /// /// Updates the Az context. From 444d3c5b415508a7ef389bd9c96c927858958d18 Mon Sep 17 00:00:00 2001 From: Maoliang Huang Date: Fri, 30 Oct 2020 13:56:43 -0700 Subject: [PATCH 3/5] Collect more data for diagnostic purpose. --- .../Az.Tools.Predictor/AzContext.cs | 106 ++++++++++++++--- .../Az.Tools.Predictor/AzPredictorService.cs | 4 +- .../AzPredictorTelemetryClient.cs | 110 +++++++++--------- .../Az.Tools.Predictor/IAzContext.cs | 25 +++- 4 files changed, 171 insertions(+), 74 deletions(-) diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs index 3060f2866e43..6e2857489638 100644 --- a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs +++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs @@ -30,30 +30,76 @@ namespace Microsoft.Azure.PowerShell.Tools.AzPredictor /// internal sealed class AzContext : IAzContext { + private static readonly Version DefaultVersion = new Version("0.0.0.0"); + + /// + public Version AzVersion { get; private set; } = AzContext.DefaultVersion; + /// - public Version AzVersion { get; private set; } = new Version("0.0.0.0"); + public IDictionary AzModulesVersions { get; private set; } = new Dictionary(); /// - public string HashUserId { get; private set; } = string.Empty; + public string UserId { get; private set; } = string.Empty; - private string _cachedHashMacAddress; + private string _macAddress; /// - public string HashMacAddress + public string MacAddress { get { - if (_cachedHashMacAddress == null) + if (_macAddress == null) { - _cachedHashMacAddress = string.Empty; + _macAddress = string.Empty; var macAddress = GetMACAddress(); if (!string.IsNullOrWhiteSpace(macAddress)) { - _cachedHashMacAddress = GenerateSha256HashString(macAddress)?.Replace("-", string.Empty).ToLowerInvariant(); + _macAddress = GenerateSha256HashString(macAddress)?.Replace("-", string.Empty).ToLowerInvariant(); } } - return _cachedHashMacAddress; + return _macAddress; + } + } + + /// + public string OSVersion + { + get + { + return Environment.OSVersion.ToString(); + } + } + + private Version _powerShellVersion; + /// + public Version PowerShellVersion + { + get + { + if (_powerShellVersion == null) + { + var outputs = AzContext.ExecuteScript("(Get-Host).Version"); + + _powerShellVersion = outputs.FirstOrDefault(); + } + + return _powerShellVersion ?? AzContext.DefaultVersion; + } + } + + private Version _moduleVersion; + /// + public Version ModuleVersion + { + get + { + if (_moduleVersion == null) + { + _moduleVersion = this.GetType().Assembly.GetName().Version; + } + + return _moduleVersion ?? AzContext.DefaultVersion; } } @@ -61,7 +107,8 @@ public string HashMacAddress public void UpdateContext() { AzVersion = GetAzVersion(); - HashUserId = GenerateSha256HashString(GetUserAccountId()); + AzModulesVersions = GetAzModulesVersions(); + UserId = GenerateSha256HashString(GetUserAccountId()); } /// @@ -69,18 +116,16 @@ public void UpdateContext() /// private Version GetAzVersion() { - Version defaultVersion = new Version("0.0.0"); + Version defaultVersion = AzContext.DefaultVersion; Version latestAz = defaultVersion; try { - var outputs = AzContext.ExecuteScript("Get-Module -Name Az -ListAvailable"); - foreach (PSObject obj in outputs) + var outputs = AzContext.ExecuteScript("Get-Module -Name Az -ListAvailable"); + foreach (var obj in outputs) { - string psVersion = obj.Properties["Version"].Value.ToString(); - int pos = psVersion.IndexOf('-'); - Version currentAz = (pos == -1) ? new Version(psVersion) : new Version(psVersion.Substring(0, pos)); + var currentAz = obj.Version; if (currentAz > latestAz) { latestAz = currentAz; @@ -94,6 +139,37 @@ private Version GetAzVersion() return latestAz; } + /// + /// Gets all the Az modules versions. + /// + private IDictionary GetAzModulesVersions() + { + var moduleVersions = new Dictionary(); + + try + { + var outputs = AzContext.ExecuteScript("Get-Module -Name Az.* -ListAvailable"); + foreach (var obj in outputs) + { + var psVersion = obj.Version; + var psName = obj.Name; + + bool setVersion = !moduleVersions.TryGetValue(psName, out var existingVersion) + || (existingVersion < psVersion); + + if (setVersion) + { + moduleVersions[psName] = psVersion; + } + } + } + catch (Exception) + { + } + + return moduleVersions; + } + /// /// Gets the user account id if the user logs in, otherwise empty string. /// diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictorService.cs b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictorService.cs index 8a022d3280a3..19d471a5e595 100644 --- a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictorService.cs +++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictorService.cs @@ -78,7 +78,7 @@ public AzPredictorService(string serviceUri, ITelemetryClient telemetryClient, I this._azContext = azContext; this._client = new HttpClient(); - this._client.DefaultRequestHeaders?.Add(AzPredictorService.ThrottleByIdHeader, this._azContext.HashedUserId); + this._client.DefaultRequestHeaders?.Add(AzPredictorService.ThrottleByIdHeader, this._azContext.UserId); RequestCommands(); } @@ -171,7 +171,7 @@ public IEnumerable> GetSuggestion(Ast input /// public virtual void RequestPredictions(IEnumerable commands) { - AzPredictorService.ReplaceThrottleUserIdToHeader(this._client?.DefaultRequestHeaders, this._azContext.HashedUserId); + AzPredictorService.ReplaceThrottleUserIdToHeader(this._client?.DefaultRequestHeaders, this._azContext.UserId); // Even if it's called multiple times, we only need to keep the one for the latest command. diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictorTelemetryClient.cs b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictorTelemetryClient.cs index c9a773233a32..df25632da075 100644 --- a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictorTelemetryClient.cs +++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictorTelemetryClient.cs @@ -19,6 +19,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; namespace Microsoft.Azure.PowerShell.Tools.AzPredictor { @@ -37,6 +38,7 @@ sealed class AzPredictorTelemetryClient : ITelemetryClient private readonly TelemetryClient _telemetryClient; private readonly IAzContext _azContext; + private Tuple, string> _cachedAzModulesVersions = Tuple.Create, string>(null, null); /// /// Constructs a new instance of @@ -61,16 +63,10 @@ public void OnHistory(string historyLine) return; } - var currentLog = new Dictionary() - { - { "History", historyLine }, - { "SessionId", SessionId }, - { "CorrelationId", CorrelationId }, - { "UserId", _azContext.HashUserId }, - { "HashMacAddress", _azContext.HashMacAddress }, - }; + var properties = CreateProperties(); + properties.Add("History", historyLine); - _telemetryClient.TrackEvent($"{AzPredictorTelemetryClient.TelemetryEventPrefix}/CommandHistory", currentLog); + _telemetryClient.TrackEvent($"{AzPredictorTelemetryClient.TelemetryEventPrefix}/CommandHistory", properties); #if DEBUG Console.WriteLine("Recording CommandHistory"); @@ -87,16 +83,10 @@ public void OnRequestPrediction(string command) CorrelationId = Guid.NewGuid().ToString(); - var currentLog = new Dictionary() - { - { "Command", command }, - { "SessionId", SessionId }, - { "CorrelationId", CorrelationId }, - { "UserId", _azContext.HashUserId }, - { "HashMacAddress", _azContext.HashMacAddress }, - }; + var properties = CreateProperties(); + properties.Add("Command", command); - _telemetryClient.TrackEvent($"{AzPredictorTelemetryClient.TelemetryEventPrefix}/RequestPrediction", currentLog); + _telemetryClient.TrackEvent($"{AzPredictorTelemetryClient.TelemetryEventPrefix}/RequestPrediction", properties); #if DEBUG Console.WriteLine("Recording RequestPrediction"); @@ -111,17 +101,11 @@ public void OnRequestPredictionError(string command, Exception e) return; } - var currentLog = new Dictionary() - { - { "Command", command }, - { "SessionId", SessionId }, - { "CorrelationId", CorrelationId }, - { "Exception", e.ToString() }, - { "UserId", _azContext.HashUserId }, - { "HashMacAddress", _azContext.HashMacAddress }, - }; + var properties = CreateProperties(); + properties.Add("Command", command); + properties.Add("Exception", e.ToString()); - _telemetryClient.TrackEvent($"{AzPredictorTelemetryClient.TelemetryEventPrefix}/RequestPredictionError", currentLog); + _telemetryClient.TrackEvent($"{AzPredictorTelemetryClient.TelemetryEventPrefix}/RequestPredictionError", properties); #if DEBUG Console.WriteLine("Recording RequestPredictionError"); @@ -136,14 +120,8 @@ public void OnSuggestionAccepted(string acceptedSuggestion) return; } - var properties = new Dictionary() - { - { "AcceptedSuggestion", acceptedSuggestion }, - { "SessionId", SessionId }, - { "CorrelationId", CorrelationId }, - { "UserId", _azContext.HashUserId }, - { "HashMacAddress", _azContext.HashMacAddress }, - }; + var properties = CreateProperties(); + properties.Add("AcceptedSuggestion", acceptedSuggestion); _telemetryClient.TrackEvent($"{AzPredictorTelemetryClient.TelemetryEventPrefix}/AcceptSuggestion", properties); @@ -160,21 +138,15 @@ public void OnGetSuggestion(string maskedUserInput, IEnumerable() - { - { "UserInput", maskedUserInput }, - { "Suggestion", JsonConvert.SerializeObject(suggestions) }, - { "SessionId", SessionId }, - { "CorrelationId", CorrelationId }, - { "IsCancelled", isCancelled.ToString(CultureInfo.InvariantCulture) }, - { "UserId", _azContext.HashUserId }, - { "HashMacAddress", _azContext.HashMacAddress }, - }; + var properties = CreateProperties(); + properties.Add("UserInput", maskedUserInput); + properties.Add("Suggestion", JsonConvert.SerializeObject(suggestions)); + properties.Add("IsCancelled", isCancelled.ToString(CultureInfo.InvariantCulture)); _telemetryClient.TrackEvent($"{AzPredictorTelemetryClient.TelemetryEventPrefix}/GetSuggestion", properties); #if DEBUG - Console.WriteLine("Recording GetSuggestioin"); + Console.WriteLine("Recording GetSuggestion"); #endif } @@ -186,14 +158,8 @@ public void OnGetSuggestionError(Exception e) return; } - var properties = new Dictionary() - { - { "SessionId", SessionId }, - { "CorrelationId", CorrelationId }, - { "Exception", e.ToString() }, - { "UserId", _azContext.HashUserId }, - { "HashMacAddress", _azContext.HashMacAddress }, - }; + var properties = CreateProperties(); + properties.Add("Exception", e.ToString()); _telemetryClient.TrackEvent($"{AzPredictorTelemetryClient.TelemetryEventPrefix}/GetSuggestionError", properties); @@ -215,5 +181,39 @@ private bool IsDataCollectionAllowed() return false; } + + /// + /// Add the common properties to the telemetry event. + /// + private IDictionary CreateProperties() + { + return new Dictionary() + { + { "SessionId", SessionId }, + { "CorrelationId", CorrelationId }, + { "UserId", _azContext.UserId }, + { "HashMacAddress", _azContext.MacAddress }, + { "PowerShellVersion", _azContext.PowerShellVersion.ToString() }, + { "ModuleVersion", _azContext.ModuleVersion.ToString() }, + { "OS", _azContext.OSVersion }, + { "AzModulesVersions", GetAzModulesVersions() }, + }; + } + + private string GetAzModulesVersions() + { + var azModulesVersions = _azContext.AzModulesVersions; + + if (_cachedAzModulesVersions.Item1 != azModulesVersions) + { + _cachedAzModulesVersions = Tuple.Create(azModulesVersions, + string.Join(",", azModulesVersions.Select(pair => + { + return $"{pair.Key}: {pair.Value}"; + }))); + } + + return _cachedAzModulesVersions.Item2; + } } } diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/IAzContext.cs b/tools/Az.Tools.Predictor/Az.Tools.Predictor/IAzContext.cs index 0fbd9268b9b5..49c7af939af5 100644 --- a/tools/Az.Tools.Predictor/Az.Tools.Predictor/IAzContext.cs +++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/IAzContext.cs @@ -13,6 +13,7 @@ // ---------------------------------------------------------------------------------- using System; +using System.Collections.Generic; namespace Microsoft.Azure.PowerShell.Tools.AzPredictor { @@ -26,15 +27,35 @@ internal interface IAzContext /// public Version AzVersion { get; } + /// + /// Gets all the Az.* modules versions. + /// + public IDictionary AzModulesVersions { get; } + /// /// Gets the hashed user account id. A empty string if the user doesn't log in. /// - public string HashUserId { get; } + public string UserId { get; } /// /// Gets the hashed MAC address. /// - public string HashMacAddress { get; } + public string MacAddress { get; } + + /// + /// Gets the OS where it's running on. + /// + public string OSVersion { get; } + + /// + /// Gets the PowerShell version it's running on. + /// + public Version PowerShellVersion { get; } + + /// + /// Gets the version of this module. + /// + public Version ModuleVersion { get; } /// /// Updates the Az context. From 21b1adbe64bfff7cdb6038bcb2b123e125578f57 Mon Sep 17 00:00:00 2001 From: Maoliang Huang Date: Tue, 3 Nov 2020 15:17:18 -0800 Subject: [PATCH 4/5] Fix typo --- tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs index 6e2857489638..0010bda28c3e 100644 --- a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs +++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs @@ -212,7 +212,7 @@ private static List ExecuteScript(string contents) /// Generate a SHA256 Hash string from the originInput. /// /// - /// The Sha256 hash, or empty if the input is only whtespace + /// The Sha256 hash, or empty if the input is only whitespace private static string GenerateSha256HashString(string originInput) { if (string.IsNullOrWhiteSpace(originInput)) @@ -240,7 +240,7 @@ private static string GenerateSha256HashString(string originInput) /// /// Get the MAC address of the default NIC, or null if none can be found /// - /// The MAC address of the defautl nic, or null if noen is found + /// The MAC address of the defautl nic, or null if none is found private static string GetMACAddress() { return NetworkInterface.GetAllNetworkInterfaces()? From 3362282cc19130c106608c2f5c0dc09d952687d9 Mon Sep 17 00:00:00 2001 From: Maoliang Huang Date: Wed, 4 Nov 2020 10:55:09 -0800 Subject: [PATCH 5/5] Remove getting Az modules versions. --- .../Az.Tools.Predictor/AzContext.cs | 67 ------------------- .../AzPredictorTelemetryClient.cs | 17 ----- .../Az.Tools.Predictor/IAzContext.cs | 11 --- 3 files changed, 95 deletions(-) diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs index 0010bda28c3e..b2e929c5be71 100644 --- a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs +++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs @@ -32,12 +32,6 @@ internal sealed class AzContext : IAzContext { private static readonly Version DefaultVersion = new Version("0.0.0.0"); - /// - public Version AzVersion { get; private set; } = AzContext.DefaultVersion; - - /// - public IDictionary AzModulesVersions { get; private set; } = new Dictionary(); - /// public string UserId { get; private set; } = string.Empty; @@ -106,70 +100,9 @@ public Version ModuleVersion /// public void UpdateContext() { - AzVersion = GetAzVersion(); - AzModulesVersions = GetAzModulesVersions(); UserId = GenerateSha256HashString(GetUserAccountId()); } - /// - /// Gets the latest version from the loaded Az modules. - /// - private Version GetAzVersion() - { - Version defaultVersion = AzContext.DefaultVersion; - - Version latestAz = defaultVersion; - - try - { - var outputs = AzContext.ExecuteScript("Get-Module -Name Az -ListAvailable"); - foreach (var obj in outputs) - { - var currentAz = obj.Version; - if (currentAz > latestAz) - { - latestAz = currentAz; - } - } - } - catch (Exception) - { - } - - return latestAz; - } - - /// - /// Gets all the Az modules versions. - /// - private IDictionary GetAzModulesVersions() - { - var moduleVersions = new Dictionary(); - - try - { - var outputs = AzContext.ExecuteScript("Get-Module -Name Az.* -ListAvailable"); - foreach (var obj in outputs) - { - var psVersion = obj.Version; - var psName = obj.Name; - - bool setVersion = !moduleVersions.TryGetValue(psName, out var existingVersion) - || (existingVersion < psVersion); - - if (setVersion) - { - moduleVersions[psName] = psVersion; - } - } - } - catch (Exception) - { - } - - return moduleVersions; - } - /// /// Gets the user account id if the user logs in, otherwise empty string. /// diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictorTelemetryClient.cs b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictorTelemetryClient.cs index c5b3f5fe5b57..d8c627a43202 100644 --- a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictorTelemetryClient.cs +++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictorTelemetryClient.cs @@ -196,24 +196,7 @@ private IDictionary CreateProperties() { "PowerShellVersion", _azContext.PowerShellVersion.ToString() }, { "ModuleVersion", _azContext.ModuleVersion.ToString() }, { "OS", _azContext.OSVersion }, - { "AzModulesVersions", GetAzModulesVersions() }, }; } - - private string GetAzModulesVersions() - { - var azModulesVersions = _azContext.AzModulesVersions; - - if (_cachedAzModulesVersions.Item1 != azModulesVersions) - { - _cachedAzModulesVersions = Tuple.Create(azModulesVersions, - string.Join(",", azModulesVersions.Select(pair => - { - return $"{pair.Key}: {pair.Value}"; - }))); - } - - return _cachedAzModulesVersions.Item2; - } } } diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/IAzContext.cs b/tools/Az.Tools.Predictor/Az.Tools.Predictor/IAzContext.cs index 49c7af939af5..7c19d92de0f8 100644 --- a/tools/Az.Tools.Predictor/Az.Tools.Predictor/IAzContext.cs +++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/IAzContext.cs @@ -13,7 +13,6 @@ // ---------------------------------------------------------------------------------- using System; -using System.Collections.Generic; namespace Microsoft.Azure.PowerShell.Tools.AzPredictor { @@ -22,16 +21,6 @@ namespace Microsoft.Azure.PowerShell.Tools.AzPredictor /// internal interface IAzContext { - /// - /// Gets the current Az module version. - /// - public Version AzVersion { get; } - - /// - /// Gets all the Az.* modules versions. - /// - public IDictionary AzModulesVersions { get; } - /// /// Gets the hashed user account id. A empty string if the user doesn't log in. ///