Skip to content

Commit 7a61084

Browse files
author
Jianghao Lu
committed
Merge pull request #869 from xindzhan/ai
Integrate Application Insights into Azure PowerShell
2 parents 2ba6cad + 42360a1 commit 7a61084

File tree

6 files changed

+283
-12
lines changed

6 files changed

+283
-12
lines changed

src/Common/Commands.Common/AzurePSCmdlet.cs

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ public abstract class AzurePSCmdlet : PSCmdlet
3636
protected static AzureProfile _currentProfile = null;
3737
protected static AzurePSDataCollectionProfile _dataCollectionProfile = null;
3838

39+
protected AzurePSQoSEvent QosEvent;
40+
41+
protected virtual bool IsUsageMetricEnabled {
42+
get { return false; }
43+
}
44+
45+
protected virtual bool IsErrorMetricEnabled
46+
{
47+
get { return true; }
48+
}
49+
3950
[Parameter(Mandatory = false, HelpMessage = "In-memory profile.")]
4051
public AzureProfile Profile { get; set; }
4152

@@ -208,6 +219,22 @@ protected static AzurePSDataCollectionProfile GetDataCollectionProfile()
208219
return _dataCollectionProfile;
209220
}
210221

222+
/// <summary>
223+
/// Check whether the data collection is opted in from user
224+
/// </summary>
225+
/// <returns>true if allowed</returns>
226+
public static bool IsDataCollectionAllowed()
227+
{
228+
if (_dataCollectionProfile != null &&
229+
_dataCollectionProfile.EnableAzureDataCollection.HasValue &&
230+
_dataCollectionProfile.EnableAzureDataCollection.Value)
231+
{
232+
return true;
233+
}
234+
235+
return false;
236+
}
237+
211238
/// <summary>
212239
/// Save the current data collection profile Json data into the default file path
213240
/// </summary>
@@ -305,7 +332,7 @@ protected override void BeginProcessing()
305332
{
306333
InitializeProfile();
307334
PromptForDataCollectionProfileIfNotExists();
308-
335+
InitializeQosEvent();
309336
if (string.IsNullOrEmpty(ParameterSetName))
310337
{
311338
WriteDebugWithTimestamp(string.Format(Resources.BeginProcessingWithoutParameterSetLog, this.GetType().Name));
@@ -346,6 +373,7 @@ protected virtual void InitializeProfile()
346373
/// </summary>
347374
protected override void EndProcessing()
348375
{
376+
LogQosEvent();
349377
string message = string.Format(Resources.EndProcessingLog, this.GetType().Name);
350378
WriteDebugWithTimestamp(message);
351379

@@ -379,6 +407,9 @@ protected bool IsVerbose()
379407
public new void WriteError(ErrorRecord errorRecord)
380408
{
381409
FlushDebugMessages();
410+
QosEvent.Exception = errorRecord.Exception;
411+
QosEvent.IsSuccess = false;
412+
LogQosEvent(true);
382413
base.WriteError(errorRecord);
383414
}
384415

@@ -506,6 +537,62 @@ private void FlushDebugMessages()
506537
}
507538
}
508539

540+
protected void InitializeQosEvent()
541+
{
542+
QosEvent = new AzurePSQoSEvent()
543+
{
544+
CmdletType = this.GetType().Name,
545+
IsSuccess = true,
546+
};
547+
548+
if (this.Profile != null && this.Profile.DefaultSubscription != null)
549+
{
550+
QosEvent.Uid = MetricHelper.GenerateSha256HashString(
551+
this.Profile.DefaultSubscription.Id.ToString());
552+
}
553+
else
554+
{
555+
QosEvent.Uid = "defaultid";
556+
}
557+
}
558+
559+
/// <summary>
560+
/// Invoke this method when the cmdlet is completed or terminated.
561+
/// </summary>
562+
protected void LogQosEvent(bool waitForMetricSending = false)
563+
{
564+
if (QosEvent == null)
565+
{
566+
return;
567+
}
568+
569+
QosEvent.FinishQosEvent();
570+
571+
if (!IsUsageMetricEnabled && (!IsErrorMetricEnabled || QosEvent.IsSuccess))
572+
{
573+
return;
574+
}
575+
576+
if (!IsDataCollectionAllowed())
577+
{
578+
return;
579+
}
580+
581+
WriteDebug(QosEvent.ToString());
582+
583+
try
584+
{
585+
MetricHelper.LogQoSEvent(QosEvent, IsUsageMetricEnabled, IsErrorMetricEnabled);
586+
MetricHelper.FlushMetric(waitForMetricSending);
587+
WriteDebug("Finish sending metric.");
588+
}
589+
catch (Exception e)
590+
{
591+
//Swallow error from Application Insights event collection.
592+
WriteWarning(e.ToString());
593+
}
594+
}
595+
509596
/// <summary>
510597
/// Asks for confirmation before executing the action.
511598
/// </summary>
@@ -516,10 +603,19 @@ private void FlushDebugMessages()
516603
/// <param name="action">The action code</param>
517604
protected void ConfirmAction(bool force, string actionMessage, string processMessage, string target, Action action)
518605
{
606+
if (QosEvent != null)
607+
{
608+
QosEvent.PauseQoSTimer();
609+
}
610+
519611
if (force || ShouldContinue(actionMessage, ""))
520612
{
521613
if (ShouldProcess(target, processMessage))
522-
{
614+
{
615+
if (QosEvent != null)
616+
{
617+
QosEvent.ResumeQosTimer();
618+
}
523619
action();
524620
}
525621
}

src/Common/Commands.Common/Commands.Common.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\..\</SolutionDir>
1616
<RestorePackages>true</RestorePackages>
1717
<CodeAnalysisAdditionalOptions>/assemblyCompareMode:StrongNameIgnoringVersion</CodeAnalysisAdditionalOptions>
18+
<NuGetPackageImportStamp>06e19c11</NuGetPackageImportStamp>
1819
</PropertyGroup>
1920
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
2021
<DebugSymbols>true</DebugSymbols>
@@ -54,6 +55,10 @@
5455
<SpecificVersion>False</SpecificVersion>
5556
<HintPath>..\..\packages\Hyak.Common.1.0.2\lib\portable-net403+win+wpa81\Hyak.Common.dll</HintPath>
5657
</Reference>
58+
<Reference Include="Microsoft.ApplicationInsights, Version=1.1.0.1899, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
59+
<HintPath>..\..\packages\Microsoft.ApplicationInsights.1.1.1-beta\lib\net45\Microsoft.ApplicationInsights.dll</HintPath>
60+
<Private>True</Private>
61+
</Reference>
5762
<Reference Include="Microsoft.Azure.Common">
5863
<SpecificVersion>False</SpecificVersion>
5964
<HintPath>..\..\packages\Microsoft.Azure.Common.2.1.0\lib\net45\Microsoft.Azure.Common.dll</HintPath>
@@ -147,6 +152,7 @@
147152
<DesignTime>True</DesignTime>
148153
<DependentUpon>Resources.resx</DependentUpon>
149154
</Compile>
155+
<Compile Include="MetricHelper.cs" />
150156
<Compile Include="SecureStringExtensions.cs" />
151157
<Compile Include="ConversionUtilities.cs" />
152158
<Compile Include="DebugStreamTraceListener.cs" />
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Security.Cryptography;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
using Microsoft.ApplicationInsights;
8+
using Microsoft.ApplicationInsights.Channel;
9+
using Microsoft.ApplicationInsights.DataContracts;
10+
using Microsoft.ApplicationInsights.Extensibility;
11+
using Microsoft.WindowsAzure.Commands.Utilities.Common;
12+
13+
namespace Microsoft.WindowsAzure.Commands.Common
14+
{
15+
public static class MetricHelper
16+
{
17+
private const int FlushTimeoutInMilli = 5000;
18+
private static readonly TelemetryClient TelemetryClient;
19+
20+
static MetricHelper()
21+
{
22+
TelemetryClient = new TelemetryClient();
23+
// TODO: InstrumentationKey shall be injected in build server
24+
TelemetryClient.InstrumentationKey = "7df6ff70-8353-4672-80d6-568517fed090";
25+
// Disable IP collection
26+
TelemetryClient.Context.Location.Ip = "0.0.0.0";
27+
28+
if (TestMockSupport.RunningMocked)
29+
{
30+
TelemetryConfiguration.Active.DisableTelemetry = true;
31+
}
32+
}
33+
34+
public static void LogQoSEvent(AzurePSQoSEvent qos, bool isUsageMetricEnabled, bool isErrorMetricEnabled)
35+
{
36+
if (!IsMetricTermAccepted())
37+
{
38+
return;
39+
}
40+
41+
if (isUsageMetricEnabled)
42+
{
43+
LogUsageEvent(qos);
44+
}
45+
46+
if (isErrorMetricEnabled && qos.Exception != null)
47+
{
48+
LogExceptionEvent(qos);
49+
}
50+
}
51+
52+
private static void LogUsageEvent(AzurePSQoSEvent qos)
53+
{
54+
var tcEvent = new RequestTelemetry(qos.CmdletType, qos.StartTime, qos.Duration, string.Empty, qos.IsSuccess);
55+
tcEvent.Context.User.Id = qos.Uid;
56+
tcEvent.Context.User.UserAgent = AzurePowerShell.UserAgentValue.ToString();
57+
tcEvent.Context.Device.OperatingSystem = Environment.OSVersion.VersionString;
58+
59+
TelemetryClient.TrackRequest(tcEvent);
60+
}
61+
62+
private static void LogExceptionEvent(AzurePSQoSEvent qos)
63+
{
64+
//Log as custome event to exclude actual exception message
65+
var tcEvent = new EventTelemetry("CmdletError");
66+
tcEvent.Properties.Add("ExceptionType", qos.Exception.GetType().FullName);
67+
tcEvent.Properties.Add("StackTrace", qos.Exception.StackTrace);
68+
if (qos.Exception.InnerException != null)
69+
{
70+
tcEvent.Properties.Add("InnerExceptionType", qos.Exception.InnerException.GetType().FullName);
71+
tcEvent.Properties.Add("InnerStackTrace", qos.Exception.InnerException.StackTrace);
72+
}
73+
74+
tcEvent.Context.User.Id = qos.Uid;
75+
tcEvent.Properties.Add("CmdletType", qos.CmdletType);
76+
77+
TelemetryClient.TrackEvent(tcEvent);
78+
}
79+
80+
public static bool IsMetricTermAccepted()
81+
{
82+
return AzurePSCmdlet.IsDataCollectionAllowed();
83+
}
84+
85+
public static void FlushMetric(bool waitForMetricSending)
86+
{
87+
if (!IsMetricTermAccepted())
88+
{
89+
return;
90+
}
91+
92+
var flushTask = Task.Run(() => FlushAi());
93+
if (waitForMetricSending)
94+
{
95+
Task.WaitAll(new[] { flushTask }, FlushTimeoutInMilli);
96+
}
97+
}
98+
99+
private static void FlushAi()
100+
{
101+
try
102+
{
103+
TelemetryClient.Flush();
104+
}
105+
catch
106+
{
107+
// ignored
108+
}
109+
}
110+
111+
/// <summary>
112+
/// Gereate a SHA256 Hash string from the originInput.
113+
/// </summary>
114+
/// <param name="originInput"></param>
115+
/// <returns></returns>
116+
public static string GenerateSha256HashString(string originInput)
117+
{
118+
SHA256 sha256 = SHA256.Create();
119+
var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(originInput));
120+
return Encoding.UTF8.GetString(bytes);
121+
}
122+
}
123+
}
124+
125+
public class AzurePSQoSEvent
126+
{
127+
private readonly Stopwatch _timer;
128+
129+
public DateTimeOffset StartTime { get; set; }
130+
public TimeSpan Duration { get; set; }
131+
public bool IsSuccess { get; set; }
132+
public string CmdletType { get; set; }
133+
public Exception Exception { get; set; }
134+
public string Uid { get; set; }
135+
136+
public AzurePSQoSEvent()
137+
{
138+
StartTime = DateTimeOffset.Now;
139+
_timer = new Stopwatch();
140+
_timer.Start();
141+
}
142+
143+
public void PauseQoSTimer()
144+
{
145+
_timer.Stop();
146+
}
147+
148+
public void ResumeQosTimer()
149+
{
150+
_timer.Start();
151+
}
152+
153+
public void FinishQosEvent()
154+
{
155+
_timer.Stop();
156+
Duration = _timer.Elapsed;
157+
}
158+
159+
public override string ToString()
160+
{
161+
return string.Format(
162+
"AzureQoSEvent: CmdletType - {0}; IsSuccess - {1}; Duration - {2}; Exception - {3};",
163+
CmdletType, IsSuccess, Duration, Exception);
164+
}
165+
}

src/Common/Commands.Common/packages.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<packages>
33
<package id="Hyak.Common" version="1.0.2" targetFramework="net45" />
4+
<package id="Microsoft.ApplicationInsights" version="1.1.1-beta" targetFramework="net45" />
45
<package id="Microsoft.AspNet.WebApi.Client" version="5.2.2" targetFramework="net45" />
56
<package id="Microsoft.Azure.Common" version="2.1.0" targetFramework="net45" />
67
<package id="Microsoft.Azure.Common.Authentication" version="1.1.3-preview" targetFramework="net45" />

src/ResourceManager/Compute/Commands.Compute/Common/ComputeClientBaseCmdlet.cs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ public abstract class ComputeClientBaseCmdlet : AzurePSCmdlet
2323
{
2424
protected const string VirtualMachineExtensionType = "Microsoft.Compute/virtualMachines/extensions";
2525

26+
protected override bool IsUsageMetricEnabled
27+
{
28+
get { return true; }
29+
}
30+
2631
private ComputeClient computeClient;
2732

2833
public ComputeClient ComputeClient
@@ -54,18 +59,11 @@ protected void ExecuteClientAction(Action action)
5459
{
5560
try
5661
{
57-
try
58-
{
59-
action();
60-
}
61-
catch (CloudException ex)
62-
{
63-
throw new ComputeCloudException(ex);
64-
}
62+
action();
6563
}
66-
catch (Exception ex)
64+
catch (CloudException ex)
6765
{
68-
WriteExceptionError(ex);
66+
throw new ComputeCloudException(ex);
6967
}
7068
}
7169
}

src/ResourceManager/Compute/Commands.Compute/VirtualMachine/Config/NewAzureVMConfigCommand.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ public class NewAzureVMConfigCommand : AzurePSCmdlet
5151
[ValidateNotNullOrEmpty]
5252
public string AvailabilitySetId { get; set; }
5353

54+
protected override bool IsUsageMetricEnabled
55+
{
56+
get { return true; }
57+
}
58+
5459
public override void ExecuteCmdlet()
5560
{
5661
var vm = new PSVirtualMachine

0 commit comments

Comments
 (0)