CT.WriteLog.psm1
Write-Verbose 'Importing from [C:\Projects\ct-writelog\CT.WriteLog\private]' # .\CT.WriteLog\private\Get-ModuleVariable.ps1 Function Get-ModuleVariable { [cmdletbinding()] Param( # Path help description [Parameter(ValueFromPipeline)] [string]$Path = "$Script:ModuleBase\lib\Variables.csv", [string]$VariableName, [switch]$All ) Begin { Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($MyInvocation.MyCommand)" # In order to make modules easier to use, they can be pre-loaded with variables. # To avoid releasing confidential information, module-wide variables are stored # in a CSV file in the .\lib directory. # The Variables.csv file should contain the name, value, and scope for each variable # used by the commands within the module. # If the variable needs to be exposed to the user, the Scope should be set to global, # otherwise the variable will just be available to the module. # In my testing, there was no difference between variables in the local scope, versus # the script scope, but I didn't test that much. if (Test-Path -Path $Path) { $VariablesInCSV = Import-Csv -Path $Path foreach ($Item in $VariablesInCSV) { Write-Verbose "Parsing variables in $Path" # Remove the variable if it exists, to prevent re-creating globally scoped variables if (Get-Variable -Name ExpandedValue -ErrorAction SilentlyContinue) { Remove-Variable -Name ExpandedValue -ErrorAction SilentlyContinue } # Convert string versions of true and false to boolean versions if needed if ($ExecutionContext.InvokeCommand.ExpandString($Item.Value) -in 'true','false') { [boolean]$ExpandedValue = [System.Convert]::ToBoolean($ExecutionContext.InvokeCommand.ExpandString($Item.Value)) } else { $ExpandedValue = $ExecutionContext.InvokeCommand.ExpandString($Item.Value) } if (!$Item.Scope) { $Scope = 'Script' } else { $Scope = $Item.Scope } if (! (Get-Variable -Name $Item.VariableName -ErrorAction SilentlyContinue) ) { Write-Verbose "Creating variable" New-Variable -Name $Item.VariableName -Value $ExpandedValue -Scope $Scope } } if ($VariableName) { Write-Verbose "Found $VariableName" Get-Variable -Name $VariableName } if ($All) { Get-Variable | Where-Object {$_.Name -in $VariablesInCSV.VariableName} } } } #begin } #close Get-ModuleVariable # .\CT.WriteLog\private\Invoke-LogRotation.ps1 function Invoke-LogRotation { <# .SYNOPSIS Handle log rotation. .DESCRIPTION Invoke-LogRotation handles log rotation, using the log parameters defined in the log object. This function is called within the Write-Log function so that log rotation are invoked after each write to the log file. .NOTES Author: CleverTwain Date: 4.8.2018 Version: 0.1.0 #> [CmdletBinding()] param ( # The log object created using the New-Log function. Defaults to reading the global PSLOG variable. [Parameter(ValueFromPipeline)] [ValidateNotNullorEmpty()] [object] $Log = $Script:PSLOG ) try { # get current size of log file $currentSize = (Get-Item $Log.Path).Length # get log name $logFileName = Split-Path $Log.Path -Leaf $logFilePath = Split-Path $Log.Path $logFileNameWithoutExtension = [System.IO.Path]::GetFileNameWithoutExtension($logFileName) $logFileNameExtension = [System.IO.Path]::GetExtension($logFileName) # if MaxLogFiles is 1 just keep the original one and let it grow if (-not($Log.MaxLogFiles -eq 1)) { if ($currentSize -ge $Log.MaxLogSize) { Write-Verbose 'We have hit the max log size' # construct name of archived log file $newLogFileName = $logFileNameWithoutExtension + (Get-Date -Format 'yyyyMMddHHmmss').ToString() + $logFileNameExtension # copy old log file to new using the archived name constructed above Copy-Item -Path $Log.Path -Destination (Join-Path (Split-Path $Log.Path) $newLogFileName) # set new empty log file if ([string]::IsNullOrEmpty($Log.Header)) { Set-Content -Path $Log.Path -Value $null -Encoding 'UTF8' -Force } else { Set-Content -Path $Log.Path -Value $Log.Header -Encoding 'UTF8' -Force } # if MaxLogFiles is 0 don't delete any old archived log files if (-not($Log.MaxLogFiles -eq 0)) { Write-Verbose 'We shouldnt need to delete old log files...' # set filter to search for archived log files $archivedLogFileFilter = $logFileNameWithoutExtension + '??????????????' + $logFileNameExtension # get archived log files $oldLogFiles = Get-Item -Path "$(Join-Path -Path $logFilePath -ChildPath $archivedLogFileFilter)" if ([bool]$oldLogFiles) { # compare found log files to MaxLogFiles parameter of the log object, and delete oldest until we are # back to the correct number if (($oldLogFiles.Count + 1) -gt $Log.MaxLogFiles) { Write-Verbose "Okay... maybe we do need to delete some old logs" [int]$numTooMany = (($oldLogFiles.Count) + 1) - $log.MaxLogFiles $oldLogFiles | Sort-Object 'LastWriteTime' | Select-Object -First $numTooMany | Remove-Item } } } } } } catch { Write-Warning $_.Exception.Message } } # .\CT.WriteLog\private\Set-ModuleVariable.ps1 Function Set-ModuleVariable { [CmdletBinding(SupportsShouldProcess, ConfirmImpact='Low')] Param( # VariableName help description [Parameter(ValueFromPipeline,Mandatory)] [string]$VariableName, [Parameter(Mandatory)] [string]$Value, # I don't know of a use case for a 'local' variable, so I didn't include it # We scope to script-level variables by default, as they will be available to functions within # the module, but they are not exposed to the user. # If there is a variable that needs to be exposed to the user, you should set the scope to global [ValidateSet("Global","Script")] [string]$Scope = 'Script', [string]$Path = "$Script:ModuleBase\lib\Variables.csv", [switch]$Force ) Begin { Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($MyInvocation.MyCommand)" $ExistingVariables = @() # If the file already exists... if ( Test-Path -Path $Path) { # Get a list of all the variables already in the CSV $ExistingVariables = Import-Csv -Path $Path } else { Write-Debug 'Variable file not found' } # Check if the variable already exists if ($ExistingVariables | Where-Object {$_.VariableName -eq $VariableName}) { Write-Debug "Updating existing variable" Write-Debug "$VariableName : $Value : $Scope" ($ExistingVariables | Where-Object {$_.VariableName -eq $VariableName}).Value = $Value ($ExistingVariables | Where-Object {$_.VariableName -eq $VariableName}).Scope = $Scope } else { Write-Debug "Creating new variable:" Write-Debug "$VariableName : $Value : $Scope" $ExistingVariables += [PSCustomObject]@{ VariableName = $VariableName Value = $Value Scope = $Scope } } Write-Verbose "Trying to export variables to $Path" Try { $ExistingVariables | ConvertTo-Csv -NoTypeInformation | Out-File -FilePath $Path -Force:$Force -ErrorAction Stop } Catch { $_ } } #begin } #close Set-ModuleVariable # .\CT.WriteLog\private\Write-MessageToHost.ps1 Function Write-MessageToHost { [cmdletbinding()] Param( # LogEntry help description [Parameter(ValueFromPipeline)] [object]$LogEntry, [Parameter()] [ValidateSet('Error', 'FailureAudit', 'Information', 'SuccessAudit', 'Warning', 'Verbose', 'Debug')] [Alias('Type')] [string] $LogType = 'Information', $NoHostWriteBack ) Begin { Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)" switch ($LogType) { 'Error' {$cmType = '3'} 'FailureAudit' {$cmType = '3'} 'Information' {$cmType = '6'} 'SuccessAudit' {$cmType = '4'} 'Warning' {$cmType = '2'} 'Verbose' {$cmType = '4'} 'Debug' {$cmType = '5'} DEFAULT {$cmType = '1'} } } #begin Process { Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] $LogEntry " switch ($cmType) { 2 { # Write the warning message back to the host $WarningPreference = $PSCmdlet.GetVariableValue('WarningPreference') Write-Warning -Message "$LogEntry" } 3 { if ($PSCmdlet.GetVariableValue('ErrorActionPreference') -ne 'SilentlyContinue' ) { $ErrorActionPreference = $PSCmdlet.GetVariableValue('ErrorActionPreference') $Host.Ui.WriteErrorLine("ERROR: $([String]$LogEntry.Exception.Message)") Write-Error $LogEntry -ErrorAction ($PSCmdlet.GetVariableValue('ErrorActionPreference')) } } 4 { # Write the verbose message back to the host $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference') Write-Verbose -Message "$LogEntry" } 5 { # Write the debug message to the Host. $DebugPreference = $PSCmdlet.GetVariableValue('DebugPreference') Write-Debug -Message "$LogEntry" } default { # Write the informational message back to the host. if ($PSVersionTable.PSVersion -gt 5.0.0.0){ $InformationPreference = $PSCmdlet.GetVariableValue('InformationPreference') Write-Information -MessageData "INFORMATION: $LogEntry" } else { # The information stream was introduced in PowerShell v5. # We have to use Write-Host in earlier versions of PowerShell. Write-Debug "We are using an older version of PowerShell. Reverting to Write-Output" Write-Output "INFORMATION: $LogEntry" } }#Information } } #process End { Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)" } #end } #close Write-LogToHost Write-Verbose 'Importing from [C:\Projects\ct-writelog\CT.WriteLog\public]' # .\CT.WriteLog\public\New-Log.ps1 function New-Log { <# .SYNOPSIS Creates a new log .DESCRIPTION The New-Log function is used to create a new log file or Windows Event log. A log object is also created and either saved in the global PSLOG variable (default) or sent to the pipeline. The latter is useful if you need to write to different log files in the same script/function. .EXAMPLE New-Log '.\myScript.log' Create a new log file called 'myScript.log' in the current folder, and save the log object in $PSLOG .EXAMPLE New-Log '.\myScript.log' -Header 'MyHeader - MyScript' -Append -CMTrace Create a new log file called 'myScript.log' if it doesn't exist already, and add a custom header to it. The log format used for logging by Write-Log is the CMTrace format. .EXAMPLE $log1 = New-Log '.\myScript_log1.log'; $log2 = New-Log '.\myScript_log2.log' Create two different logs that can be written to depending on your own internal script logic. Remember to pass the correct log object to Write-Log! .EXAMPLE New-Log -EventLogName 'PowerShell Scripts' -EventLogSource 'MyScript' Create a new log called 'PowerShell Scripts' with a source of 'MyScript', for logging to the Windows Event Log. .NOTES Author: CleverTwain Date: 4.8.2018 #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact='Medium', DefaultParameterSetName = 'PlainText')] param ( # Create or append to a Plain Text log file # Log Entry Example: # 03-22-2018 12:37:59.168-240 INFORMATION: Generic Log Entry [Parameter( ParameterSetName = 'PlainText', Position = 0 )] [switch]$PlainText, # Create or append to a Minimal log file # Log Entry Example: # Generic Log Entry [Parameter( ParameterSetName = 'Minimal', Position = 0 )] [switch]$Minimal, # Create or append to a CMTrace log file [Parameter( ParameterSetName = 'CMTrace', Position = 0 )] [switch]$CMTrace, # Create or append to a Windows Event Log [Parameter( ParameterSetName = 'EventLog', Position = 0 )] [switch]$EventLog, # Select which type of log to create or append to # The format of the log file. Valid choices are 'Minimal', 'PlainText' and 'CMTrace'. # The 'Minimal' format will just pass the log entry to the log file, while the 'PlainText' includes meta-data. # CMTrace format are viewable using the CMTrace.exe tool. # Path to log file. [Parameter( ParameterSetName = 'PlainText', Position = 1)] [ValidateNotNullorEmpty()] [Parameter( ParameterSetName = 'Minimal', Position = 1)] [ValidateNotNullorEmpty()] [Parameter( ParameterSetName = 'CMTrace', Position = 1)] [string] $Path = "$env:TEMP\$(Get-Date -Format FileDateTimeUniversal).log", # Optionally define a header to be added when a new empty log file is created. # Headers only apply to files, and do not apply to CMTrace files [Parameter( ParameterSetName = 'PlainText', Mandatory = $false, Position = 2)] [Parameter( ParameterSetName = 'Minimal', Mandatory = $false, Position = 2)] [string]$Header, # If log file already exist, append instead of creating a new empty log file. [Parameter( ParameterSetName = 'PlainText')] [Parameter( ParameterSetName = 'Minimal')] [Parameter( ParameterSetName = 'CMTrace')] [switch] $Append, # Maximum size of log file. [Parameter( ParameterSetName = 'PlainText' )] [Parameter( ParameterSetName = 'Minimal' )] [Parameter( ParameterSetName = 'CMTrace' )] [int64] $MaxLogSize = 5242880, # in bytes, default is 5242880 = 5 MB # Maximum number of log files to keep. Default is 3. Setting MaxLogFiles to 0 will keep all log files. [Parameter( ParameterSetName = 'PlainText' )] [Parameter( ParameterSetName = 'Minimal' )] [Parameter( ParameterSetName = 'CMTrace' )] [ValidateRange(0,99)] [int32] $MaxLogFiles = 3, # Specifies the name of the event log. [Parameter( ParameterSetName = 'EventLog', Position = 3)] [string] $EventLogName = 'CT.WriteLog', # Specifies the name of the event log source. [Parameter( ParameterSetName = 'EventLog')] [string] $EventLogSource, # Define the default Event ID to use when writing to the Windows Event Log. # This Event ID will be used when writing to the Windows log, but can be overrided by the Write-Log function. [Parameter( ParameterSetName = 'EventLog')] [string] $DefaultEventID = '1000', # When UseLocalVariable is True, the log object is not saved in the global PSLOG variable, # otherwise it's returned to the pipeline. [Parameter()] [switch] $UseLocalVariable, # Messages written via Write-Log are also written back to the host by default. Specifying this option, disables # that functionality for the log object. [Parameter()] [switch] $NoHostWriteBack, # When writing to the log, the data stream name (DEBUG, WARNING, VERBOSE, etc.) is not included by default. # Specifying this option will include the stream name in all Write-Log messages. [Parameter()] [switch] $IncludeStreamName ) if ($PSCmdlet.ParameterSetName -eq 'EventLog') { $LogType = 'EventLog' } else { $LogType = 'LogFile' $LogFormat = $PSCmdlet.ParameterSetName } if ($LogType -eq 'EventLog') { if (!$EventLogSource) { if ( (Get-PSCallStack)[1].FunctionName ) { $EventLogSource = (Get-PSCallStack)[1].FunctionName } else { $EventLogSource = (Get-PSCallStack)[1].Command } if ($EventLogSource = '<ScriptBlock>') { $EventLogSource = 'ScriptBlock' } } if ([System.Diagnostics.EventLog]::SourceExists($EventLogSource)) { $AssociatedLog = [System.Diagnostics.EventLog]::LogNameFromSourceName($EventLogSource,".") if ($AssociatedLog -ne $EventLogName) { Write-Warning "The eventlog source $EventLogSource is already associated with a different eventlog" $LogType = $null return $null } } try { if (-not([System.Diagnostics.EventLog]::SourceExists($EventLogSource))) { # In order to create a new event log, or add a new source to an existing eventlog, # the user must be running the command as an administrator. # We are checking for that here $windowsIdentity=[System.Security.Principal.WindowsIdentity]::GetCurrent() $windowsPrincipal=new-object System.Security.Principal.WindowsPrincipal($windowsIdentity) $adm=[System.Security.Principal.WindowsBuiltInRole]::Administrator if ($windowsPrincipal.IsInRole($adm)) { Remove-Variable -Name Format,MaxLogSize,MaxLogFiles -ErrorAction SilentlyContinue # create new event log if needed New-EventLog -Source $EventLogSource -LogName $EventLogName Write-Verbose "Created new event log (Name: $($EventLogName), Source: $($EventLogSource))" } else { Write-Warning 'When creating a Windows Event Log you need to run as a user with elevated rights!' } } else { Write-Verbose "$($EventLogName) exists, skip create new event log." } $logType = 'EventLog' } catch { Write-Warning $_.Exception.Message } } else { Remove-Variable -Name EventLogName,EventLogSource,DefaultEventID -ErrorAction SilentlyContinue $Counter = 0 $HaveLog = $false While (-not $HaveLog) { $Mutex = New-Object System.Threading.Mutex($false, "LoggingMutex") Write-Debug "Requesting mutex to test access to log" [void]$Mutex.WaitOne(1000) Write-Debug "Received Mutex to test access to log" Try { [io.file]::OpenWrite($Path).close() $HaveLog = $true } Catch [System.UnauthorizedAccessException] { $FileName = $Path.Split("\") | Select-Object -Last 1 $Path = "$env:TEMP\$FileName" Write-Warning "Current user does not have permission to write to $Path. Redirecting log to $Path" $HaveLog = $true } Catch { $Counter++ Write-Debug $_ } Finally { Write-Debug "Releasing Mutex to access log" [void]$Mutex.ReleaseMutex() } if ($Counter -gt 99) { $HaveLog = $false Write-Error "Unable to obtain lock on file" $logType = $null return $null } } # create new log file if needed ( we need to re-check if the file exists here because the # path may have changed since we last checked) if((-not $Append) -or (-not(Test-Path $Path))){ Write-Verbose "Log does not currently exist, or we are overwriting an existing log" try { if($Header){ Set-Content -Path $Path -Value $Header -Encoding 'UTF8' -Force } else{ Set-Content -Path $Path -Value $null -Encoding 'UTF8' -Force } Write-Verbose "Created new log file ($($Path))" } catch{ Write-Warning $_.Exception.Message } } } Write-Verbose "Creating Log Object" # create log object switch ($LogType) { 'EventLog' { # create log object $logObject = [PSCustomObject]@{ PSTypeName = 'CT.EventLog' Type = $logType Name = $EventLogName Source = $EventLogSource DefaultEventID = $DefaultEventID IncludeStreamName = $IncludeStreamName HostWriteBack = (!$NoHostWriteBack) MaxLogSize = $MaxLogSize # Limit-EventLog # Minimum 64KB Maximum 4GB and must be divisible by 64KB (65536) MaxLogRetention = $MaxLogRetention # Limit-EventLog # RetentionDays OverflowAction = $OverflowAction # Limit-EventLog # OverwriteOlder, OverwriteAsNeeded, DoNotOverwrite } } 'LogFile' { $logObject = [PSCustomObject]@{ PSTypeName = 'CT.LogFile' Type = $logType Path = $Path Format = $LogFormat Header = $Header IncludeStreamName = $IncludeStreamName HostWriteBack = (!$NoHostWriteBack) MaxLogSize = $MaxLogSize MaxLogFiles = $MaxLogFiles } } default {$logObject = $null} } # Return the log to the pipeline if ($UseLocalVariable) { Write-Output $logObject } else { if (Get-Variable PSLog -ErrorAction SilentlyContinue) { Remove-Variable -Name PSLOG } New-Variable -Name PSLOG -Value $logObject -Scope Script } } # .\CT.WriteLog\public\Show-Log.ps1 Function Show-Log { <# .SYNOPSIS Shows a log .DESCRIPTION The Show-Log function is used to display a log file, event log, or log object. .EXAMPLE Show-Log '.\myScript.log' Create a new log file called 'myScript.log' in the current folder, and save the log object in $PSLOG .EXAMPLE New-Log '.\myScript.log' -Header 'MyHeader - MyScript' -Append -CMTrace Create a new log file called 'myScript.log' if it doesn't exist already, and add a custom header to it. The log format used for logging by Write-Log is the CMTrace format. .EXAMPLE $log1 = New-Log '.\myScript_log1.log'; $log2 = New-Log '.\myScript_log2.log' Create two different logs that can be written to depending on your own internal script logic. Remember to pass the correct log object to Write-Log! .EXAMPLE New-Log -EventLogName 'PowerShell Scripts' -EventLogSource 'MyScript' Create a new log called 'PowerShell Scripts' with a source of 'MyScript', for logging to the Windows Event Log. .NOTES Author: CleverTwain Date: 4.8.2018 #> [cmdletbinding()] Param( # Log object to show [Parameter(ValueFromPipeline)] [object]$LogObject = $Script:PSLOG, # Event log to show [string]$EventLog, # File log to show [string]$Path ) Begin { Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)" } #begin Process { Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] $LogObject " if ($Path -or $LogObject.Type -eq 'LogFile') { Write-Verbose "Working with a logfile" if ($LogObject.Path) { $Path = $LogObject.Path } Write-Verbose "Trying to invoke-item $Path" Invoke-Item $Path } if ($EventLog -or $LogObject.Type -eq 'EventLog') { Write-Verbose "Working with event log" if ($LogObject.Name) { $EventLog = $LogObject.Name } Write-Verbose "Trying to EventVwr.exe /c:'$EventLog'" EventVwr.exe /c:"$EventLog" } # If the log is a file, invoke-item on it. That should just open the file with # the users preferred log viewer <# You can get the properties of the event log file itself by typing WevtUtil.exe Get-Log "$EventLogName" /format:xml Here is how you can get the location [xml]$EventLog = WevtUtil.exe Get-Log "$EventLogName" /format:xml $LogPath = $EventLog.Channel.Logging.LogFileName --- The log can be pulled up directly by running: EventVwr.exe /c:"$EventLogName" (Except on my machine, because it is a piece of crap!) #> } #process End { Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)" } #end } #close Show-Log # .\CT.WriteLog\public\Write-Log.ps1 Function Write-Log { <# .SYNOPSIS Write to the log and back to the host by default. .DESCRIPTION The Write-Log function is used to write to the log and by default, also write back to the host. It is using the log object created by New-AHGLog to determine if it's going to write to a log file or to a Windows Event log. Log files can be created in multiple formats .EXAMPLE Write-AHGLog 'Finished running WMI query' Get the log object from $PSLOG and write to the log. .EXAMPLE $myLog | Write-AHGLog 'Finished running WMI query' Use the log object saved in $myLog and write to the log. .EXAMPLE Write-AHGLog 'WMI query failed - Access denied!' -LogType Error -PassThru | Write-Warning Will write an error to the event log, and then pass the log entry to the Write-Warning cmdlet. .NOTES Author: CleverTwain Date: 4.8.2018 Dependencies: Invoke-AHGLogRotation #> [cmdletbinding()] param ( # The text you want to write to the log. # Not limiting this to a string, as this function can catch exceptions as well [Parameter(Position = 0)] [Alias('Message')] $LogEntry, # The type of log entry. Valid choices are 'Error', 'FailureAudit','Information','SuccessAudit' and 'Warning'. # Note that the CMTrace format only supports 3 log types (1-3), so 'Error' and 'FailureAudit' are translated to CMTrace log type 3, 'Information' and 'SuccessAudit' # are translated to 1, while 'Warning' is translated to 2. 'FailureAudit' and 'SuccessAudit' are only really included since they are valid log types when # writing to the Windows Event Log. [Parameter()] [ValidateSet('Error', 'FailureAudit', 'Information', 'SuccessAudit', 'Warning', 'Verbose', 'Debug')] [Alias('Type')] [string] $LogType = 'Information', # Include the stream name in the log entry when writing CMTrace logs [Parameter()] [switch]$IncludeStreamName, # Do not write the message back to the host via the specified stream # By default, log entries are written back to the host via the specified data stream. [Parameter()] [switch]$NoHostWriteBack, # Event ID. Only applicable when writing to the Windows Event Log. [Parameter()] [string] $EventID, # The log object created using the New-Log function. Defaults to reading the PSLOG variable. [Parameter(ValueFromPipeline)] [ValidateNotNullorEmpty()] [Alias('LogFile')] [object] $Log = $Script:PSLOG, # PassThru passes the log entry to the pipeline for further processing. [Parameter()] [switch] $PassThru ) Begin { Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)" if ( (!$Log.HostWriteBack) -or ($NoHostWriteBack)) { $NoHostWriteBack = $true } if ( ($Log.IncludeStreamName) -or ($IncludeStreamName)) { $IncludeStreamName = $true } # An attribute of the Log object will be flagged if we do not have appropriate permissions. # If that attribute is flagged, we should still write to the screen if appropriate } #begin Process { Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] $LogEntry " try { # get information from log object $logObject = $Log Write-Verbose "Received the log object of type $($logObject.Type)" if ($logObject.Format) { Write-Debug "LogFormat: $($LogObject.Format)" } # translate event types to CMTrace types, and gather information for error if ($logObject.Format -eq 'CMTrace' -or $logObject.Type -eq 'EventLog') { switch ($LogType) { 'Error' { $cmType = '3' #Get the info about the calling script, function etc $CallingInfo = (Get-PSCallStack)[1] if (!$LogEntry.Exception.Message) { [System.Exception]$Exception = $LogEntry [String]$ErrorID = 'Custom Error' [System.Management.Automation.ErrorCategory]$ErrorCategory = [Management.Automation.ErrorCategory]::WriteError $ErrorRecord = New-Object Management.automation.errorrecord ($Exception, $ErrorID, $ErrorCategory, $LogEntry) $LogEntry = $ErrorRecord $LogEntry = "$([String]$LogEntry.Exception.Message)`r`r`n" + "`nFunction: $($Callinginfo.FunctionName)" + "`nScriptName: $($Callinginfo.Scriptname)" + "`nLine Number: $($Callinginfo.ScriptLineNumber)" + "`nColumn Number: $($Callinginfo.Position.StartColumnNumber)" + "`nLine: $($Callinginfo.Position.StartScriptPosition.Line)" } else { $LogEntry = "$([String]$LogEntry.Exception.Message)`r`r`n" + "`nCommand: $($LogEntry.InvocationInfo.MyCommand)" + "`nScriptName: $($LogEntry.InvocationInfo.Scriptname)" + "`nLine Number: $($LogEntry.InvocationInfo.ScriptLineNumber)" + "`nColumn Number: $($LogEntry.InvocationInfo.OffsetInLine)" + "`nLine: $($LogEntry.InvocationInfo.Line)" } } 'FailureAudit' {$cmType = '3'} 'Information' {$cmType = '6'} 'SuccessAudit' {$cmType = '4'} 'Warning' {$cmType = '2'} 'Verbose' {$cmType = '4'} 'Debug' {$cmType = '5'} DEFAULT {$cmType = '1'} } Write-Debug "$LogType : $cmType" } if ($logObject.Type -eq 'EventLog') { # if EventID is not specified use default event id from the log object if([system.string]::IsNullOrEmpty($EventID)) { $EventID = $logObject.DefaultEventID } if ($LogType -notin ('Error','FailureAudit','SuccessAudit','Warning')) { $LogType = 'Information' } $LogEntryString = $LogEntry Write-Verbose "LogEntryString: $LogEntryString" Write-Verbose "lo.name: $($logObject.Name)" Write-Verbose "lo.Source: $($logObject.Source)" Write-Verbose "EntryType: $($LogType)" Write-Verbose "EventId: $($EventID)" Write-Verbose 'Trying to write to the event log' Write-EventLog -LogName $logObject.Name -Source $logObject.Source -EntryType $LogType -EventId $EventID -Message $LogEntryString -Verbose } else { $DateTime = New-Object -ComObject WbemScripting.SWbemDateTime $DateTime.SetVarDate($(Get-Date)) $UtcValue = $DateTime.Value $UtcOffset = $UtcValue.Substring(21, $UtcValue.Length - 21) $Date = Get-Date -Format 'MM-dd-yyyy' $Time = "$(Get-Date -Format HH:mm:ss.fff)$($UtcOffset)" # handle the different log file formats switch ($logObject.Format) { 'Minimal' { $logEntryString = $LogEntry} 'PlainText' { $logEntryString = "$Date $Time" if ($IncludeStreamName) { $LogEntryString = "$logEntryString $($LogType.ToUpper()):" } $LogEntryString = "$LogEntryString $($LogEntry)" } 'CMTrace' { # Get invocation information about the script/function/module that called us $thisInvocation = (Get-Variable -Name 'MyInvocation' -Scope 2).Value # get calling script info if(-not ($thisInvocation.ScriptName)){ $scriptName = $thisInvocation.MyCommand $Source = "$($scriptName)" } else{ $scriptName = Split-Path -Leaf ($thisInvocation.ScriptName) $Source = "$($scriptName):$($thisInvocation.ScriptLineNumber)" } # get calling command info $component = "$($thisInvocation.MyCommand)" $Context = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name $Source = (Get-PSCallStack)[1].Location if ( (Get-PSCallStack)[1].FunctionName ) { $Component = (Get-PSCallStack)[1].FunctionName } else { $Component = (Get-PSCallStack)[1].Command } #Set Component Information if ($Source -eq '<No file>') { $Source = (Get-Process -Id $PID).ProcessName } $Line = '<![LOG[{0}]LOG]!><time="{1}" date="{2}" component="{3}" context="{4}" type="{5}" thread="{6}" file="{7}">' $LineFormat = $LogEntry, $Time, $Date, $Component, $Context, $CMType, $PID, $Source $logEntryString = $Line -f $LineFormat Write-Debug "$logEntryString" } } $PendingWrite = $true $Counter = 0 $MaxLoops = 100 <# While ($PendingWrite -and ($Counter -lt $MaxLoops)) { $Counter++ try { # create a mutex, so we can lock the file while writing to it $mutex = New-Object System.Threading.Mutex($false, 'LogMutex') # write to the log file Add-Content -Path $logObject.Path -Value $logEntryString -ErrorAction Stop $PendingWrite = $false $mutex.ReleaseMutex() } catch { [void]$mutex.WaitOne() } finally { if ($Counter -eq $MaxLoops) { Write-Warning "Unable to gain lock on file at $($logObject.Path)" } } } #> While ($PendingWrite -and ($Counter -lt $MaxLoops)) { $Mutex = New-Object System.Threading.Mutex($false, "LoggingMutex") Write-Debug "Requesting mutex to write to log" [void]$Mutex.WaitOne(1000) Write-Debug "Received Mutex to write to log" Try { Add-Content -Path $logObject.Path -Value $logEntryString -ErrorAction Stop $PendingWrite = $false } Catch { $Counter++ Write-Debug $_ } Finally { Write-Debug "Releasing Mutex to access log" [void]$Mutex.ReleaseMutex() } } # invoke log rotation if log is file if ($logObject.LogType -eq 'LogFile') { $logObject | Invoke-LogRotation } } } catch { Write-Warning $_.Exception.Message } } #process End { Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)" if (!$NoHostWriteBack) { Write-Verbose "Writing message back to host" Write-MessageToHost -LogEntry $LogEntryString -LogType $LogType } # handle PassThru if ($PassThru) { Write-Output $LogEntry } } #end } #close Write-Log Write-Verbose 'Importing from [C:\Projects\ct-writelog\CT.WriteLog\classes]' |