Skip to content

Commit fa8d316

Browse files
committed
Implemented custom listener to log to memory, added logging to all cmdlets
1 parent 034ee33 commit fa8d316

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+588
-190
lines changed

resources/PnP.PowerShell.Format.ps1xml

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3157,6 +3157,50 @@
31573157
</TableRowEntry>
31583158
</TableRowEntries>
31593159
</TableControl>
3160-
</View>
3160+
</View>
3161+
<View>
3162+
<Name>CheckedOutFile</Name>
3163+
<ViewSelectedBy>
3164+
<TypeName>PnP.PowerShell.Commands.Utilities.Logging.TraceLogEntry</TypeName>
3165+
</ViewSelectedBy>
3166+
<TableControl>
3167+
<TableHeaders>
3168+
<TableColumnHeader>
3169+
<Label>TimeStamp</Label>
3170+
<Alignment>left</Alignment>
3171+
</TableColumnHeader>
3172+
<TableColumnHeader>
3173+
<Label>Source</Label>
3174+
<Alignment>left</Alignment>
3175+
</TableColumnHeader>
3176+
<TableColumnHeader>
3177+
<Label>Level</Label>
3178+
<Alignment>left</Alignment>
3179+
</TableColumnHeader>
3180+
<TableColumnHeader>
3181+
<Label>Message</Label>
3182+
<Alignment>left</Alignment>
3183+
</TableColumnHeader>
3184+
</TableHeaders>
3185+
<TableRowEntries>
3186+
<TableRowEntry>
3187+
<TableColumnItems>
3188+
<TableColumnItem>
3189+
<PropertyName>TimeStamp</PropertyName>
3190+
</TableColumnItem>
3191+
<TableColumnItem>
3192+
<PropertyName>Source</PropertyName>
3193+
</TableColumnItem>
3194+
<TableColumnItem>
3195+
<PropertyName>Level</PropertyName>
3196+
</TableColumnItem>
3197+
<TableColumnItem>
3198+
<PropertyName>Message</PropertyName>
3199+
</TableColumnItem>
3200+
</TableColumnItems>
3201+
</TableRowEntry>
3202+
</TableRowEntries>
3203+
</TableControl>
3204+
</View>
31613205
</ViewDefinitions>
31623206
</Configuration>

src/Commands/Admin/GetTimeZoneId.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
using System.Collections.Generic;
44
using System.Linq;
55
using System.Management.Automation;
6+
using PnP.PowerShell.Commands.Base;
67

78
namespace PnP.PowerShell.Commands
89
{
910
[Cmdlet(VerbsCommon.Get, "PnPTimeZoneId")]
10-
public class GetTimeZoneId : PSCmdlet
11+
public class GetTimeZoneId : BasePSCmdlet
1112
{
1213
[Parameter(Mandatory = false, Position = 0)]
1314
public string Match;

src/Commands/Admin/NewSite.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,12 +190,12 @@ protected override void ExecuteCmdlet()
190190
}
191191
catch (Exception ex)
192192
{
193-
WriteError(ex, ErrorCategory.WriteError);
193+
WriteError(ex);
194194
}
195195
}
196196
else
197197
{
198-
WriteError(new PSInvalidOperationException("Creating a new teamsite requires an underlying Microsoft 365 group. In order to create this we need to acquire an access token for the Microsoft Graph. This is not possible using ACS App Only connections."), ErrorCategory.SecurityError);
198+
WriteError(new PSInvalidOperationException("Creating a new teamsite requires an underlying Microsoft 365 group. In order to create this we need to acquire an access token for the Microsoft Graph. This is not possible using ACS App Only connections."));
199199
}
200200
}
201201
else

src/Commands/AzureAD/GetAzureADAppPermission.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ protected override void ExecuteCmdlet()
2323
var app = Identity.GetApp(GraphRequestHelper);
2424
if (app == null)
2525
{
26-
WriteError(new PSArgumentException("Azure AD App not found"), ErrorCategory.ObjectNotFound);
26+
WriteError(new PSArgumentException("Azure AD App not found"));
2727
}
2828
WriteObject(ConvertToPSObject(app));
2929
}

src/Commands/AzureAD/RegisterManagementShellAccess.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
using PnP.Framework;
2+
using PnP.PowerShell.Commands.Base;
23
using PnP.PowerShell.Commands.Utilities;
34
using System;
45
using System.Management.Automation;
56

67
namespace PnP.PowerShell.Commands.AzureAD
78
{
89
[Cmdlet(VerbsLifecycle.Register, "PnPManagementShellAccess")]
9-
public class RegisterManagementShellAccess : PSCmdlet
10+
public class RegisterManagementShellAccess : BasePSCmdlet
1011
{
1112
private const string ParameterSet_REGISTER = "Register access";
1213
private const string ParameterSet_SHOWURL = "Show Consent Url";

src/Commands/Base/AddStoredCredential.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace PnP.PowerShell.Commands.Base
55
{
66
[Cmdlet(VerbsCommon.Add, "PnPStoredCredential")]
77
[OutputType(typeof(void))]
8-
public class AddStoredCredential : PSCmdlet
8+
public class AddStoredCredential : BasePSCmdlet
99
{
1010
[Parameter(Mandatory = true)]
1111
public string Name;

src/Commands/Base/BasePSCmdlet.cs

Lines changed: 117 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -9,53 +9,79 @@ namespace PnP.PowerShell.Commands.Base
99
/// </summary>
1010
public class BasePSCmdlet : PSCmdlet
1111
{
12+
/// <summary>
13+
/// Generate a new correlation id for each cmdlet execution. This is used to correlate log entries in the PnP PowerShell log stream.
14+
/// </summary>
15+
internal Guid? CorrelationId { get; } = Guid.NewGuid();
16+
17+
#region Cmdlet execution
18+
19+
/// <summary>
20+
/// Triggered when the cmdlet is started. This is the place to do any initialization work.
21+
/// </summary>
1222
protected override void BeginProcessing()
1323
{
14-
Framework.Diagnostics.Log.Debug("PnPPowerShell", $"Executing {MyInvocation.MyCommand.Name}");
24+
LogDebug("Cmdlet execution started");
1525
base.BeginProcessing();
16-
PnP.Framework.Diagnostics.Log.Info("PnP.PowerShell", $"Executing {this.MyInvocation.InvocationName}");
17-
if (MyInvocation.MyCommand.Name.ToLower() != MyInvocation.InvocationName.ToLower())
26+
27+
CheckForDeprecationAttributes();
28+
}
29+
30+
/// <summary>
31+
/// Executes the cmdlet. This is the place to do the actual work of the cmdlet.
32+
/// </summary>
33+
protected virtual void ExecuteCmdlet()
34+
{ }
35+
36+
/// <summary>
37+
/// Triggered for the execution of the cmdlet. Use ExecuteCmdlet() to do the actual work of the cmdlet.
38+
/// </summary>
39+
protected override void ProcessRecord()
40+
{
41+
try
1842
{
19-
var attribute = Attribute.GetCustomAttribute(this.GetType(), typeof(WriteAliasWarningAttribute));
20-
if (attribute != null)
43+
ExecuteCmdlet();
44+
}
45+
catch (Model.Graph.GraphException gex)
46+
{
47+
var errorMessage = gex.Error.Message;
48+
49+
if (gex.Error.Code == "Authorization_RequestDenied")
2150
{
22-
var warningAttribute = attribute as WriteAliasWarningAttribute;
23-
if (!string.IsNullOrEmpty(warningAttribute?.DeprecationMessage))
51+
if (!string.IsNullOrEmpty(gex.AccessToken))
2452
{
25-
WriteWarning(warningAttribute.DeprecationMessage);
53+
TokenHandler.EnsureRequiredPermissionsAvailableInAccessTokenAudience(GetType(), gex.AccessToken);
2654
}
2755
}
56+
if (string.IsNullOrWhiteSpace(errorMessage) && gex.HttpResponse != null && gex.HttpResponse.StatusCode == System.Net.HttpStatusCode.Forbidden)
57+
{
58+
errorMessage = "Access denied. Check for the required permissions.";
59+
}
60+
throw new PSInvalidOperationException(errorMessage);
2861
}
29-
// if (PnPConnection.Current == null)
30-
// {
31-
// if (Settings.Current.LastUserTenant != null)
32-
// {
33-
// var clientid = PnPConnection.GetCacheClientId(Settings.Current.LastUserTenant);
34-
// if (clientid != null)
35-
// {
36-
// var cancellationTokenSource = new CancellationTokenSource();
37-
// PnPConnection.Current = PnPConnection.CreateWithInteractiveLogin(new Uri(Settings.Current.LastUserTenant.ToLower()), clientid, null, Framework.AzureEnvironment.Production, cancellationTokenSource, false, null, false, false, Host);
38-
// }
39-
40-
// }
41-
// }
4262
}
4363

64+
/// <summary>
65+
/// Triggered when the cmdlet is done executing. This is the place to do any cleanup or finalization work.
66+
/// </summary>
4467
protected override void EndProcessing()
4568
{
4669
base.EndProcessing();
70+
LogDebug("Cmdlet execution done");
4771
}
4872

4973
/// <summary>
50-
/// Checks if a parameter with the provided name has been provided in the execution command
74+
/// Triggered when the cmdlet is stopped
5175
/// </summary>
52-
/// <param name="parameterName">Name of the parameter to validate if it has been provided in the execution command</param>
53-
/// <returns>True if a parameter with the provided name is present, false if it is not</returns>
54-
public bool ParameterSpecified(string parameterName)
76+
protected override void StopProcessing()
5577
{
56-
return MyInvocation.BoundParameters.ContainsKey(parameterName);
78+
base.StopProcessing();
5779
}
5880

81+
#endregion
82+
83+
#region Helper methods
84+
5985
protected string ErrorActionSetting
6086
{
6187
get
@@ -67,42 +93,84 @@ protected string ErrorActionSetting
6793
}
6894
}
6995

70-
protected virtual void ExecuteCmdlet()
71-
{ }
72-
73-
protected override void ProcessRecord()
96+
/// <summary>
97+
/// Checks if deprecation attribute is present on the cmdlet and if so, writes a warning message to the console to notify the user to change their script to use the new cmdlet name.
98+
/// </summary>
99+
private void CheckForDeprecationAttributes()
74100
{
75-
try
76-
{
77-
ExecuteCmdlet();
78-
}
79-
catch (Model.Graph.GraphException gex)
101+
if (MyInvocation.MyCommand.Name.ToLower() != MyInvocation.InvocationName.ToLower())
80102
{
81-
var errorMessage = gex.Error.Message;
82-
83-
if (gex.Error.Code == "Authorization_RequestDenied")
103+
var attribute = Attribute.GetCustomAttribute(GetType(), typeof(WriteAliasWarningAttribute));
104+
if (attribute != null)
84105
{
85-
if (!string.IsNullOrEmpty(gex.AccessToken))
106+
var warningAttribute = attribute as WriteAliasWarningAttribute;
107+
if (!string.IsNullOrEmpty(warningAttribute?.DeprecationMessage))
86108
{
87-
TokenHandler.EnsureRequiredPermissionsAvailableInAccessTokenAudience(GetType(), gex.AccessToken);
109+
WriteWarning(warningAttribute.DeprecationMessage);
88110
}
89111
}
90-
if (string.IsNullOrWhiteSpace(errorMessage) && gex.HttpResponse != null && gex.HttpResponse.StatusCode == System.Net.HttpStatusCode.Forbidden)
91-
{
92-
errorMessage = "Access denied. Check for the required permissions.";
93-
}
94-
throw new PSInvalidOperationException(errorMessage);
95112
}
96113
}
97114

98-
protected override void StopProcessing()
115+
/// <summary>
116+
/// Checks if a parameter with the provided name has been provided in the execution command
117+
/// </summary>
118+
/// <param name="parameterName">Name of the parameter to validate if it has been provided in the execution command</param>
119+
/// <returns>True if a parameter with the provided name is present, false if it is not</returns>
120+
public bool ParameterSpecified(string parameterName)
99121
{
100-
base.StopProcessing();
122+
return MyInvocation.BoundParameters.ContainsKey(parameterName);
123+
}
124+
125+
#endregion
126+
127+
#region Logging
128+
129+
/// <summary>
130+
/// Allows logging an error
131+
/// </summary>
132+
/// <param name="exception">The exception to log as an error</param>
133+
internal void WriteError(Exception exception)
134+
{
135+
WriteError(exception.Message);
101136
}
102137

103-
internal void WriteError(Exception exception, ErrorCategory errorCategory, object target = null)
138+
/// <summary>
139+
/// Allows logging an error
140+
/// </summary>
141+
/// <param name="message">The message to log</param>
142+
internal void WriteError(string message)
143+
{
144+
Utilities.Logging.LoggingUtility.Error(this, message, correlationId: CorrelationId);
145+
}
146+
147+
/// <summary>
148+
/// Allows logging a debug message
149+
/// </summary>
150+
/// <param name="message">The message to log</param>
151+
internal void LogDebug(string message)
152+
{
153+
Utilities.Logging.LoggingUtility.Debug(this, message, correlationId: CorrelationId);
154+
}
155+
156+
/// <summary>
157+
/// Allows logging a warning
158+
/// </summary>
159+
/// <param name="message">The message to log</param>
160+
internal void LogWarning(string message)
161+
{
162+
Utilities.Logging.LoggingUtility.Warning(this, message, correlationId: CorrelationId);
163+
}
164+
165+
/// <summary>
166+
/// Allows logging an informational message
167+
/// </summary>
168+
/// <param name="message">The message to log</param>
169+
internal void LogInformational(string message)
104170
{
105-
WriteError(new ErrorRecord(exception, string.Empty, errorCategory, target));
171+
Utilities.Logging.LoggingUtility.Info(this, message, correlationId: CorrelationId);
106172
}
173+
174+
#endregion
107175
}
108176
}

src/Commands/Base/ClearTraceLog.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System.Diagnostics;
2+
using System.Management.Automation;
3+
using PnP.PowerShell.Commands.Utilities.Logging;
4+
5+
namespace PnP.PowerShell.Commands.Base
6+
{
7+
[Cmdlet(VerbsCommon.Clear, "PnPTraceLog")]
8+
[OutputType(typeof(void))]
9+
public class ClearTraceLog : BasePSCmdlet
10+
{
11+
protected override void ProcessRecord()
12+
{
13+
if (Trace.Listeners[LogStreamListener.DefaultListenerName] is not LogStreamListener logStreamListener)
14+
{
15+
LogWarning("Log stream listener named {LogStreamListener.DefaultLogStreamListenerName} not found. No entries cleared.");
16+
}
17+
else
18+
{
19+
LogDebug($"Clearing {(logStreamListener.Entries.Count != 1 ? $"{logStreamListener.Entries.Count} log entries" : "one log entry")} from log stream listener named {LogStreamListener.DefaultListenerName}");
20+
logStreamListener.Entries.Clear();
21+
}
22+
}
23+
}
24+
}

src/Commands/Base/DisconnectOnline.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace PnP.PowerShell.Commands.Base
99
{
1010
[Cmdlet(VerbsCommunications.Disconnect, "PnPOnline")]
1111
[OutputType(typeof(void))]
12-
public class DisconnectOnline : PSCmdlet
12+
public class DisconnectOnline : BasePSCmdlet
1313
{
1414

1515
[Parameter(Mandatory = false)]

src/Commands/Base/FormatTraceLog.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
using System.Management.Automation;
2-
using PnP.PowerShell.Model;
2+
using PnP.PowerShell.Commands.Utilities.Logging;
33

44
namespace PnP.PowerShell.Commands.Base
55
{
66
[Cmdlet(VerbsCommon.Format, "PnPTraceLog")]
7-
public class FormatTraceLog : PSCmdlet
7+
public class FormatTraceLog : BasePSCmdlet
88
{
9-
[Parameter(Mandatory = false, Position = 0, ValueFromPipeline = true, ParameterSetName = "On")]
9+
[Parameter(Mandatory = false, Position = 0, ValueFromPipeline = true)]
1010
public string LogLine;
1111

1212
protected override void ProcessRecord()

0 commit comments

Comments
 (0)