diff --git a/src/Common/Commands.Common/AzurePSCmdlet.cs b/src/Common/Commands.Common/AzurePSCmdlet.cs index ed00e2e32a4c..43555c5eb5f1 100644 --- a/src/Common/Commands.Common/AzurePSCmdlet.cs +++ b/src/Common/Commands.Common/AzurePSCmdlet.cs @@ -36,6 +36,17 @@ public abstract class AzurePSCmdlet : PSCmdlet protected static AzureProfile _currentProfile = null; protected static AzurePSDataCollectionProfile _dataCollectionProfile = null; + protected AzurePSQoSEvent QosEvent; + + protected virtual bool IsUsageMetricEnabled { + get { return false; } + } + + protected virtual bool IsErrorMetricEnabled + { + get { return true; } + } + [Parameter(Mandatory = false, HelpMessage = "In-memory profile.")] public AzureProfile Profile { get; set; } @@ -208,6 +219,22 @@ protected static AzurePSDataCollectionProfile GetDataCollectionProfile() return _dataCollectionProfile; } + /// + /// Check whether the data collection is opted in from user + /// + /// true if allowed + public static bool IsDataCollectionAllowed() + { + if (_dataCollectionProfile != null && + _dataCollectionProfile.EnableAzureDataCollection.HasValue && + _dataCollectionProfile.EnableAzureDataCollection.Value) + { + return true; + } + + return false; + } + /// /// Save the current data collection profile Json data into the default file path /// @@ -305,7 +332,7 @@ protected override void BeginProcessing() { InitializeProfile(); PromptForDataCollectionProfileIfNotExists(); - + InitializeQosEvent(); if (string.IsNullOrEmpty(ParameterSetName)) { WriteDebugWithTimestamp(string.Format(Resources.BeginProcessingWithoutParameterSetLog, this.GetType().Name)); @@ -346,6 +373,7 @@ protected virtual void InitializeProfile() /// protected override void EndProcessing() { + LogQosEvent(); string message = string.Format(Resources.EndProcessingLog, this.GetType().Name); WriteDebugWithTimestamp(message); @@ -379,6 +407,9 @@ protected bool IsVerbose() public new void WriteError(ErrorRecord errorRecord) { FlushDebugMessages(); + QosEvent.Exception = errorRecord.Exception; + QosEvent.IsSuccess = false; + LogQosEvent(true); base.WriteError(errorRecord); } @@ -506,6 +537,62 @@ private void FlushDebugMessages() } } + protected void InitializeQosEvent() + { + QosEvent = new AzurePSQoSEvent() + { + CmdletType = this.GetType().Name, + IsSuccess = true, + }; + + if (this.Profile != null && this.Profile.DefaultSubscription != null) + { + QosEvent.Uid = MetricHelper.GenerateSha256HashString( + this.Profile.DefaultSubscription.Id.ToString()); + } + else + { + QosEvent.Uid = "defaultid"; + } + } + + /// + /// Invoke this method when the cmdlet is completed or terminated. + /// + protected void LogQosEvent(bool waitForMetricSending = false) + { + if (QosEvent == null) + { + return; + } + + QosEvent.FinishQosEvent(); + + if (!IsUsageMetricEnabled && (!IsErrorMetricEnabled || QosEvent.IsSuccess)) + { + return; + } + + if (!IsDataCollectionAllowed()) + { + return; + } + + WriteDebug(QosEvent.ToString()); + + try + { + MetricHelper.LogQoSEvent(QosEvent, IsUsageMetricEnabled, IsErrorMetricEnabled); + MetricHelper.FlushMetric(waitForMetricSending); + WriteDebug("Finish sending metric."); + } + catch (Exception e) + { + //Swallow error from Application Insights event collection. + WriteWarning(e.ToString()); + } + } + /// /// Asks for confirmation before executing the action. /// @@ -516,10 +603,19 @@ private void FlushDebugMessages() /// The action code protected void ConfirmAction(bool force, string actionMessage, string processMessage, string target, Action action) { + if (QosEvent != null) + { + QosEvent.PauseQoSTimer(); + } + if (force || ShouldContinue(actionMessage, "")) { if (ShouldProcess(target, processMessage)) - { + { + if (QosEvent != null) + { + QosEvent.ResumeQosTimer(); + } action(); } } diff --git a/src/Common/Commands.Common/Commands.Common.csproj b/src/Common/Commands.Common/Commands.Common.csproj index b908e8a2341a..945d7c23cfec 100644 --- a/src/Common/Commands.Common/Commands.Common.csproj +++ b/src/Common/Commands.Common/Commands.Common.csproj @@ -15,6 +15,7 @@ ..\..\ true /assemblyCompareMode:StrongNameIgnoringVersion + 06e19c11 true @@ -54,6 +55,10 @@ False ..\..\packages\Hyak.Common.1.0.2\lib\portable-net403+win+wpa81\Hyak.Common.dll + + ..\..\packages\Microsoft.ApplicationInsights.1.1.1-beta\lib\net45\Microsoft.ApplicationInsights.dll + True + False ..\..\packages\Microsoft.Azure.Common.2.1.0\lib\net45\Microsoft.Azure.Common.dll @@ -147,6 +152,7 @@ True Resources.resx + diff --git a/src/Common/Commands.Common/MetricHelper.cs b/src/Common/Commands.Common/MetricHelper.cs new file mode 100644 index 000000000000..9bb3d20e4296 --- /dev/null +++ b/src/Common/Commands.Common/MetricHelper.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.Channel; +using Microsoft.ApplicationInsights.DataContracts; +using Microsoft.ApplicationInsights.Extensibility; +using Microsoft.WindowsAzure.Commands.Utilities.Common; + +namespace Microsoft.WindowsAzure.Commands.Common +{ + public static class MetricHelper + { + private const int FlushTimeoutInMilli = 5000; + private static readonly TelemetryClient TelemetryClient; + + static MetricHelper() + { + TelemetryClient = new TelemetryClient(); + // TODO: InstrumentationKey shall be injected in build server + TelemetryClient.InstrumentationKey = "7df6ff70-8353-4672-80d6-568517fed090"; + // Disable IP collection + TelemetryClient.Context.Location.Ip = "0.0.0.0"; + + if (TestMockSupport.RunningMocked) + { + TelemetryConfiguration.Active.DisableTelemetry = true; + } + } + + public static void LogQoSEvent(AzurePSQoSEvent qos, bool isUsageMetricEnabled, bool isErrorMetricEnabled) + { + if (!IsMetricTermAccepted()) + { + return; + } + + if (isUsageMetricEnabled) + { + LogUsageEvent(qos); + } + + if (isErrorMetricEnabled && qos.Exception != null) + { + LogExceptionEvent(qos); + } + } + + private static void LogUsageEvent(AzurePSQoSEvent qos) + { + var tcEvent = new RequestTelemetry(qos.CmdletType, qos.StartTime, qos.Duration, string.Empty, qos.IsSuccess); + tcEvent.Context.User.Id = qos.Uid; + tcEvent.Context.User.UserAgent = AzurePowerShell.UserAgentValue.ToString(); + tcEvent.Context.Device.OperatingSystem = Environment.OSVersion.VersionString; + + TelemetryClient.TrackRequest(tcEvent); + } + + private static void LogExceptionEvent(AzurePSQoSEvent qos) + { + //Log as custome event to exclude actual exception message + var tcEvent = new EventTelemetry("CmdletError"); + tcEvent.Properties.Add("ExceptionType", qos.Exception.GetType().FullName); + tcEvent.Properties.Add("StackTrace", qos.Exception.StackTrace); + if (qos.Exception.InnerException != null) + { + tcEvent.Properties.Add("InnerExceptionType", qos.Exception.InnerException.GetType().FullName); + tcEvent.Properties.Add("InnerStackTrace", qos.Exception.InnerException.StackTrace); + } + + tcEvent.Context.User.Id = qos.Uid; + tcEvent.Properties.Add("CmdletType", qos.CmdletType); + + TelemetryClient.TrackEvent(tcEvent); + } + + public static bool IsMetricTermAccepted() + { + return AzurePSCmdlet.IsDataCollectionAllowed(); + } + + public static void FlushMetric(bool waitForMetricSending) + { + if (!IsMetricTermAccepted()) + { + return; + } + + var flushTask = Task.Run(() => FlushAi()); + if (waitForMetricSending) + { + Task.WaitAll(new[] { flushTask }, FlushTimeoutInMilli); + } + } + + private static void FlushAi() + { + try + { + TelemetryClient.Flush(); + } + catch + { + // ignored + } + } + + /// + /// Gereate a SHA256 Hash string from the originInput. + /// + /// + /// + public static string GenerateSha256HashString(string originInput) + { + SHA256 sha256 = SHA256.Create(); + var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(originInput)); + return Encoding.UTF8.GetString(bytes); + } + } +} + +public class AzurePSQoSEvent +{ + private readonly Stopwatch _timer; + + public DateTimeOffset StartTime { get; set; } + public TimeSpan Duration { get; set; } + public bool IsSuccess { get; set; } + public string CmdletType { get; set; } + public Exception Exception { get; set; } + public string Uid { get; set; } + + public AzurePSQoSEvent() + { + StartTime = DateTimeOffset.Now; + _timer = new Stopwatch(); + _timer.Start(); + } + + public void PauseQoSTimer() + { + _timer.Stop(); + } + + public void ResumeQosTimer() + { + _timer.Start(); + } + + public void FinishQosEvent() + { + _timer.Stop(); + Duration = _timer.Elapsed; + } + + public override string ToString() + { + return string.Format( + "AzureQoSEvent: CmdletType - {0}; IsSuccess - {1}; Duration - {2}; Exception - {3};", + CmdletType, IsSuccess, Duration, Exception); + } +} diff --git a/src/Common/Commands.Common/packages.config b/src/Common/Commands.Common/packages.config index abdfbbed145e..90ec260fe139 100644 --- a/src/Common/Commands.Common/packages.config +++ b/src/Common/Commands.Common/packages.config @@ -1,6 +1,7 @@  + diff --git a/src/ResourceManager/Compute/Commands.Compute/Common/ComputeClientBaseCmdlet.cs b/src/ResourceManager/Compute/Commands.Compute/Common/ComputeClientBaseCmdlet.cs index d26840fadf17..340bc49506d2 100644 --- a/src/ResourceManager/Compute/Commands.Compute/Common/ComputeClientBaseCmdlet.cs +++ b/src/ResourceManager/Compute/Commands.Compute/Common/ComputeClientBaseCmdlet.cs @@ -23,6 +23,11 @@ public abstract class ComputeClientBaseCmdlet : AzurePSCmdlet { protected const string VirtualMachineExtensionType = "Microsoft.Compute/virtualMachines/extensions"; + protected override bool IsUsageMetricEnabled + { + get { return true; } + } + private ComputeClient computeClient; public ComputeClient ComputeClient @@ -54,18 +59,11 @@ protected void ExecuteClientAction(Action action) { try { - try - { - action(); - } - catch (CloudException ex) - { - throw new ComputeCloudException(ex); - } + action(); } - catch (Exception ex) + catch (CloudException ex) { - WriteExceptionError(ex); + throw new ComputeCloudException(ex); } } } diff --git a/src/ResourceManager/Compute/Commands.Compute/VirtualMachine/Config/NewAzureVMConfigCommand.cs b/src/ResourceManager/Compute/Commands.Compute/VirtualMachine/Config/NewAzureVMConfigCommand.cs index 2f88fa1fd386..25293d78aa91 100644 --- a/src/ResourceManager/Compute/Commands.Compute/VirtualMachine/Config/NewAzureVMConfigCommand.cs +++ b/src/ResourceManager/Compute/Commands.Compute/VirtualMachine/Config/NewAzureVMConfigCommand.cs @@ -51,6 +51,11 @@ public class NewAzureVMConfigCommand : AzurePSCmdlet [ValidateNotNullOrEmpty] public string AvailabilitySetId { get; set; } + protected override bool IsUsageMetricEnabled + { + get { return true; } + } + public override void ExecuteCmdlet() { var vm = new PSVirtualMachine