Skip to content

Integrate Application Insights into Azure PowerShell #869

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Sep 9, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 98 additions & 2 deletions src/Common/Commands.Common/AzurePSCmdlet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down Expand Up @@ -208,6 +219,22 @@ protected static AzurePSDataCollectionProfile GetDataCollectionProfile()
return _dataCollectionProfile;
}

/// <summary>
/// Check whether the data collection is opted in from user
/// </summary>
/// <returns>true if allowed</returns>
public static bool IsDataCollectionAllowed()
{
if (_dataCollectionProfile != null &&
_dataCollectionProfile.EnableAzureDataCollection.HasValue &&
_dataCollectionProfile.EnableAzureDataCollection.Value)
{
return true;
}

return false;
}

/// <summary>
/// Save the current data collection profile Json data into the default file path
/// </summary>
Expand Down Expand Up @@ -305,7 +332,7 @@ protected override void BeginProcessing()
{
InitializeProfile();
PromptForDataCollectionProfileIfNotExists();

InitializeQosEvent();
if (string.IsNullOrEmpty(ParameterSetName))
{
WriteDebugWithTimestamp(string.Format(Resources.BeginProcessingWithoutParameterSetLog, this.GetType().Name));
Expand Down Expand Up @@ -346,6 +373,7 @@ protected virtual void InitializeProfile()
/// </summary>
protected override void EndProcessing()
{
LogQosEvent();
string message = string.Format(Resources.EndProcessingLog, this.GetType().Name);
WriteDebugWithTimestamp(message);

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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";
}
}

/// <summary>
/// Invoke this method when the cmdlet is completed or terminated.
/// </summary>
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());
}
}

/// <summary>
/// Asks for confirmation before executing the action.
/// </summary>
Expand All @@ -516,10 +603,19 @@ private void FlushDebugMessages()
/// <param name="action">The action code</param>
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();
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/Common/Commands.Common/Commands.Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\..\</SolutionDir>
<RestorePackages>true</RestorePackages>
<CodeAnalysisAdditionalOptions>/assemblyCompareMode:StrongNameIgnoringVersion</CodeAnalysisAdditionalOptions>
<NuGetPackageImportStamp>06e19c11</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
Expand Down Expand Up @@ -54,6 +55,10 @@
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\packages\Hyak.Common.1.0.2\lib\portable-net403+win+wpa81\Hyak.Common.dll</HintPath>
</Reference>
<Reference Include="Microsoft.ApplicationInsights, Version=1.1.0.1899, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.ApplicationInsights.1.1.1-beta\lib\net45\Microsoft.ApplicationInsights.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.Azure.Common">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\packages\Microsoft.Azure.Common.2.1.0\lib\net45\Microsoft.Azure.Common.dll</HintPath>
Expand Down Expand Up @@ -147,6 +152,7 @@
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="MetricHelper.cs" />
<Compile Include="SecureStringExtensions.cs" />
<Compile Include="ConversionUtilities.cs" />
<Compile Include="DebugStreamTraceListener.cs" />
Expand Down
165 changes: 165 additions & 0 deletions src/Common/Commands.Common/MetricHelper.cs
Original file line number Diff line number Diff line change
@@ -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
}
}

/// <summary>
/// Gereate a SHA256 Hash string from the originInput.
/// </summary>
/// <param name="originInput"></param>
/// <returns></returns>
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);
}
}
1 change: 1 addition & 0 deletions src/Common/Commands.Common/packages.config
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Hyak.Common" version="1.0.2" targetFramework="net45" />
<package id="Microsoft.ApplicationInsights" version="1.1.1-beta" targetFramework="net45" />
<package id="Microsoft.AspNet.WebApi.Client" version="5.2.2" targetFramework="net45" />
<package id="Microsoft.Azure.Common" version="2.1.0" targetFramework="net45" />
<package id="Microsoft.Azure.Common.Authentication" version="1.1.3-preview" targetFramework="net45" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down