Powershell - Simplified Logging - Is this legid? - powershell

Today I have dealt with logging in PowerShell as well as with the different streams and the pipeline. Unfortunately, none of these solutions really met my needs.
My requirements are:
I need to output information from PowerShell to a file log.
The file log has a predefined structure. So it's not enough to just redirect all streams to the file.
I want to use mainly standard PowerShell functions such as Write-Error, Write-Warning, Write-Verbose.
The overhead in the code through logging should be minimal.
I have now developed the following idea:
When calling a function from my script, all streams are piped to a logging function.
This function separates the debug, verbose, warning and error objects from the resulting object.
The resulting object is released back into the pipeline.
Here is my solution:
function Split-Streams {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline)]
$InputStream
)
process{
switch($InputStream.GetType())
{
'System.Management.Automation.DebugRecord' {
# Do whatever you want, like formatting an writing to a file.
Write-Host $InputStream -ForegroundColor Gray
}
'System.Management.Automation.ErrorRecord' {
Write-Host $InputStream -ForegroundColor Red
Write-Host ('Error function: {0}' -f $InputStream[0].InvocationInfo.MyCommand.Name) -ForegroundColor Red
}
'System.Management.Automation.VerboseRecord' { Write-Host $InputStream -ForegroundColor Cyan }
'System.Management.Automation.WarningRecord' { Write-Host $InputStream -ForegroundColor Yellow }
default { return $InputStream }
}
}
}
function Write-Messages
{
[CmdletBinding()]
param()
Write-Debug "Debug message"
Write-Output "Output message"
Write-Verbose "Verbose message"
Write-Warning "Warning message"
Write-Error "Error message"
}
$Test2 = Write-Messages -Verbose -Debug *>&1 | Split-Streams
Write-Host $Test2 -ForegroundColor White
So now my question:
Is there something wrong with my solution? Have I missed any problems?
Translated with www.DeepL.com/Translator

Related

Write-Information does not show in a file transcribed by Start-Transcript

I'm using PowerShell 5.1 and I am trying to determine why Write-Information messages do not show in the transcript log created by Start-Transcript unless I set $InformationPreference to SilentlyContinue. I want to both display the messages in the console and have them written to the log file.
I looked here:
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_preference_variables?view=powershell-5.1#informationpreference
Then I decided to create this script to test what gets written and when. See the preference section right underneath Testing explicit behavior with transcripts -------------
Clear-Host
$ErrorActionPreference = "Stop"
try {
Write-Host "Starting transcript"
Start-Transcript -Force -Path "$PSScriptRoot\default.txt"
<#
In PowerShell 5.1 the default behavior is as follows:
$DebugPreference = SilentlyContinue
$InformationPreference = SilentlyContinue
$ProgressPreference = Continue
$VerbosePreference = SilentlyContinue
See the following for more information:
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_preference_variables?view=powershell-5.1
#>
# I am not testing Write-Output as I am not worried about programmatic/pipeline stuff, just contextual messages for end-users or logging
Write-Host "`nTesting default behavior with transcripts --------------------------------`n"
# Setting these just in case I launch this script in a session where a previous script might have modified the preference variables
$DebugPreference = "SilentlyContinue"
$InformationPreference = "SilentlyContinue"
$ProgressPreference = "Continue"
$VerbosePreference = "SilentlyContinue"
Write-Host "Calling Write-Host"
Write-Debug "Calling Write-Debug"
Write-Error "Calling Write-Error" -ErrorAction "Continue"
Write-Information "Calling Write-Information"
Write-Progress "Calling Write-Progress"
Write-Verbose "Calling Write-Verbose"
Stop-Transcript
Start-Transcript -Force -Path "$PSScriptRoot\everything_continue.txt"
Write-Host "`nTesting explicit behavior with transcripts --------------------------------`n"
# Turn everything on
$DebugPreference = "Continue"
$InformationPreference = "Continue" # Setting this to SilentlyContinue makes it show up in the log but not the console. Setting this to 'Continue' makes it show up in the console but not the log.
$ProgressPreference = "Continue"
$VerbosePreference = "Continue"
Write-Host "Calling Write-Host"
Write-Debug "Calling Write-Debug"
Write-Error "Calling Write-Error" -ErrorAction "Continue"
Write-Information "Calling Write-Information"
Write-Progress "Calling Write-Progress"
Write-Verbose "Calling Write-Verbose"
Stop-Transcript
Write-Host "`nResults -------------------------------------------------------------------`n"
# See what actually gets captured and written by the transcriber
$messageTypes = #("Write-Debug", "Write-Error", "Write-Host", "Write-Information", "Write-Verbose")
Write-Host "Default" -ForegroundColor Cyan
$lines = Get-Content "$PSScriptRoot\default.txt"
foreach ($message in $messageTypes) {
if ($lines -like "*Calling $message*") {
Write-Host " $message PRESENT" -ForegroundColor Green
}
else {
Write-Host " $message MISSING" -ForegroundColor Red
}
}
Write-Host "Everything Continue" -ForegroundColor Cyan
$lines = Get-Content "$PSScriptRoot\everything_continue.txt"
foreach ($message in $messageTypes) {
if ($lines -like "*Calling $message*") {
Write-Host " $message PRESENT" -ForegroundColor Green
}
else {
Write-Host " $message MISSING" -ForegroundColor Red
}
}
}
catch {
Write-Host "----------------------------------------------------------------------------------------------------"
Write-Host $_.Exception
Write-Host $_.ScriptStackTrace
Write-Host "----------------------------------------------------------------------------------------------------"
try { Stop-Transcript } catch { }
throw $_
}
What you're seeing is a bug in Windows PowerShell (as of v5.1.17134.590) that has been fixed in PowerShell Core (as of at least v6.1.0 - though other transcript-related problems persist; see this GitHub issue).
I encourage you to report it in the Windows PowerShell UserVoice forum (note that the PowerShell GitHub-repo issues forum is only for errors also present in PowerShell Core).
Here's how to verify if the bug is present in your PowerShell version:
Create a script with the code below and run it:
'--- Direct output'
$null = Start-Transcript ($tempFile = [io.path]::GetTempFileName())
# Note that 'SilentlyContinue' is also the default value.
$InformationPreference = 'SilentlyContinue'
# Produces no output.
Write-Information '1-information'
# Prints '2-Information' to the console.
Write-Information '2-information' -InformationAction Continue
$null = Stop-Transcript
'--- Write-Information output transcribed:'
Select-String '-information' $tempFile | Select-Object -ExpandProperty Line
Remove-Item $tempFile
With the bug present (Windows PowerShell), you'll see:
--- Direct output
2-information
--- Write-Information output transcribed:
INFO: 1-information
That is, the opposite of the intended behavior occurred: the transcript logged the call it should'nt have (because it produced no output), and it didn't log the one it should have.
Additionally, the logged output is prefixed with INFO: , which is an inconsistency that has also been fixed in PowerShell Core.
There is no full workaround, except that you can use Write-Host calls in cases where do you want the output logged in the transcript - but such calls will be logged unconditionally, irrespective of the value of preference variable $InformationPreference (while Write-Host formally provides an -InformationAction common parameter, it is ignored).
With the bug fixed (PowerShell Core), you'll see:
--- Direct output
2-information
--- Write-Information output transcribed:
2-information
The transcript is now consistent with the direct output.

How to handle Active Directory exceptions via powershell?

I am trying to handle an ActiveDirectoryObjectNotFoundException exception in PowerShell when using the Forest.GetForest method.
https://msdn.microsoft.com/en-us/library/system.directoryservices.activedirectory.forest.getforest(v=vs.110).aspx
# Clear screen
Clear
# Change below as per your requirements
$context='forest'
$name='My.Lab.Local'
$username="fake\Administrator"
$password="FakePassword"
Write-Host -Object "Connecting $context... -> $name " -BackgroundColor Yellow -ForegroundColor Blue
try
{
$DC = new-object System.DirectoryServices.ActiveDirectory.DirectoryContext($context,$name,$username,$password)
Write-Host -Object "Successfully connected to $context using discovery account $username." -BackgroundColor Green -ForegroundColor Blue
Write-Host -Object "Retrieving details of the forest..." -BackgroundColor Yellow -ForegroundColor Blue
$Forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($DC)
$Forest.Name
Write-Host -Object "Successfully retrived your forest..." -BackgroundColor Green -ForegroundColor Blue
}
catch [System.DirectoryServices.ActiveDirectory.ActiveDirectoryObjectNotFoundException]
{
Write-Host "ActiveDirectoryObjectNotFoundException exception"
}
catch [System.Security.Authentication.AuthenticationException]
{
Write-Host "AuthenticationException exception ( Catch Block )"
}
finally
{
Write-Host "cleaning up ...( Finally Block )"
}
The Output
Connecting forest... -> Web.Metacash.Com
Successfully connected to forest using discovery account fake\Administrator.
Retrieving details of the forest...
AuthenticationException exception ( Catch Block )
cleaning up ...( Finally Block )
How to do I get the original failing message instead of giving my own message, like using $_.message or something?
$_ should have an ErrorRecord in the catch block. The exception should be in there. For example, use $_.Exception.Message to get its message. Of course the error record has more info about the error. $_.InvocationInfo.ScriptLineNumber would have the line number where the error occurred.

How to propagate -Verbose to module functions?

According to answers like this one and my own experience, Powershell can take care of propagating -Verbose (and -Debug) automatically, which is very convenient. However this stops working when the functions which I want to propagate verbosity to are in a module. Code used for testing this:
Create a directory called Mod somewhere, suppose in c:, and add 2 files:
File c:\Mod\Functions.ps1:
function Show-VerbosityB { [cmdletbinding()]Param()
Write-Output "Show-VerbosityB called"
Write-Verbose "Show-VerbosityB is Verbose"
}
File c:\Mod\Mod.psd1:
#{
ModuleVersion = '1.0.0.0'
NestedModules = #('Functions.ps1')
FunctionsToExport = #('*-*')
}
Now crate the main script, say c:\Foo.ps1:
Import-Module c:\Mod
function Show-VerbosityA { [cmdletbinding()]Param()
Write-Output "Show-VerbosityA called"
Write-Verbose "Show-VerbosityA is Verbose"
}
function Show-Verbosity { [cmdletbinding()]Param()
Write-Output "Show-Verbosity called"
Write-Verbose "Show-Verbosity is Verbose"
Write-Output "Testing propagation"
Show-VerbosityA
Show-VerbosityB
}
Show-Verbosity -Verbose
Results in
PS> . C:\Foo.ps1
Show-Verbosity called
VERBOSE: Show-Verbosity is Verbose
Testing propagation
Show-VerbosityA called
VERBOSE: Show-VerbosityA is Verbose
Show-VerbosityB called
Why is the Write-Verbose in the module's function skipped, why does propagation not behave like it does for Show-VerbosityA? (If I just dot-source Functions.ps1 instead of importing the module, the line VERBOSE: Show-VerbosityB is Verbose is printed). I could make propagation manual by e.g. calling Show-VerbosityB -Verbose:$PSBoundParameters['Verbose']. Or are there other, preferrably shorter, ways? It is quite messy if functions behave differently depending on whether they are part of a module or dot-sourced.
The reason this is happening is because the $VerbosePreference is not propagated when the module is called.
I modified your script to explicitly print the value at the same points you are outputting via Write-Verbose and Write-Output.
This powershell.org post proposes adding this to the module, which worked like a charm for me:
if (-not $PSBoundParameters.ContainsKey('Verbose'))
{
$VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
}
One of the comments mentions bug report with link (it doesn't exist or I don't have permissions to view)
The issue is discussed in a TechNet post, with a link to a Get-CallerPreferance function that addresses this issue.
Module:
function Show-VerbosityB { [cmdletbinding()]Param()
<# uncomment to get verbose preference from caller, when verbose switch not explicitly used.
if (-not $PSBoundParameters.ContainsKey('Verbose'))
{
$VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
}
#>
Write-Output "`nShow-VerbosityB called"
Write-output "Global pref: $($global:VerbosePreference)"
Write-output "Script pref: $($script:VerbosePreference)"
Write-output "Effect pref: $VerbosePreference"
Write-Verbose "Show-VerbosityB is Verbose"
}
Caller:
Import-Module C:\Mod
Write-output "On startup: $VerbosePreference"
function Show-VerbosityA { [cmdletbinding()]Param()
Write-Output "`nShow-VerbosityA called"
Write-output "Global pref: $($global:VerbosePreference)"
Write-output "Script pref: $($script:VerbosePreference)"
Write-output "Effect pref: $VerbosePreference"
Write-Verbose "Show-VerbosityA is Verbose"
}
function Show-Verbosity { [cmdletbinding()]Param()
Write-Output "`nShow-Verbosity called"
Write-output "Global pref: $($global:VerbosePreference)"
Write-output "Script pref: $($script:VerbosePreference)"
Write-output "Effect pref: $VerbosePreference"
Write-Verbose "Show-Verbosity is Verbose"
Write-Output "`nTesting propagation"
Show-VerbosityA
Show-VerbosityB
}
Show-Verbosity -Verbose
If you want set from the caller script, try this:
(Get-Module 'ModuleName').SessionState.PSVariable.Set('Global:VerbosePreference', $VerbosePreference )

Use Begin, Process, End in Scriptblock

Is it possible to use the adavanced function features Begin, Process, End in a script-block?
For example I've the following script block:
$startStopService = {
Param(
[bool] $startService)
if ($startService){
...
Start-Service "My-Service"
}
else {
Stop-Service "My-Service"
}
}
Since I want to be able to control the verbose output of the scriptblock I want to change the block to:
$startStopService = {
Param(
[bool] $startService)
Begin {
$oldPreference = $VerbosePreference
$VerbosePreference = $Using:VerbosePreference
}
Process {
if ($startService){
...
Start-Service "My-Service"
}
else {
Stop-Service "My-Service"
}
}
End {
# Restore the old preference
$VerbosePreference = $oldPreference
}
}
Is it possible to use Begin, Process, End here, though the scriptblock isn't a cmdlet? I simply want that the VerbosePreference gets restored to the old value, regardless an error occurred or not. Of course I could use try{}finally{} as an alternative, but I find that Begin, Process, End is more intuitive.
Thx
It is possible, as described in about_script_blocks:
Like functions, script blocks can include the DynamicParam, Begin,
Process, and End keywords. For more information, see about_Functions
and about_Functions_Advanced.
To test this out, I modified your scriptblock and ran this:
$startStopService = {
Param(
# a bool needs $true or $false passed AFAIK
# A switch is $true if specified, $false if not included
[switch] $startService
)
Begin {
$oldPreference = $VerbosePreference
Write-Output "Setting VerbosePreference to Continue"
# $Using:VerbosePreference gave me an error
$VerbosePreference = "Continue"
}
Process {
if ($startService){
Write-Verbose "Service was started"
}
else {
Write-Verbose "Service was not started"
}
}
End {
# Restore the old preference
Write-Output "Setting VerbosePreference back to $oldPreference"
$VerbosePreference = $oldPreference
}
}
Write-Verbose "This message will not print if VerbosePreference is the default SilentlyContinue"
. $startStopService -startService
Write-Verbose "This message will not print if VerbosePreference is the default SilentlyContinue"
What functionality are you after? If you would to print verbose messages when running a scriptblock but not change the $VerbosePreference in the rest of the script , consider using [CmdletBinding()] and the -Verbose flag:
$startStopService = {
[CmdLetBinding()]
Param(
[switch] $startService
)
Write-Verbose "This is a verbose message"
}
Write-Verbose "This message will not print if VerbosePreference is the default SilentlyContinue"
. $startStopService -verbose
Write-Verbose "This message will not print if VerbosePreference is the default SilentlyContinue"
Edit - Invoke-Command
After your comment, I looking into the functionality of Invoke-Command. And found a lot of things that don't work.
The short version that I believe is most useful to you: you can declare $VerbosePreference = "Continue" within a scriptblock and this will be limited to the scope of the scriptblock. No need to change back after.
$startStopService = {
[CmdLetBinding()]
Param(
[parameter(Position=0)]
[switch]$startStopService,
[parameter(Position=1)]
[switch]$Verbose
)
if($Verbose){
$VerbosePreference = "Continue"
}
Write-Verbose "This is a verbose message"
}
Write-output "VerbosePreference: $VerbosePreference"
Write-Verbose "This message will not print if VerbosePreference is the default SilentlyContinue"
Invoke-Command -Scriptblock $startStopService -ArgumentList ($true,$true)
Write-output "VerbosePreference: $VerbosePreference"
Write-Verbose "This message will not print if VerbosePreference is the default SilentlyContinue"
Trying to pass the -Verbose switch CommonParameter to Invoke-Command was a no-go. This uses a standard Verbose switch parameter that allows you to pass $true/$false (or omit) to control the verbose output.
Related:
about_Functions
about_Functions_Advanced

Stop service and take back up of respected folder items and start service

I wrote a script which will
stop different services of two or more servers (one server containing two services),
take a back up of respected folders (folder name starting with Job) to backup folder,
copy newest files from staging location to destination folder,
start different services in servers.
Looks like I have chosen long procedure to do it. Please suggest if I can modify the script.
Function Get-Kettle
{
$DestinationFolder = "D:\AppCode\Kettle"
$BackUpFolder = "D:\AppCode\Kettle\BackUp"
$StagingFolder = "\\server001\e$\Packages\RTO\RTO\ETL"
$ServerList = #("server222")
$ServicesList = #("WindowsScheduler","WindowsSchedulerLogon")
Foreach ($Server in $ServerList)
{
$CheckStagingFolder = Get-ChildItem $StagingFolder
if($CheckStagingFolder.count)
{
Write-Host "StagingFolder contains files.. Continue with Deployment"
if((Test-Path "$DestinationFolder") -and (Test-Path "$BackUpFolder"))
{
Write-Host "Taking BackUp"
Copy-Item "$DestinationFolder" -Destination "$BackUpFolder"
Write-Host "BackUp is completed"
Write-Host "Stopping the service WindowsSchedulerLogon"
Stop-Service $Server.WindowsSchedulerLogon -Force
Write-Host "WindowsSchedulerLogon service is stopped"
Write-Host "Stopping the service WindowsScheduler"
Stop-Service WindowsScheduler -Force
Write-Host "WindowsScheduler service is stopped"
Copy-Item "$StagingFolder" -Destination "$DestinationFolder" -Recurse
Write-Host "Starting the service WindowsSchedulerLogon"
Start-Service WindowsSchedulerLogon -Force
Write-Host "WindowsSchedulerLogon service is Started"
Write-Host "Starting the service WindowsScheduler"
Start-Service WindowsScheduler -Force
Write-Host "WindowsScheduler service is started"
Write-Host "Deployment is completed"
}
else
{
Write-Host "No Destination and BackUp Folder..Script Exiting...!"
Exit;
}
}
else
{
Write-Host "StagingFolder does not contains files.. Exiting with Deployment"
exit;
}
}
}
Get-Kettle
I agree this would be better placed on Code Review but as it's still open here I thought I'd suggest some improvements:
Add [cmdletbinding()] and put your variables at the top in a Param() block. Cmdletbinding means you get access to a set of common parameters that allow you to utilise a series of builtin functionality (such as Write-Verbose which I will mention later). Putting your variables in a Param block means you can change them by calling them in your Function, just like you do with built-in cmdlets.
Add Begin, Process and End blocks. I can't see any need to do anything in Begin or End at this point, but the rest of your code can live in Process. This means you could later change this function to support pipeline input if you wanted to.
Change all your Write-Host statements to Write-Verbose. Write-Host is considered harmful because it interferes with automation. Write-Verbose means you can see information messages when you want to but not by default, and those messages to a separate output stream that doesn't then interfere with the output of the function. To see the messages simply do Get-Kettle -Verbose. A further benefit of this is that it will also turn on the -Verbose messages that are built in to the standard cmdlets you are using (such as Copy-Item or Start-Service).
You can should change your last two Write-Host statements (in the else blocks) to Write-Warning messages, these will then always be displayed (regardless of -verbose etc.) when you need to inform the user of something but again, do not interfere the default output pipeline.
You can drop the Exit statements as they not really achieving anything.
Other improvements you might consider include:
Accepting input from the pipeline.
Parameter sets or validating on your parameters and setting variable types on them (such as [string] etc.).
Supporting -confirm and -whatif by adding supportsshouldprocess and putting these around the parts of the script that make changes.
All of these things are features of what is called an Advanced function or Cmdlet. If you haven't read it, take a look at Learn PowerShell Toolmaking in a Month of Lunches as it covers these topics and a lot more.
Here's a copy of your code with the first set of improvements I suggested implemented:
Function Get-Kettle
{
[cmdletbinding()]
Param(
$DestinationFolder = "D:\AppCode\Kettle",
$BackUpFolder = "D:\AppCode\Kettle\BackUp",
$StagingFolder = "\\server001\e$\Packages\RTO\RTO\ETL",
$ServerList = #("server222"),
$ServicesList = #("WindowsScheduler","WindowsSchedulerLogon")
)
Begin {}
Process {
Foreach ($Server in $ServerList)
{
$CheckStagingFolder = Get-ChildItem $StagingFolder
if($CheckStagingFolder.count)
{
Write-Verbose "StagingFolder contains files.. Continue with Deployment"
if((Test-Path "$DestinationFolder") -and (Test-Path "$BackUpFolder"))
{
Write-Verbose "Taking BackUp"
Copy-Item "$DestinationFolder" -Destination "$BackUpFolder"
Write-Verbose "BackUp is completed"
Write-Verbose "Stopping the service WindowsSchedulerLogon"
Stop-Service $Server.WindowsSchedulerLogon -Force
Write-Verbose "WindowsSchedulerLogon service is stopped"
Write-Verbose "Stopping the service WindowsScheduler"
Stop-Service WindowsScheduler -Force
Write-Verbose "WindowsScheduler service is stopped"
Copy-Item "$StagingFolder" -Destination "$DestinationFolder" -Recurse
Write-Verbose "Starting the service WindowsSchedulerLogon"
Start-Service WindowsSchedulerLogon -Force
Write-Verbose "WindowsSchedulerLogon service is Started"
Write-Verbose "Starting the service WindowsScheduler"
Start-Service WindowsScheduler -Force
Write-Verbose "WindowsScheduler service is started"
Write-Verbose "Deployment is completed"
}
else
{
Write-Warning "No Destination and BackUp Folder..Script Exiting...!"
}
}
else
{
Write-Warning "StagingFolder does not contains files.. Exiting with Deployment"
}
}
}
End {}
}
Get-Kettle -Verbose