SimplePSLogger.psm1
class SimplePSLogger : System.IDisposable { # Create with providers SimplePSLogger([string]$Name, [LoggingProvider[]]$LoggingProviders) { $this.Name = $Name $this.AddLoggingProviders($LoggingProviders) Write-Information "Logging providers registered - $($this.LoggingProviders.Count)" -InformationAction Continue Write-Information "$([Environment]::NewLine)----------------- SimpleLogger instance initialized with name '$Name' ---------------------- $([Environment]::NewLine)" -InformationAction Continue } <#------------------------------- Members Variables ----------------------------------#> hidden [string] $Name = $null hidden [System.Collections.ArrayList] $LoggingProviders = [System.Collections.ArrayList]@() hidden [string] $DefaultLogLevel = "information" hidden [hashtable] $LogLevels = [ordered]@{"verbose" = 0; "debug" = 1; "information" = 2; "warning" = 3; "error" = 4; "critical" = 5; "none" = 6 } <#------------------------------- Members Variables ----------------------------------#> hidden static [object] CreateLogger($Name, [LoggingProvider[]]$LoggingProviders) { if (-Not $Name) { throw "Cannot create logger without 'name'" } return [SimplePSLogger]::new($Name, $LoggingProviders) } hidden static [string] GenerateLoggerName() { # https://devblogs.microsoft.com/scripting/powertip-create-a-new-guid-by-using-powershell/ return [Guid]::NewGuid().ToString() } hidden [string] GetTimestamp() { return (Get-Date).ToUniversalTime().ToString("yyyy/MM/dd HH:mm:ss:ffff tt") } hidden [string] GetLogLevel($level) { # TODO : optimize it later if (-Not $this.LogLevels[$level]) { Write-Information "Log level '$level' not supported, defaulting to '$this.DefaultLogLevel'" -InformationAction Continue $level = $this.DefaultLogLevel } return $level } <# This method checks configured loglevel(for provider) against user provided loglevel in .Log() method #> hidden [Boolean] ShouldLog($ConfugeredLevel, $Level) { # provided level should be greater or equal to configured value. return $this.LogLevels[$ConfugeredLevel] -le $this.LogLevels[$Level] } hidden [void] AddLoggingProviders([LoggingProvider[]]$loggers) { foreach ($logger in $loggers) { if ($logger.GetType() -ne [LoggingProvider]) { throw [InvalidCastException]::new() } $this.LoggingProviders.Add($logger) } } # TODO : need more information. [void] Dispose() { Write-Warning "$($this.Name) SimplePSLogger disposed" -WarningAction Continue } <#------------------------------- /Internals ---------------------------------------#> <#------------------------------- Public methods ---------------------------------------#> [void] Log( [string]$Level, [object]$Message ) { $Level = $this.GetLogLevel($Level) if (-Not $($Message.GetType() -eq 'String')) { $Message = $Message | ConvertTo-Json -Compress -Depth 100 } if ($this.LoggingProviders.Count -le 0) { Write-Information "Zero logging providers registered." -InformationAction Continue } foreach ($logger in $this.LoggingProviders) { Write-Verbose "Writing using logger - $($logger.Name)" if (-Not $logger.Config) { try { if ($this.ShouldLog($this.DefaultLogLevel, $Level) -and $logger.Config["Enabled"]) { Invoke-Command $logger.Function -ArgumentList $this.Name, $Level, $Message -ErrorAction Continue } } catch { Write-Warning "---------------------- Attention : error occurred while writing log------------------" -WarningAction Continue Write-Warning "$_" -WarningAction Continue Write-Warning "---------------------- /Attention ------------------" -WarningAction Continue } finally { # TODo : need help } } else { try { $UserProvidedLogLevel = if (-Not $logger.Config["LogLevel"]) { $this.DefaultLogLevel }else { $logger.Config["LogLevel"] } if ($this.ShouldLog($UserProvidedLogLevel, $Level) -and $logger.Config["Enabled"]) { Invoke-Command $logger.Function -ArgumentList ($this.Name), $Level, $Message, ($logger.Config) -ErrorAction Continue } } catch { Write-Warning "---------------------- Attention : error occurred while writing log------------------" -WarningAction Continue Write-Warning "$_" -WarningAction Continue Write-Warning "---------------------- /Attention ------------------" -WarningAction Continue } finally { #TODO : need help } } } } [void] Log( [object]$Message ) { # Default loglevel $Level = $this.DefaultLogLevel $this.Log($Level, $Message) } <# .SYNOPSIS Register your custom logging provider .DESCRIPTION Use this method of [SimplePSLogger] to register your custom logging provider. Make sure that your custom logging script/module/function is available before registering. .PARAMETER Name Name of your logging provider .PARAMETER ProviderFunctionName Powershell function name of your provider .PARAMETER Configuration Configuratoin object needed for your logging provider Example configuration object : $config = @{ Enabled = $true LogLevel = "information" Authkey = "key" } .EXAMPLE $myLogger = New-SimplePSLogger -Name "mylogger" $myLogger.RegisterProvider("myAwesomeProvider", "New-MyAwesomeProvider-Logger", $config) .NOTES Make sure that "New-MyAwesomeProvider-Logger" function and it's dependencies are imported/initialized before registering your provider #> [void] RegisterProvider($Name, $ProviderFunctionName, $Configuration) { try { Write-Information "$([Environment]::NewLine)----------------- SimpleLogger - Registering Custom Provider ---------------------- $([Environment]::NewLine)" -InformationAction Continue if (-Not $Name) { throw "Provider name is required" } if (-Not $ProviderFunctionName) { throw "Provider function name is required" } Get-Command -Name $ProviderFunctionName -ErrorAction Stop $ProviderFunctionCode = [scriptblock]::Create($(Get-Command -Name $ProviderFunctionName).Definition) $ProviderLogger = [LoggingProvider]::Create($Name, $ProviderFunctionCode, $Configuration) $this.AddLoggingProviders($ProviderLogger) Write-Information "$([Environment]::NewLine)----------------- SimpleLogger - Registering Custom Provider ---------------------- $([Environment]::NewLine)" -InformationAction Continue } catch [System.Management.Automation.CommandNotFoundException] { throw "Provider module/function is recognized as name of cmdlet, please make sure that your provider is available" } catch { throw $_.Exception.Message } } <# Processes remaining logs #> [void] Flush() { Write-Information "Flushing buffered logs of providers those support buffer." -InformationAction Continue foreach ($logger in $this.LoggingProviders) { if ($logger.Config -and $logger.Config["Flush"]) { try { Invoke-Command $logger.Function -ArgumentList ($this.Name), "information", "flush logs", $logger.Config, $true -ErrorAction Continue } catch { Write-Warning "---------------------- Attention : error occurred while flushing logs of $($logger.Name) ------------------" -WarningAction Continue Write-Warning "$_" -WarningAction Continue Write-Warning "---------------------- /Attention ------------------" -WarningAction Continue } finally { #TODO : need help } } } } <#------------------------------- /Public methods : everything is public tho :P ---------------------------------------#> } class LoggingProvider { hidden LoggingProvider($name, $function, $config) { Write-Information "Registering '$name' provider" -InformationAction Continue if (-Not $name) { throw "Logging provider name is required" } if (-Not $function) { throw "Logging provider function is required" } #TODO : validate function type if ($($function.GetType()) -ne [scriptblock]) { throw "Logging provider's function should be ScriptBlock" } if (-Not $config) { Write-Information "Configurations not provided for '$name' provider or it does not need any configurations." -InformationAction Continue } $this.Config = $config $this.Name = $name $this.Function = $function Write-Information "Registered '$name' provider" -InformationAction Continue } hidden static [LoggingProvider] Create($name, $function, $config) { return [LoggingProvider]::new($name, $function, $config) } [string] $Name [object] $Function [object] $Config } <# .SYNOPSIS Create new SimplePSLogger instance .DESCRIPTION You can create multiple loggers for one action but we recommend creating one single logger for your action SimplePSLogger logger automatically registers x loggers. Custom logging provider support will be added soon. .PARAMETER Name SimplePSLogger name which is used to identify current logger instnace Examples : your script name, unique task name etc. It will help you to analyze logs .PARAMETER Configuration SimplePSLogger configuratoin object, this will contain configurations for supported/reistred providers Configuration for each provider should be defined by creating new section/key with it's name Example configuration object : $SimplePSLoggerConfig = @{ Name = "config-example" Providers = @{ File = @{ LiteralFilePath = "G:\Git\simple-ps-logger\Examples\example-with-config-file\example-with-config.log" } } } .EXAMPLE $myLogger = New-SimplePSLogger -Name "action-1234" $myLogger.Log('level', 'log message') $myLogger.Dispose() .NOTES #> function New-SimplePSLogger { param ( [Parameter(Mandatory = $false, HelpMessage = "Unique/Identifiable name for your logger instance")] [string] $Name, [Parameter(Mandatory = $false, HelpMessage = "Config object")] [object] <#TODO : Add support to read form file path, First class support for object seems resonable because user can add secrets by without storing those in config file #> $Configuration ) try { if (-Not $Name) { if ($null -ne $Configuration) { if (-Not $Configuration["Name"]) { Write-Information "SimplePSLogger name not provided, initializing instance with auto generated name : '$Name'" -InformationAction Continue $Name = [SimplePSLogger]::GenerateLoggerName() } $Name = $Configuration["Name"] } else { $Name = [guid]::NewGuid() } } Write-Information "----------------- Initializing SimpleLogger instance with name '$Name' ----------------------`n" -InformationAction Continue <# NOTE : We can merge User provided with default configurations but merging deep hastables is complicated and expensive. #> if (-Not $Configuration) { #TODO : Create and read default config file. Write-Warning "Configuration not provided, all providers will get configured without configurations(Not recommended)" -WarningAction Continue $Configuration = @{ Providers = @{ Console = @{ Enabled = $true LogLevel = "information" } } } } else { # TODO : will it be helpful if we enable Console provider forcefully? if ($Configuration.Providers["Console"]) { $Configuration.Providers.Console["Enabled"] = $true } else { $Configuration.Providers["Console"] = @{ Enabled = $true } } } $NestedProviders = $((Get-Module SimplePSLogger).NestedModules) if ($NestedProviders.Count -le 0) { Write-Error "Zero logging provider registred, there is no use of SimplePSLogger with logging providers." -ErrorAction Continue } $Loggers = [System.Collections.ArrayList]@() foreach ($provider in $NestedProviders) { $ProviderSectionName = $($provider.Name -replace "SimplePSLogger.", "") $ProviderSectionConfig = $Configuration.Providers[$ProviderSectionName] # IsEnabled? if ($ProviderSectionConfig -and $ProviderSectionConfig["Enabled"]) { $ProviderFunctionName = "New-$ProviderSectionName-Logger" if (-Not $($provider.ExportedCommands.Keys.Contains($ProviderFunctionName))) { Write-Information "$($provider.Name) does not have module member exposed in 'New-YuorLoogerName-Logger' format" -InformationAction Continue } else { $ProviderFunctionCode = [scriptblock]::Create($(Get-Command $ProviderFunctionName).Definition) $ProviderLogger = [LoggingProvider]::Create($ProviderSectionName, $ProviderFunctionCode, $ProviderSectionConfig) # .Add method retuns int32 in pipeline $added = $Loggers.Add($ProviderLogger) } } } $logger = [SimplePSLogger]::CreateLogger($Name, $Loggers) $InformationPreference = $PrevInfoPreference return $logger } catch { throw } } Export-ModuleMember -Function New-SimplePSLogger # SIG # Begin signature block # MIIYYQYJKoZIhvcNAQcCoIIYUjCCGE4CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUfllQX4rGWwtNoj+gNE9qUbzV # IaGgghKzMIIFcDCCA1igAwIBAgIQZDtcNubuNpVEfXOVHbciTzANBgkqhkiG9w0B # AQsFADBQMSUwIwYDVQQDDBxHYW5lc2ggUmFza2FyIFNpbXBsZVBTTG9nZ2VyMScw # JQYJKoZIhvcNAQkBFhhnYW5lc2hyYXNrYXJAb3V0bG9vay5jb20wHhcNMjAwNzA0 # MTgzMjE0WhcNMjEwNzA0MTg1MjE0WjBQMSUwIwYDVQQDDBxHYW5lc2ggUmFza2Fy # IFNpbXBsZVBTTG9nZ2VyMScwJQYJKoZIhvcNAQkBFhhnYW5lc2hyYXNrYXJAb3V0 # bG9vay5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCywhH9ot8P # 9i7rClMbjRwIH1DeVotpNCscFvcSqEbMo3AtJ0WFOb13Xt/g1d0axBa3DvriedVR # KF2AD0U8jGIsToPBnIdBKVXQ72CN+i1BHrqu9DOI7+csE0hVFAqYCv7qIXgAGued # FQOe1oD5q3H3GlzLDfwhk/cdLCt+WEgzWdtTca8kirNoRftSNrl7R6MEEZIRY9d4 # /J1RhjKoDfyWn2+8KcAtAkLOND5yjRPzY+jM2IEe4uELQbzbdYASmJYZq6h8Dy/6 # w/A3LRpi06kzceTao8xyUax3K11Sw/QkljLr2BKpwO8JNMMUTLNx5mAZBRyfvFvG # dZDjnnIrK2vr/qBB+cGb5S7son0N90Xc0yXyDWqAEQqMT4C0jX8FDGv//ZafbsHj # wSEV//L8E6GBP76QxfX+TTZIik0wy52Gf79ok1v3s7PgE2SyaE+iY0klYzo0bBKK # B2MPTQ2dmRy9peTzeC+KP7DWxm/9fF7OjsXY2FdWnIQL/qW6YEN5g+c+mTV/8OqB # w3xiK5WUHsd39GAGKNaHfKdUQ0nPrzDdsxGlwBlqmX+kPj7b+hZR+NBiwQLyPEk+ # qK3YKNF182GLjUj0uH5FsGrT6d+UteyB2OibxH3I2rN/KKHqQFKbC8Lbw4SbY154 # pLDc5Tfn+4GcHbpnN8pjEIsOF6Efa3/yxQIDAQABo0YwRDAOBgNVHQ8BAf8EBAMC # B4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHQYDVR0OBBYEFJuHGRIu8rLatkA/b+kP # R1QXu0RlMA0GCSqGSIb3DQEBCwUAA4ICAQAjr84JfMOG6r3jzwLl6z0nKmL5faE+ # 02U2QIcOHOlHnXOtMBmLm4IX91gOG93rBCB94GRVOUFMhAx1xkLa6moHIimfyaEU # 2UoLKVNtqrMymq6ZlQ/Yg8tPMEZfDN9wq/3EewJu+OvQvkMPRuC4LTEZs2GIHQbJ # urAzp7YT7agchAo/lm65Alg4e8QNALPcx3hkwuZtYs/FI6PEqlzpFF5RFy8Prys8 # gO4yAR3BHrz5Ri5wj9QD54kLdCvw50jJwWu+d4no2kMcKa90zjie4RBS7v4LwLdS # Yg1RMKT9yE53ch7lef/0Oj/m09mnIp+KVhtb0pe3ZOCDVvl7MCq7JWj/IGa1SMOP # 1BPxaP4FBr3vB6eE4WVyOETtT3heJScETmMXlDl7MQgc8KXz2B6iOPYMkblt3d7W # jtwIRrkujL468qzmxrlPTcEPKLqpfHaYGD3lHfwQm59KBEAqiqf/Rrya3uQmkG+a # pNoG9sWRF/M0BsOnRj31gpJqysvrHRlIf2XQebQqWY3ztVDsht+GQ8kEZnDic6j7 # oP6eSdjVG/Itv7w8ZHbqeRbXmE5mbzw1kTl3ZCPsPgmIU5r/PMcsH6H+aWJNwann # 5f+MZfM1Yozmot2z8Z65AnWB+0EfLv97Y70s1TWGaUsFk1e36f2sh/bLlSIFjcaG # 8DViFGT0BzLX4jCCBmowggVSoAMCAQICEAMBmgI6/1ixa9bV6uYX8GYwDQYJKoZI # hvcNAQEFBQAwYjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ # MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgQXNz # dXJlZCBJRCBDQS0xMB4XDTE0MTAyMjAwMDAwMFoXDTI0MTAyMjAwMDAwMFowRzEL # MAkGA1UEBhMCVVMxETAPBgNVBAoTCERpZ2lDZXJ0MSUwIwYDVQQDExxEaWdpQ2Vy # dCBUaW1lc3RhbXAgUmVzcG9uZGVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB # CgKCAQEAo2Rd/Hyz4II14OD2xirmSXU7zG7gU6mfH2RZ5nxrf2uMnVX4kuOe1Vpj # WwJJUNmDzm9m7t3LhelfpfnUh3SIRDsZyeX1kZ/GFDmsJOqoSyyRicxeKPRktlC3 # 9RKzc5YKZ6O+YZ+u8/0SeHUOplsU/UUjjoZEVX0YhgWMVYd5SEb3yg6Np95OX+Ko # ti1ZAmGIYXIYaLm4fO7m5zQvMXeBMB+7NgGN7yfj95rwTDFkjePr+hmHqH7P7IwM # Nlt6wXq4eMfJBi5GEMiN6ARg27xzdPpO2P6qQPGyznBGg+naQKFZOtkVCVeZVjCT # 88lhzNAIzGvsYkKRrALA76TwiRGPdwIDAQABo4IDNTCCAzEwDgYDVR0PAQH/BAQD # AgeAMAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwggG/BgNV # HSAEggG2MIIBsjCCAaEGCWCGSAGG/WwHATCCAZIwKAYIKwYBBQUHAgEWHGh0dHBz # Oi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwggFkBggrBgEFBQcCAjCCAVYeggFSAEEA # bgB5ACAAdQBzAGUAIABvAGYAIAB0AGgAaQBzACAAQwBlAHIAdABpAGYAaQBjAGEA # dABlACAAYwBvAG4AcwB0AGkAdAB1AHQAZQBzACAAYQBjAGMAZQBwAHQAYQBuAGMA # ZQAgAG8AZgAgAHQAaABlACAARABpAGcAaQBDAGUAcgB0ACAAQwBQAC8AQwBQAFMA # IABhAG4AZAAgAHQAaABlACAAUgBlAGwAeQBpAG4AZwAgAFAAYQByAHQAeQAgAEEA # ZwByAGUAZQBtAGUAbgB0ACAAdwBoAGkAYwBoACAAbABpAG0AaQB0ACAAbABpAGEA # YgBpAGwAaQB0AHkAIABhAG4AZAAgAGEAcgBlACAAaQBuAGMAbwByAHAAbwByAGEA # dABlAGQAIABoAGUAcgBlAGkAbgAgAGIAeQAgAHIAZQBmAGUAcgBlAG4AYwBlAC4w # CwYJYIZIAYb9bAMVMB8GA1UdIwQYMBaAFBUAEisTmLKZB+0e36K+Vw0rZwLNMB0G # A1UdDgQWBBRhWk0ktkkynUoqeRqDS/QeicHKfTB9BgNVHR8EdjB0MDigNqA0hjJo # dHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURDQS0xLmNy # bDA4oDagNIYyaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJl # ZElEQ0EtMS5jcmwwdwYIKwYBBQUHAQEEazBpMCQGCCsGAQUFBzABhhhodHRwOi8v # b2NzcC5kaWdpY2VydC5jb20wQQYIKwYBBQUHMAKGNWh0dHA6Ly9jYWNlcnRzLmRp # Z2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRENBLTEuY3J0MA0GCSqGSIb3DQEB # BQUAA4IBAQCdJX4bM02yJoFcm4bOIyAPgIfliP//sdRqLDHtOhcZcRfNqRu8WhY5 # AJ3jbITkWkD73gYBjDf6m7GdJH7+IKRXrVu3mrBgJuppVyFdNC8fcbCDlBkFazWQ # EKB7l8f2P+fiEUGmvWLZ8Cc9OB0obzpSCfDscGLTYkuw4HOmksDTjjHYL+NtFxMG # 7uQDthSr849Dp3GdId0UyhVdkkHa+Q+B0Zl0DSbEDn8btfWg8cZ3BigV6diT5VUW # 8LsKqxzbXEgnZsijiwoc5ZXarsQuWaBh3drzbaJh6YoLbewSGL33VVRAA5Ira8JR # wgpIr7DUbuD0FAo6G+OPPcqvao173NhEMIIGzTCCBbWgAwIBAgIQBv35A5YDreoA # Cus/J7u6GzANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMM # RGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQD # ExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcN # MjExMTEwMDAwMDAwWjBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQg # SW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2Vy # dCBBc3N1cmVkIElEIENBLTEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQDogi2Z+crCQpWlgHNAcNKeVlRcqcTSQQaPyTP8TUWRXIGf7Syc+BZZ3561JBXC # mLm0d0ncicQK2q/LXmvtrbBxMevPOkAMRk2T7It6NggDqww0/hhJgv7HxzFIgHwe # og+SDlDJxofrNj/YMMP/pvf7os1vcyP+rFYFkPAyIRaJxnCI+QWXfaPHQ90C6Ds9 # 7bFBo+0/vtuVSMTuHrPyvAwrmdDGXRJCgeGDboJzPyZLFJCuWWYKxI2+0s4Grq2E # b0iEm09AufFM8q+Y+/bOQF1c9qjxL6/siSLyaxhlscFzrdfx2M8eCnRcQrhofrfV # dwonVnwPYqQ/MhRglf0HBKIJAgMBAAGjggN6MIIDdjAOBgNVHQ8BAf8EBAMCAYYw # OwYDVR0lBDQwMgYIKwYBBQUHAwEGCCsGAQUFBwMCBggrBgEFBQcDAwYIKwYBBQUH # AwQGCCsGAQUFBwMIMIIB0gYDVR0gBIIByTCCAcUwggG0BgpghkgBhv1sAAEEMIIB # pDA6BggrBgEFBQcCARYuaHR0cDovL3d3dy5kaWdpY2VydC5jb20vc3NsLWNwcy1y # ZXBvc2l0b3J5Lmh0bTCCAWQGCCsGAQUFBwICMIIBVh6CAVIAQQBuAHkAIAB1AHMA # ZQAgAG8AZgAgAHQAaABpAHMAIABDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIABjAG8A # bgBzAHQAaQB0AHUAdABlAHMAIABhAGMAYwBlAHAAdABhAG4AYwBlACAAbwBmACAA # dABoAGUAIABEAGkAZwBpAEMAZQByAHQAIABDAFAALwBDAFAAUwAgAGEAbgBkACAA # dABoAGUAIABSAGUAbAB5AGkAbgBnACAAUABhAHIAdAB5ACAAQQBnAHIAZQBlAG0A # ZQBuAHQAIAB3AGgAaQBjAGgAIABsAGkAbQBpAHQAIABsAGkAYQBiAGkAbABpAHQA # eQAgAGEAbgBkACAAYQByAGUAIABpAG4AYwBvAHIAcABvAHIAYQB0AGUAZAAgAGgA # ZQByAGUAaQBuACAAYgB5ACAAcgBlAGYAZQByAGUAbgBjAGUALjALBglghkgBhv1s # AxUwEgYDVR0TAQH/BAgwBgEB/wIBADB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUH # MAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDov # L2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNy # dDCBgQYDVR0fBHoweDA6oDigNoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0Rp # Z2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDA6oDigNoY0aHR0cDovL2NybDQuZGln # aWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDAdBgNVHQ4EFgQU # FQASKxOYspkH7R7for5XDStnAs0wHwYDVR0jBBgwFoAUReuir/SSy4IxLVGLp6ch # nfNtyA8wDQYJKoZIhvcNAQEFBQADggEBAEZQPsm3KCSnOB22WymvUs9S6TFHq1Zc # e9UNC0Gz7+x1H3Q48rJcYaKclcNQ5IK5I9G6OoZyrTh4rHVdFxc0ckeFlFbR67s2 # hHfMJKXzBBlVqefj56tizfuLLZDCwNK1lL1eT7EF0g49GqkUW6aGMWKoqDPkmzmn # xPXOHXh2lCVz5Cqrz5x2S+1fwksW5EtwTACJHvzFebxMElf+X+EevAJdqP77BzhP # DcZdkbkPZ0XN1oPt55INjbFpjE/7WeAjD9KqrgB87pxCDs+R1ye3Fu4Pw718CqDu # LAhVhSK46xgaTfwqIa1JMYNHlXdx3LEbS0scEJx3FMGdTy9alQgpECYxggUYMIIF # FAIBATBkMFAxJTAjBgNVBAMMHEdhbmVzaCBSYXNrYXIgU2ltcGxlUFNMb2dnZXIx # JzAlBgkqhkiG9w0BCQEWGGdhbmVzaHJhc2thckBvdXRsb29rLmNvbQIQZDtcNubu # NpVEfXOVHbciTzAJBgUrDgMCGgUAoHgwGAYKKwYBBAGCNwIBDDEKMAigAoAAoQKA # ADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYK # KwYBBAGCNwIBFTAjBgkqhkiG9w0BCQQxFgQUM/0SUAuMYdupRerQgK+7MzTMfPEw # DQYJKoZIhvcNAQEBBQAEggIAm8E45LqPfh2CPCPpjUXVNDcMVggxFwZbJJ995Olu # oQrZO1HGFDyfOJOq8G1WIz4ThU/D9LMi39u5DXkgG47uy9MmgWE3lZNIlRs/m0z4 # IQaZb1M9eOczCTOlXnBjgNqn+LJk6lyZbjcD15Qo9460UnV6eqfpDL8E/zjOW5B6 # FMKu0RZatlsHLM+8THm/BNl4UUSI/SNfAfAI2abX7ckSjhBfTeD7XUblX9p6ZfV/ # +BcVz+esdpUFLaLWAIZaltVCEcUiR7kaK8MJz2ZQXdC3AcMG1r6216dlaBHpLun6 # WYfXDj495CQcGzT2oibbmWyP8C33e49ZgmXH6xEFCauocPBCX2UbmfuN33y9GV8L # od7b8ld9ED0peXp3qAh/ScbbLYn3iHj4tyUsU6DyUoWwxu6xOAIsy5jZGGmiBApa # p/qucWbJUycu+JtYsJrHtTMwSF70PTi9WuxMFAxB0/meTDq4xz5VYg1/1YQ8OB5t # h6HfUcEmEFVnKXOmpBACpmWIioKIzpXXIQoilPtQv+KhDbeDxneETTtghve8xKFn # 2c/9upxf3RYC+kxQO8pBYURoyv/EmuscsZYII1f5BFVc0kT/SGKTxyHQqIYZN71y # +Yuplj0cLILCao1ajUeOn7QyyQRUCyOEpvJ9+tLIcfQbKJ/ODscEspOnav5xCOSw # oLGhggIPMIICCwYJKoZIhvcNAQkGMYIB/DCCAfgCAQEwdjBiMQswCQYDVQQGEwJV # UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu # Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBBc3N1cmVkIElEIENBLTECEAMBmgI6/1ix # a9bV6uYX8GYwCQYFKw4DAhoFAKBdMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEw # HAYJKoZIhvcNAQkFMQ8XDTIwMDcwNDIwNDgwMVowIwYJKoZIhvcNAQkEMRYEFC+e # S0Ws+DFgg4QYlvUZ2TIEF3bhMA0GCSqGSIb3DQEBAQUABIIBAA42zCf5igzK2r1z # 2/GdHo8wqD3ZYZnGYcVgfRMiUC2o7gGPHFI7GXcuoGBzrh4VGOAwM4bjHy1BMcNK # T9DOY5y6G/AXxNfjhTbwMrn0FIdyGIyWgP1QsNgFr/U+f717tl1t5UzFIKftYl20 # 4Fi9CbxTYi9UXqjDCa+pEuTnCLbVFIFkHtrNdGnn6ttVtO4erDXYNIf6eZZj8ZB0 # T1tf+/okvpLtiYDY+HJ8tW8+szuiSiF6Zq1uSRfjsi7N5GXNIya7rgAos3p6Asdd # WFD8sC3H6mBCw31mcSsyIbfZRo5VGMcqqcTV7l8g40ZDt6SDuZfzHYdt3aD7WNHS # GleXPV4= # SIG # End signature block |