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 e1d7efbeba2d..3e4907dd98d3 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); } /// @@ -157,7 +158,8 @@ public void VerifySuggestionOnIncompleteCommand() var localAzPredictor = new AzPredictor(this._service, this._telemetryClient, new Settings() { SuggestionCount = 7, - }); + }, + null); var userInput = "New-AzResourceGroup -Name 'ResourceGroup01' -Location 'Central US' -WhatIf -"; var expected = "New-AzResourceGroup -Name 'ResourceGroup01' -Location 'Central US' -WhatIf -Verbose ***"; 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..b2e929c5be71 --- /dev/null +++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs @@ -0,0 +1,186 @@ +// ---------------------------------------------------------------------------------- +// +// 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 + { + private static readonly Version DefaultVersion = new Version("0.0.0.0"); + + /// + public string UserId { get; private set; } = string.Empty; + + private string _macAddress; + /// + public string MacAddress + { + get + { + if (_macAddress == null) + { + _macAddress = string.Empty; + + var macAddress = GetMACAddress(); + if (!string.IsNullOrWhiteSpace(macAddress)) + { + _macAddress = GenerateSha256HashString(macAddress)?.Replace("-", string.Empty).ToLowerInvariant(); + } + } + + 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; + } + } + + /// + public void UpdateContext() + { + UserId = GenerateSha256HashString(GetUserAccountId()); + } + + /// + /// 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 whitespace + 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 none 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 8269d4c80747..51ccf0d93445 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); @@ -69,16 +70,20 @@ 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(); lock (_userAcceptedAndSuggestion) { _userAcceptedAndSuggestion.Clear(); @@ -330,9 +335,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 2ea5e265542d..ea1508e15092 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; @@ -56,7 +57,8 @@ private sealed class CommandRequestContext public Version VersionNumber{ get; set; } = new Version(0, 0); } - 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. @@ -67,6 +69,7 @@ private sealed class CommandRequestContext private readonly ParameterValuePredictor _parameterValuePredictor = new ParameterValuePredictor(); private readonly ITelemetryClient _telemetryClient; + private readonly IAzContext _azContext; /// /// The AzPredictor service interacts with the Aladdin service specified in serviceUri. @@ -74,11 +77,16 @@ private sealed class CommandRequestContext /// /// 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}?clientType={AzPredictorService.ClientType}&context={JsonConvert.SerializeObject(new CommandRequestContext())}"; this._predictionsEndpoint = serviceUri + AzPredictorConstants.PredictionsEndpoint; this._telemetryClient = telemetryClient; + this._azContext = azContext; + + this._client = new HttpClient(); + this._client.DefaultRequestHeaders?.Add(AzPredictorService.ThrottleByIdHeader, this._azContext.UserId); RequestCommands(); } @@ -173,6 +181,7 @@ public IEnumerable>, PredictionSo /// public virtual void RequestPredictions(IEnumerable commands) { + AzPredictorService.ReplaceThrottleUserIdToHeader(this._client?.DefaultRequestHeaders, this._azContext.UserId); var localCommands= string.Join(AzPredictorConstants.CommandConcatenator, commands); this._telemetryClient.OnRequestPrediction(localCommands); @@ -241,7 +250,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); @@ -290,5 +299,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 f1f8a837df4f..d8c627a43202 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 { @@ -36,11 +37,14 @@ sealed class AzPredictorTelemetryClient : ITelemetryClient public string CorrelationId { get; private set; } = Guid.NewGuid().ToString(); private readonly TelemetryClient _telemetryClient; + private readonly IAzContext _azContext; + private Tuple, string> _cachedAzModulesVersions = Tuple.Create, string>(null, null); /// /// 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 @@ -48,6 +52,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; } /// @@ -58,14 +63,10 @@ public void OnHistory(string historyLine) return; } - var currentLog = new Dictionary() - { - { "History", historyLine }, - { "SessionId", SessionId }, - { "CorrelationId", CorrelationId }, - }; + var properties = CreateProperties(); + properties.Add("History", historyLine); - _telemetryClient.TrackEvent($"{AzPredictorTelemetryClient.TelemetryEventPrefix}/CommandHistory", currentLog); + _telemetryClient.TrackEvent($"{AzPredictorTelemetryClient.TelemetryEventPrefix}/CommandHistory", properties); #if TELEMETRY_TRACE && DEBUG Console.WriteLine("Recording CommandHistory"); @@ -82,14 +83,10 @@ public void OnRequestPrediction(string command) CorrelationId = Guid.NewGuid().ToString(); - var currentLog = new Dictionary() - { - { "Command", command }, - { "SessionId", SessionId }, - { "CorrelationId", CorrelationId }, - }; + var properties = CreateProperties(); + properties.Add("Command", command); - _telemetryClient.TrackEvent($"{AzPredictorTelemetryClient.TelemetryEventPrefix}/RequestPrediction", currentLog); + _telemetryClient.TrackEvent($"{AzPredictorTelemetryClient.TelemetryEventPrefix}/RequestPrediction", properties); #if TELEMETRY_TRACE && DEBUG Console.WriteLine("Recording RequestPrediction"); @@ -104,15 +101,11 @@ public void OnRequestPredictionError(string command, Exception e) return; } - var currentLog = new Dictionary() - { - { "Command", command }, - { "SessionId", SessionId }, - { "CorrelationId", CorrelationId }, - { "Exception", e.ToString() }, - }; + 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 TELEMETRY_TRACE && DEBUG Console.WriteLine("Recording RequestPredictionError"); @@ -127,12 +120,8 @@ public void OnSuggestionAccepted(string acceptedSuggestion) return; } - var properties = new Dictionary() - { - { "AcceptedSuggestion", acceptedSuggestion }, - { "SessionId", SessionId }, - { "CorrelationId", CorrelationId }, - }; + var properties = CreateProperties(); + properties.Add("AcceptedSuggestion", acceptedSuggestion); _telemetryClient.TrackEvent($"{AzPredictorTelemetryClient.TelemetryEventPrefix}/AcceptSuggestion", properties); @@ -149,19 +138,15 @@ public void OnGetSuggestion(string maskedUserInput, IEnumerable() - { - { "UserInput", maskedUserInput }, - { "Suggestion", JsonConvert.SerializeObject(suggestions) }, - { "SessionId", SessionId }, - { "CorrelationId", CorrelationId }, - { "IsCancelled", isCancelled.ToString(CultureInfo.InvariantCulture) }, - }; + 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 TELEMETRY_TRACE && DEBUG - Console.WriteLine("Recording GetSuggestioin"); + Console.WriteLine("Recording GetSuggestion"); #endif } @@ -173,12 +158,8 @@ public void OnGetSuggestionError(Exception e) return; } - var properties = new Dictionary() - { - { "SessionId", SessionId }, - { "CorrelationId", CorrelationId }, - { "Exception", e.ToString() }, - }; + var properties = CreateProperties(); + properties.Add("Exception", e.ToString()); _telemetryClient.TrackEvent($"{AzPredictorTelemetryClient.TelemetryEventPrefix}/GetSuggestionError", properties); @@ -200,5 +181,22 @@ 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 }, + }; + } } } diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/IAzContext.cs b/tools/Az.Tools.Predictor/Az.Tools.Predictor/IAzContext.cs new file mode 100644 index 000000000000..7c19d92de0f8 --- /dev/null +++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/IAzContext.cs @@ -0,0 +1,54 @@ +// ---------------------------------------------------------------------------------- +// +// 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; + +namespace Microsoft.Azure.PowerShell.Tools.AzPredictor +{ + /// + /// Represents the current Azure PowerShell context. + /// + internal interface IAzContext + { + /// + /// Gets the hashed user account id. A empty string if the user doesn't log in. + /// + public string UserId { get; } + + /// + /// Gets the hashed MAC address. + /// + 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. + /// + public void UpdateContext(); + } +}