modules/AzStack.Observability/AzStack.Observability.psm1

<###################################################
# #
# Copyright (c) Microsoft. All rights reserved. #
# #
##################################################>


#################################################################
# #
# ENUMS AND CLASSES #
# #
#################################################################

enum TraceLevel {
    Error
    Exception
    Informational
    Verbose
    Warning
}

#################################################################
# #
# STARTUP ACTIONS ON IMPORT #
# #
#################################################################

# Localized strings for Enable-AzsSupportTraceLog/Disable-AzsSupportTraceLog (moved here from
# AzStack.Common alongside those functions, since they exclusively control the
# AZS_SUPPORT_TRACE_ENABLED env var this module's own Write-EtwTrace reads).
Import-LocalizedData -BindingVariable 'msg' -BaseDirectory "$PSScriptRoot\locale" -UICulture $PSUICulture

# Module-scoped state backing Write-EtwTrace's lazily-initialized logger instance. This is
# intentionally independent of any importing module's own concerns -- every AzStack.* module now
# gets Write-AzsSupportLog/Trace-Exception simply by importing this module (see below), and this
# flag only tracks whether the ETW/file-logging backend itself is usable this session (e.g.
# missing assembly, a previous log call's disposed instance), so repeated failures don't keep
# re-attempting logger initialization and don't block console output, which never depends on this
# flag.
$script:EtwLoggerInstance = $null
$script:EtwLoggingEnabled = $true

# Load the assembly from an in-memory byte array instead of 'Add-Type -Path'.
# 'Add-Type -Path' loads the DLL directly from disk, which keeps a lock on the
# file for the lifetime of the PowerShell session and prevents the module
# folder from being deleted while a terminal is open. Loading from a byte array
# leaves the file on disk unlocked.
#
# Byte-loaded assemblies have no CodeBase/Location, so the CLR cannot probe the
# DLL's folder for dependencies. Dependencies are resolved lazily (at JIT time,
# not at Load time), so an AssemblyResolve handler scoped to the Load() call
# fires too early to help. Instead, pre-load all sibling DLLs from bytes first.
# Once an assembly is registered in the AppDomain, the CLR finds it by identity
# on any later lazy reference without needing file-system probing.
#
# This whole block is wrapped in try/catch so that a missing/broken assembly can never fail THIS
# MODULE'S OWN import. Write-AzsSupportLog/Trace-Exception/Write-EtwTrace/Initialize-Logger are
# now the single logging entry points for every AzStack.* module (they Import-Module this file
# directly), so if this block were left to throw uncaught, a missing DLL would take down every
# caller's console logging too, not just ETW/file logging -- a much bigger blast radius than
# today's actual failure mode (the compiled assembly being absent, e.g. in a broken deployment).
# On failure, $script:EtwLoggingEnabled is set to $false up front so Write-EtwTrace's own
# lazy-init path (below) skips straight to its no-op return without re-attempting anything.
try {
    if (-not ('Microsoft.AzureStack.CSSTools.Observability.Events.Logger' -as [type])) {
        # This path resolves against the *packaged* module layout, not the source tree. The
        # nuspec packs out\build\Events\** into 'lib', producing <moduleRoot>\lib\net48 alongside
        # <moduleRoot>\modules\AzStack.Observability, so '..\..\lib\net48' is correct once
        # installed. In a source checkout src\lib\net48 does not exist and never will: this
        # try/catch disables ETW/file logging for the session. Tests that need these types must
        # load the assembly from out\build\Events\net48 after .\build.ps1 (see
        # tests\TestCommon\Observability.TestCommon.ps1).
        $observabilityLibPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\lib\net48'
        $mainDllName = 'Microsoft.AzureStack.CSSTools.Observability.Events.dll'

        # Pre-load sibling dependency DLLs (skip any that are already in the AppDomain).
        $loadedAssemblyNames = [System.AppDomain]::CurrentDomain.GetAssemblies() |
            ForEach-Object { $_.GetName().Name }

        Get-ChildItem -Path $observabilityLibPath -Filter '*.dll' -ErrorAction Stop |
            Where-Object { $_.Name -ne $mainDllName } |
            ForEach-Object {
                $simpleName = [System.IO.Path]::GetFileNameWithoutExtension($_.Name)
                if ($simpleName -notin $loadedAssemblyNames) {
                    $null = [System.Reflection.Assembly]::Load([System.IO.File]::ReadAllBytes($_.FullName))
                }
            }

        $observabilityDllPath = Join-Path -Path $observabilityLibPath -ChildPath $mainDllName
        $null = [System.Reflection.Assembly]::Load([System.IO.File]::ReadAllBytes($observabilityDllPath))
    }
}
catch {
    # The compiled event assembly is unavailable (missing DLL, broken deployment, etc.). ETW/file
    # logging is disabled for the session, but this module -- and Write-AzsSupportLog/
    # Trace-Exception's console output -- still import and work normally.
    $script:EtwLoggingEnabled = $false
}

# Enable-AzsSupportTraceLog / Disable-AzsSupportTraceLog (below) toggle this machine environment
# variable. When set, Write-EtwTrace additionally writes to a shared file path unless the caller
# explicitly supplies its own -FilePath. Centralizing this check here (rather than in each caller,
# e.g. Write-AzsSupportLog) ensures all consumers respect the toggle consistently, and avoids a
# first-caller-wins ordering bug against the shared logger singleton.
$script:TraceLogEnabledEnvVar = 'AZS_SUPPORT_TRACE_ENABLED'
$script:DefaultTraceLogFilePath = Join-Path -Path $env:SystemDrive -ChildPath 'Temp\Azs.Support\TraceLog.log'

#################################################################
# #
# FUNCTIONS #
# #
#################################################################

function Get-FormattedException {
    <#
    .SYNOPSIS
        Extracts details from an exception that is used to format the error message in a consistent manner.
        Does not capture the exception message as this might contain PII
 
    .PARAMETER Exception
        An exception or PowerShell ErrorRecord thrown by the CLR or with the throw keyword
    #>

    param(
        [AllowNull()]
        $Exception
    )
    $errorRecord = $null
    $actualException = $null
    $outerTypeName = $null
    $innerTypeName = $null

    if ($Exception -is [System.Management.Automation.ErrorRecord]) {
        $errorRecord = $Exception
        $actualException = $Exception.Exception
    }
    elseif ($Exception -is [System.Exception]) {
        $actualException = $Exception
        if ($Exception.PSObject.Properties['ErrorRecord'] -and $Exception.ErrorRecord -is [System.Management.Automation.ErrorRecord]) {
            $errorRecord = $Exception.ErrorRecord
        }
    }
    else {
        $receivedType = if ($null -eq $Exception) { 'null' } else { $Exception.GetType().FullName }
        $outerTypeName = "Unformattable:$receivedType"
    }

    if ($null -ne $actualException) {
        $outerTypeName = $actualException.GetType().FullName
    }

    if ($actualException -and $null -ne $actualException.InnerException) {
        $innerTypeName = $actualException.InnerException.GetType().FullName
    }

    $sanitizeStackTrace = {
        param([AllowNull()][string]$StackTrace)

        if ([string]::IsNullOrWhiteSpace($StackTrace)) {
            return $StackTrace
        }

        try {
            # The root alternation must cover every shape a rooted path can take, otherwise the
            # directory portion survives and reaches telemetry. [\\/]{1,2} covers the POSIX root
            # (/home/...), the UNC root (\\server\share\...) and its forward-slash form
            # (//server/share/...). CSSTools is routinely run from a share such as
            # \\server\share\<alias>\CSSTools, so an unsanitized UNC frame leaks the alias.
            return [regex]::Replace(
                $StackTrace,
                '(?<prefix>(?:,|\bin)\s+)(?:(?:[a-z]:[\\/])|[\\/]{1,2})(?:[^\\/\r\n:]+[\\/])+(?<file>[^\\/:\r\n]+)(?=:\s*(?:line\s*)?\d+)',
                '${prefix}${file}',
                [System.Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [System.Text.RegularExpressions.RegexOptions]::Multiline,
                [TimeSpan]::FromMilliseconds(250)
            )
        }
        catch [System.Text.RegularExpressions.RegexMatchTimeoutException] {
            return '[stack trace omitted: sanitization timeout]'
        }
    }

    $safeInvocationInfo = $null
    if ($errorRecord -and $errorRecord.InvocationInfo) {
        $scriptName = if ([string]::IsNullOrWhiteSpace($errorRecord.InvocationInfo.ScriptName)) {
            $null
        }
        else {
            [System.IO.Path]::GetFileName($errorRecord.InvocationInfo.ScriptName)
        }

        $safeInvocationInfo = @{
            CommandName      = if ($errorRecord.InvocationInfo.MyCommand) { $errorRecord.InvocationInfo.MyCommand.Name } else { $null }
            ScriptName       = $scriptName
            ScriptLineNumber = $errorRecord.InvocationInfo.ScriptLineNumber
            OffsetInLine     = $errorRecord.InvocationInfo.OffsetInLine
        }
    }

    return (@{
            ErrorRecord    = @{
                ScriptStackTrace = if ($errorRecord) { & $sanitizeStackTrace $errorRecord.ScriptStackTrace } else { $null }
                InvocationInfo   = $safeInvocationInfo
            }
            OuterException = @{
                TypeName   = $outerTypeName
                Source     = if ($actualException) { $actualException.Source } else { $null }
                StackTrace = if ($actualException) { & $sanitizeStackTrace $actualException.StackTrace } else { $null }
            }
            InnerException = @{
                TypeName   = $innerTypeName
                Source     = if ($actualException -and $actualException.InnerException) { $actualException.InnerException.Source } else { $null }
                StackTrace = if ($actualException -and $actualException.InnerException) { & $sanitizeStackTrace $actualException.InnerException.StackTrace } else { $null }
            }
        }) | ConvertTo-Json -Depth 3 -Compress
}

function Initialize-Logger {
    param (
        [Parameter(Mandatory = $true)]
        [guid]$InstanceGuid,

        [Parameter(Mandatory = $false)]
        [string]$FilePath
    )

    # If FilePath is not provided, then it will be an empty string and the logger will not log to a file.
    $logger = [Microsoft.AzureStack.CSSTools.Observability.Events.Logger]::New($InstanceGuid, $FilePath)
    return $logger
}

function Write-EtwTrace {
    <#
    .SYNOPSIS
        Writes a message to the shared CSSTools ETW logger.
    .DESCRIPTION
        Centralizes ETW logger lifecycle management (lazy initialization, dispatch, and
        fail-safe disable-on-error) so multiple modules (AzStack.Common, AzStack.Insights, etc.)
        can share a single implementation instead of duplicating ETW dispatch logic. Callers are
        responsible for their own "did AzStack.Observability import succeed" gate; this function
        only tracks whether a previous ETW log call itself failed at runtime.
    .PARAMETER Level
        The ETW log level. Valid values are 'Verbose', 'Informational', 'Warning', 'Error', 'Exception'.
    .PARAMETER Message
        The message to log.
    .PARAMETER FormattedException
        The formatted exception details to log. Required when -Level is 'Exception'.
    .PARAMETER ModuleName
        Optional name of the PowerShell module the log call originated from. Passed through to the
        ETW event and the debug file log for source attribution. Empty when the caller is not part
        of an imported module (e.g. an Insight Rule/Analyzer/Component/Remediation .ps1 script).
    .PARAMETER FunctionName
        Optional name of the function or script that made the log call. For a direct function call
        this is the function name; for a .ps1 script invoked via the call operator this is the
        script's file name. Passed through to the ETW event and the debug file log for source
        attribution.
    .PARAMETER FilePath
        Optional file path to additionally log to. If not provided, the logger falls back to a shared
        default file path when the AZS_SUPPORT_TRACE_ENABLED machine environment variable is set
        (see Enable-AzsSupportTraceLog/Disable-AzsSupportTraceLog in AzStack.Common), and otherwise
        does not log to a file.
    .PARAMETER InstanceGuid
        The instance GUID for the logger. Default is a new GUID.
    .EXAMPLE
        PS> Write-EtwTrace -Level 'Verbose' -Message 'This is a verbose message.'
    .EXAMPLE
        PS> Write-EtwTrace -Level 'Exception' -Message $exceptionMessage -FormattedException $formattedException
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet('Verbose', 'Informational', 'Warning', 'Error', 'Exception')]
        [string]$Level,

        [Parameter(Mandatory = $false)]
        [string]$Message,

        [Parameter(Mandatory = $false)]
        $FormattedException,

        [Parameter(Mandatory = $false)]
        [string]$ModuleName,

        [Parameter(Mandatory = $false)]
        [string]$FunctionName,

        [Parameter(Mandatory = $false)]
        [string]$FilePath,

        [Parameter(Mandatory = $false)]
        [Guid]$InstanceGuid = [guid]::NewGuid()
    )

    # A previous ETW log call already failed this session (e.g. missing assembly, disposed
    # instance). Skip re-attempting logger initialization so callers are not repeatedly disrupted.
    if (-not $script:EtwLoggingEnabled) {
        return
    }

    try {
        # Initialize the shared logger instance if not already done.
        if ($null -eq $script:EtwLoggerInstance) {
            $loggerParams = @{
                InstanceGuid = $InstanceGuid
            }

            # An explicit -FilePath always wins. Otherwise, fall back to the shared default file
            # path when file-based tracing has been enabled via the machine environment variable.
            $resolvedFilePath = $FilePath
            if (-not $resolvedFilePath -and [System.Environment]::GetEnvironmentVariable($script:TraceLogEnabledEnvVar, [System.EnvironmentVariableTarget]::Machine)) {
                $resolvedFilePath = $script:DefaultTraceLogFilePath
            }

            if ($resolvedFilePath) {
                [void]$loggerParams.Add('FilePath', $resolvedFilePath)
            }

            $script:EtwLoggerInstance = Initialize-Logger @loggerParams
        }

        switch ($Level) {
            'Verbose' { $script:EtwLoggerInstance.LogVerbose($Message, $ModuleName, $FunctionName) }
            'Informational' { $script:EtwLoggerInstance.LogInformational($Message, $ModuleName, $FunctionName) }
            'Warning' { $script:EtwLoggerInstance.LogWarning($Message, $ModuleName, $FunctionName) }
            'Error' { $script:EtwLoggerInstance.LogError($Message, $ModuleName, $FunctionName) }
            'Exception' { $script:EtwLoggerInstance.LogException($Message, $FormattedException, $ModuleName, $FunctionName) }
        }
    }
    catch {
        # ETW logging infrastructure is unavailable (e.g. missing assembly, disposed instance).
        # Disable for the remainder of the session so repeated failures don't affect callers.
        $script:EtwLoggingEnabled = $false
        $script:EtwLoggerInstance = $null
    }
}

function Write-AzsSupportLog {
    <#
    .SYNOPSIS
        Writes a log message to the AzStack Support/Insights log. The single logging entry point
        for the Microsoft.AzLocal.CSSTools module.
    .PARAMETER Message
        The log message to write.
    .PARAMETER Level
        The log level. Valid values are 'Verbose', 'Informational', 'Warning', 'Error', 'Exception'.
        Default is 'Informational'. Automatically forced to 'Exception' when -Exception is supplied.
    .PARAMETER FunctionName
        Optional override for the function/script name attributed to this log entry in the ETW event
        and debug file log. Defaults to the immediate caller, auto-detected from the call stack.
    .PARAMETER ModuleName
        Optional override for the module name attributed to this log entry in the ETW event and debug
        file log. Defaults to the immediate caller's module, auto-detected from the call stack (empty
        when the caller is not part of an imported module, e.g. an Insight Rule .ps1 script).
    .PARAMETER Exception
        An exception or PowerShell ErrorRecord to log. When supplied, -Level is forced to 'Exception'
        and -Message is derived from it. Accepts pipeline input (e.g. `$_ | Write-AzsSupportLog` in a
        catch block).
    .DESCRIPTION
        This function writes a log message using the CSSToolsObservabilityLogger. File-based ETW
        tracing is controlled centrally by this module's Write-EtwTrace and the
        AZS_SUPPORT_TRACE_ENABLED machine environment variable (see Enable-AzsSupportTraceLog/
        Disable-AzsSupportTraceLog in AzStack.Common) rather than by this function.
        In addition to ETW logging, this function also prints the message to the console via PowerShell's
        standard streams so callers have user-facing visibility: 'Verbose' is written via Write-Verbose
        (visible only when the caller passes -Verbose), 'Informational' via Write-Information (visible only
        when the caller passes -InformationAction Continue or sets $InformationPreference), 'Warning' via
        Write-Warning, and 'Error' via Write-Error. 'Exception' is intentionally silent on the console (no
        Write-Error) so that catching and logging an exception here never surfaces a user-facing error;
        callers that need the exception surfaced to the user should Write-Error it themselves separately.
        Console output is independent of ETW logging and is emitted even if ETW logging is unavailable or
        disabled (e.g. the compiled event assembly is missing).
    .EXAMPLE
        PS> Write-AzsSupportLog -Message "This is a verbose message." -Level "Verbose"
    .EXAMPLE
        PS> Write-AzsSupportLog -Message "This is an informational message." -Level "Informational"
    .EXAMPLE
        PS> Write-AzsSupportLog -Message "This is a warning message." -Level "Warning"
    .EXAMPLE
        PS> Write-AzsSupportLog -Message "This is an error message." -Level "Error"
    .EXAMPLE
        PS> try { 1 / 0 } catch { $_ | Write-AzsSupportLog }
    #>

    [CmdletBinding(DefaultParameterSetName = 'Message')]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Message')]
        [string]$Message,

        [Parameter(Mandatory = $false, ParameterSetName = 'Message')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Exception')]
        [TraceLevel]$Level = 'Informational',

        [Parameter(Mandatory = $false, ParameterSetName = 'Message')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Exception')]
        [string]$FunctionName,

        [Parameter(Mandatory = $false, ParameterSetName = 'Message')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Exception')]
        [string]$ModuleName,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Exception')]
        $Exception
    )
    process {

        # $Level is not itself pipeline-bound, so PowerShell binds it once for the whole pipeline
        # invocation and reuses the same variable across every process{} iteration -- but the
        # Exception branch below mutates it to [TraceLevel]::Exception. Without re-deriving it fresh
        # here, a later Message-set item in a mixed pipeline would inherit the PRIOR item's mutated
        # Exception level instead of the caller's originally-requested (or default) level, and
        # incorrectly hit the "Level 'Exception' requires an Exception parameter" guard below.
        # $PSBoundParameters['Level'] reflects only what the caller actually passed and is never
        # touched by this function's own internal reassignments, so it is safe to re-read every time.
        $Level = if ($PSBoundParameters.ContainsKey('Level')) { $PSBoundParameters['Level'] } else { [TraceLevel]::Informational }

        if ($Exception) {
            # Handle both ErrorRecord and System.Exception types
            if ($Exception -is [System.Management.Automation.ErrorRecord]) {
                $actualException = $Exception.Exception
            }
            elseif ($Exception -is [System.Exception]) {
                $actualException = $Exception
            }
            else {
                throw "Exception parameter must be of type [System.Exception] or [ErrorRecord]. Received type: $($Exception.GetType().FullName)"
            }

            # Validate the exception has a message
            if ([string]::IsNullOrWhiteSpace($actualException.Message)) {
                throw "The Exception must contain a valid error message. The Message property is null or empty."
            }

            $Message = $actualException.Message
            # Preserve the original ErrorRecord so the formatter can retain PowerShell's invocation and script stack.
            $formattedException = Get-FormattedException -Exception $Exception
            $Level = [TraceLevel]::Exception
        }
        else {
            $formattedException = $null
            if ($Level -ieq 'Exception') {
                throw "Level 'Exception' requires an Exception parameter to be provided."
            }
        }

        # Write-AzsSupportLog is exported from this module and Import-Module'd (not dot-sourced) by
        # every other AzStack.* module, so its unbound $VerbosePreference/$InformationPreference
        # resolve against THIS module's own session state (module scope, then global scope) and never
        # against the caller's scope. A caller several frames up that set -InformationAction
        # Continue/-Verbose (e.g. Invoke-AzsSupportInsight -InformationAction Continue) would otherwise
        # be silently ignored here even though the preference threads correctly through the
        # intermediate component/analyzer/rule *scripts* (which inherit it via normal dynamic scoping
        # since they are not separately-imported modules).
        # $PSCmdlet.GetVariableValue() looks up the variable in the caller's scope chain rather than
        # this module's, so pull it explicitly.
        # Guard each assignment on $PSBoundParameters: when -Verbose/-InformationAction is passed to
        # Write-AzsSupportLog itself, PowerShell has already seeded the local preference from that
        # switch, and an unconditional overwrite would discard it in favor of the (possibly quieter)
        # caller preference. An explicit switch on this function must win over the inherited one.
        if (-not $PSBoundParameters.ContainsKey('Verbose')) {
            $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
        }
        if (-not $PSBoundParameters.ContainsKey('InformationAction')) {
            $InformationPreference = $PSCmdlet.GetVariableValue('InformationPreference')
        }

        # Auto-detect the immediate caller for source attribution in the ETW event/file log, unless an
        # explicit override was supplied (e.g. a wrapper like Trace-Exception that wants to attribute
        # the log entry to its own logical caller rather than to itself). Call stack index 1 is the
        # direct caller of Write-AzsSupportLog (index 0 is Write-AzsSupportLog itself). '.Command'
        # resolves correctly for both a normal function call (function name) and a .ps1 script invoked
        # via the call operator, e.g. an Insight Rule/Analyzer/Component/Remediation (script file name),
        # unlike '.FunctionName' which would just report '<ScriptBlock>' for the latter.
        if (-not $PSBoundParameters.ContainsKey('FunctionName')) {
            $FunctionName = (Get-PSCallStack)[1].Command
        }
        if (-not $PSBoundParameters.ContainsKey('ModuleName')) {
            # Empty when the caller is not part of an imported module (e.g. an Insight Rule .ps1
            # script invoked directly rather than as an exported module function).
            $ModuleName = (Get-PSCallStack)[1].InvocationInfo.MyCommand.ModuleName
        }

        # Print the message to the console via PowerShell's standard streams so callers get user-facing
        # visibility. This is independent of the ETW/file logging below (which is not user-facing on its
        # own) and runs regardless of whether ETW logging is enabled or available.
        switch ($Level) {
            'Verbose'       { Write-Verbose -Message $Message }
            'Informational' { Write-Information -MessageData $Message }
            'Warning'       { Write-Warning -Message $Message }
            'Error'         { Write-Error -Message $Message -ErrorAction Continue }
            'Exception'     {
                # Intentionally silent on the console: Write-AzsSupportLog is meant to catch and log an
                # exception without surfacing a user-facing error. Callers that need the exception
                # surfaced to the user should Write-Error it themselves separately.
            }
        }

        $etwParams = @{
            Level        = $Level.ToString()
            Message      = $Message
            ModuleName   = $ModuleName
            FunctionName = $FunctionName
        }
        if ($Level -eq 'Exception') {
            $etwParams['FormattedException'] = $formattedException
        }

        try {
            # Forward to the shared ETW dispatcher (Write-EtwTrace, a sibling function in this same
            # module). Write-EtwTrace owns lazy logger initialization and its own fail-safe internally
            # (it disables itself on failure via $script:EtwLoggingEnabled, which is also proactively
            # set to $false above if this module's own DLL-loading failed at import time), so this call
            # is unconditional -- no separate "did logging initialize" gate is needed here. The
            # try/catch is defense in depth only: a dispatch failure must never impact console output
            # above, or propagate to the caller of Write-AzsSupportLog.
            Write-EtwTrace @etwParams
        }
        catch {
            # Deliberately empty -- see above.
        }
    }
}

function Trace-Exception {
    <#
    .SYNOPSIS
        Extracts information out of exceptions to write to the log file.
        Pipe exceptions to this command in a catch block.
    .PARAMETER Exception
        Any exception inherited from [System.Exception]
    .EXAMPLE
        try
        {
            1 / 0 #divide by 0 exception
        }
        catch
        {
            $_ | Trace-Exception
        }
    #>

    param(
        [parameter(Mandatory = $True, ValueFromPipeline = $true)]
        $Exception
    )
    process {

        # Attribute the log entry to THIS function's own caller (not to Trace-Exception itself), since
        # Trace-Exception is just a convenience wrapper -- callers write `$_ | Trace-Exception` in their
        # own catch blocks and expect the log to point at their own function/script, not at this one.
        # Both -FunctionName and -ModuleName must come from the SAME upstream frame (index 1, i.e. this
        # function's own caller): Write-AzsSupportLog only auto-detects a value when it is NOT supplied,
        # so if only -FunctionName were forwarded here, its own auto-detected -ModuleName would resolve
        # to *this* function's module (AzStack.Observability) rather than the real caller's module,
        # producing mixed provenance like "AzStack.Observability\<external caller's function>".
        $callerFrame = (Get-PSCallStack)[1]
        Write-AzsSupportLog -Exception $Exception -FunctionName $callerFrame.Command -ModuleName $callerFrame.InvocationInfo.MyCommand.ModuleName
    }
}

function Set-TraceLogEnvironmentVariable {
    <#
    .SYNOPSIS
        Sets the AZS_SUPPORT_TRACE_ENABLED machine environment variable.
    .DESCRIPTION
        Private helper that wraps the static [System.Environment]::SetEnvironmentVariable call so it
        can be mocked in unit tests. Not exported.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.Boolean]
        $Enabled
    )

    [System.Environment]::SetEnvironmentVariable($script:TraceLogEnabledEnvVar, $Enabled, [System.EnvironmentVariableTarget]::Machine)
}

function Enable-AzsSupportTraceLog {
    <#
    .SYNOPSIS
        Enables trace logging to file for AzStack Support.
    .DESCRIPTION
        This function sets the AZS_SUPPORT_TRACE_ENABLED machine environment variable. When set,
        the shared ETW dispatcher (Write-EtwTrace in AzStack.Observability) additionally writes
        Write-AzsSupportLog events to a shared log file, in addition to ETW tracing.
        Because the underlying ETW logger is a single instance shared for the lifetime of the
        PowerShell process, this setting only takes effect for new sessions started after it is
        changed.
    .EXAMPLE
        PS> Enable-AzsSupportTraceLog
    #>

    [CmdletBinding()]
    param ()

    try {
        Set-TraceLogEnvironmentVariable -Enabled $true
        Write-Information -MessageData ($msg.TraceLogStatus -f "Enabled") -InformationAction Continue
        Write-Information -MessageData $msg.TraceLogRestartRequired -InformationAction Continue
    }
    catch {
        $failureMessage = $msg.TraceLogFailure -f "Enabled", $_.Exception.Message
        Write-Error -Message $failureMessage
    }
}

function Disable-AzsSupportTraceLog {
    <#
    .SYNOPSIS
        Disables trace logging to file for AzStack Support.
    .DESCRIPTION
        This function removes or clears the AZS_SUPPORT_TRACE_ENABLED machine environment variable.
        When disabled, Write-AzsSupportLog events will only use ETW tracing. Because
        the underlying ETW logger is a single instance shared for the lifetime of the PowerShell
        process, this setting only takes effect for new sessions started after it is changed.
    .EXAMPLE
        PS> Disable-AzsSupportTraceLog
    #>

    [CmdletBinding()]
    param ()

    try {
        Set-TraceLogEnvironmentVariable -Enabled $false
        Write-Information -MessageData ($msg.TraceLogStatus -f "Disabled") -InformationAction Continue
        Write-Information -MessageData $msg.TraceLogRestartRequired -InformationAction Continue
    }
    catch {
        $failureMessage = $msg.TraceLogFailure -f "Disabled", $_.Exception.Message
        Write-Error -Message $failureMessage
    }
}

function Enable-AzsSupportInsightLog {
    <#
    .SYNOPSIS
        Deprecated. Use Enable-AzsSupportTraceLog instead.
    .DESCRIPTION
        This function has been renamed to Enable-AzsSupportTraceLog as part of the AzStack.Observability
        consolidation. This shim forwards the call to the new function so existing scripts continue to
        work, and prints a deprecation notice via plain Write-Information so it is always visible
        regardless of the caller's InformationPreference.
    .EXAMPLE
        PS> Enable-AzsSupportInsightLog
    #>

    [CmdletBinding()]
    param ()

    # TODO: Deprecated function - remove in post 2609 release
    # -MessageData must be passed by name, not piped: it is ValueFromPipeline on pwsh 7 but NOT on
    # Windows PowerShell 5.1 (the shipping edition per this module's PowerShellVersion requirement),
    # so a piped call silently prints nothing and raises a missing-mandatory-parameter error there.
    Write-Information -MessageData $msg.EnableAzsSupportInsightLogDeprecated -InformationAction Continue
    Enable-AzsSupportTraceLog
}

function Disable-AzsSupportInsightLog {
    <#
    .SYNOPSIS
        Deprecated. Use Disable-AzsSupportTraceLog instead.
    .DESCRIPTION
        This function has been renamed to Disable-AzsSupportTraceLog as part of the AzStack.Observability
        consolidation. This shim forwards the call to the new function so existing scripts continue to
        work, and prints a deprecation notice via plain Write-Information so it is always visible
        regardless of the caller's InformationPreference.
    .EXAMPLE
        PS> Disable-AzsSupportInsightLog
    #>

    [CmdletBinding()]
    param ()

    # TODO: Deprecated function - remove in post 2609 release
    # -MessageData must be passed by name, not piped: it is ValueFromPipeline on pwsh 7 but NOT on
    # Windows PowerShell 5.1 (the shipping edition per this module's PowerShellVersion requirement),
    # so a piped call silently prints nothing and raises a missing-mandatory-parameter error there.
    Write-Information -MessageData $msg.DisableAzsSupportInsightLogDeprecated -InformationAction Continue
    Disable-AzsSupportTraceLog
}

Export-ModuleMember -Function Get-FormattedException
Export-ModuleMember -Function Initialize-Logger
Export-ModuleMember -Function Write-EtwTrace
Export-ModuleMember -Function Write-AzsSupportLog
Export-ModuleMember -Function Trace-Exception
Export-ModuleMember -Function Enable-AzsSupportTraceLog
Export-ModuleMember -Function Disable-AzsSupportTraceLog
Export-ModuleMember -Function Enable-AzsSupportInsightLog
Export-ModuleMember -Function Disable-AzsSupportInsightLog

# SIG # Begin signature block
# MIInRAYJKoZIhvcNAQcCoIInNTCCJzECAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBWPbKDU2CG6erM
# r+q1ob72fppjLRPQgJfVny5RO40N3KCCDLowggX1MIID3aADAgECAhMzAAACHU0Z
# yE7XD1dIAAAAAAIdMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNVBAYTAlVTMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBD
# b2RlIFNpZ25pbmcgUENBIDIwMjQwHhcNMjYwNDE2MTg1OTQzWhcNMjcwNDE1MTg1
# OTQzWjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYD
# VQQDExVNaWNyb3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IB
# DwAwggEKAoIBAQDQvewXxx9gZZFC6Ys1WBay8BJ8kGA4JQnH5CMafqOASlTpK9H8
# o5ZXTXt0caVQTNMUPt445wXYD+dFtaKWTwDn1I52oUSrC9vJin1Gsqt+zyKJL5Dg
# 3eQXbQNR61DmMy20GLTIO3SFed9Rfi/ophgCLGFLDR3r0KvHjwMb/jYWS0celV/4
# Lz27LfAekm8v9E5IXaeiXbAUYZKK090n4CVl3JBtbN+9DtI9SNu/yjvozW52/u7R
# X/Ttpa/KDlpuokZ+Zcbvmtd9ur9gFLvZzh41o9MsE/clQtdaFWGvuo6Jua/ntpgk
# ey3E5/vBFe+MJPG6phdnuo6r57ZudCudiI1bAgMBAAGjggGbMIIBlzAOBgNVHQ8B
# Af8EBAMCB4AwHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0O
# BBYEFH6QuMwqcPG0hQlQ6c5jCtTTLrVeMEUGA1UdEQQ+MDykOjA4MR4wHAYDVQQL
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xFjAUBgNVBAUTDTIzMDAxMis1MDc1NTkw
# HwYDVR0jBBgwFoAUf1k/VCHarU/vBeXmo9ctBpQSCDEwYAYDVR0fBFkwVzBVoFOg
# UYZPaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0
# JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNybDBtBggrBgEFBQcBAQRh
# MF8wXQYIKwYBBQUHMAKGUWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMv
# Y2VydHMvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNy
# dDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQBKTbYOjzwTG/DXGaz9
# s6+fQeaTtDcFmMY+5UyVFCyj7Pv+5i37qfX8lSL/tBIfYQfWsMuBQlfZurJD6r4H
# VJ2CeH+1fgiq8dcHdVKoZ3Sa2qXoX3cq9iS8cVb06B7+5/XJ7I0OxHH9fDsvJ3T3
# w5V/ZtAIFmLrl+P0CtG+92uzRsn0nTbdFjOkLMLWPLAU3THohKRlSEMgFJpPkm5n
# 5UAZ35xX6FWCrDLsSKb555bTifwa8mJBwdlof0bmfYidH+dxZ1FdDxvLnNl9zeKs
# A4kejaaIqqIPguhwAti5Ql7BlTNoJNwxCvBmqW2MQLnCkYN/VVUsR3V2x/rcTNzo
# Bf/Z/SpROvdaA2ZOOd1uioXJt3tdLQ7vHpqpib0KfWr/FWXW10q38VxfCnRQBqzb
# SuztR7nEMuzX7Ck+B/XaPDXd1qh72+QYyB0Z2VzWmO9zsnb9Uq/dwu8LGeQqnyu6
# 7SDGACvnXii2fb9+US492VTnXSnFKyqwgzUyFMtZK1/sHYTv6bG4TtQUygQxTN+Z
# V+aJIlKO2MqZ7bKrAnOzS9m6NgoTdWOq11bTOZwKlIEV/EhV9SWkDmdpR/hPPT2v
# 6TEj4F8PT/zHjRezIU5c/DGlt/VhY/pK0XkJtEyMmmS1BMtjU/rqBZVMIm3dnxQs
# /TBByr+Cf8Z1r7aifQVQ+WSqzjCCBr0wggSloAMCAQICEzMAAAA5O7Y3Gb8GHWcA
# AAAAADkwDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX
# YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg
# Q29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRl
# IEF1dGhvcml0eSAyMDExMB4XDTI0MDgwODIwNTQxOFoXDTM2MDMyMjIyMTMwNFow
# VzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEo
# MCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAyNDCCAiIwDQYJ
# KoZIhvcNAQEBBQADggIPADCCAgoCggIBANgBnB7jOMeqlRYHNa265v4IY9fH8TKh
# emHfPINe1gpLaV3dhg324WwH06LcHbpnsBukCDNitryo0dtS/EW6I/yEL/bLSY8h
# KpbfQuWusBPr9qazYcDxCW/qnjb5JsI1s8bNOg3bVATvQVL4tcf03aTycsz8QeCd
# M0l/yHRObJ9QqazM1r6VPEOJ7LL+uEEb73w6QCuhs89a1uv1zerOYMnsneRRwCbp
# yW11IcggU0cRKDDq1pjVJzIbIF6+oiXXbReOsgeI8zu1FyQfK0fVkaya8SmVHQ/t
# Of23mZ4W9k0Ri22QW9p3UgSC5OUDktKxxcCmGL6tXLfOGSWHIIV4YrTJTT6PNty5
# REojHJuZHArkF9VnHTERWoTjAzfI3kP+5b4alUdhgAZ7ttOu1bVnXfHaqPYl2rPs
# 20ji03LOVWsh/radgE17es5hL+t6lV0eVHrVhsssROWJuz2MXMCt7iw7lFPG9LXK
# Gjsmonn2gotGdHIuEg5JnJMJVmixd5LRlkmgYRZKzhxSCwyoGIq0PhaA7Y+VPct5
# pCHkijcIIDm0nlkK+0KyepolcqGm0T/GYQRMhHJlGOOmVQop36wUVUYklUy++vDW
# eEgEo4s7hxN6mIbf2MSIQ/iIfMZgJxC69oukMUXCrOC3SkE/xIkgpfl22MM1itkZ
# 35nNXkMolU1lAgMBAAGjggFOMIIBSjAOBgNVHQ8BAf8EBAMCAYYwEAYJKwYBBAGC
# NxUBBAMCAQAwHQYDVR0OBBYEFH9ZP1Qh2q1P7wXl5qPXLQaUEggxMBkGCSsGAQQB
# gjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU
# ci06AjGQQ7kUBU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBPoE2gS4ZJaHR0cDovL2Ny
# bC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0MjAx
# MV8yMDExXzAzXzIyLmNybDBeBggrBgEFBQcBAQRSMFAwTgYIKwYBBQUHMAKGQmh0
# dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0MjAx
# MV8yMDExXzAzXzIyLmNydDANBgkqhkiG9w0BAQwFAAOCAgEAFJQfOChP7onn6fLI
# MKrSlN1WYKwDFgAddymOUO3FrM8d7B/W/iQ6DxXsDn7D5W4wMwYeLystcEqfkjz4
# NURRgazyMu5yRzQh4LqjA4tStTcJh1opExo7nn5PuPBYnbu0+THSuVHTe0VTTPVh
# ily/piFrDo3axQ9P4C+Ol5yet+2gTfekICS5xS+cYfSIvgn0JksVBVMYVI5QFu/q
# hnLhsEFEUzG8fvv0hjgkO+lkpV9ty6GkN4vdnd7ya6Q6aR9y34aiM1qmxaxBi6OU
# nyNl6fkuun/diTFnYDLTppOkr/mg5WSfCiDVMNCxtj4wPKC5OmHm1DQIt/MNokbb
# H3UGsFP1QbzsLocuSqLCvH09Io3fDPTmscR9Y75G4qX7RTX8AdBPo0I6OEojf39z
# uFZt0qOHm65YWQE69cZM2ueE1MB05dNNgHK9gTE7zKvK/fg8B2qjW88MT/WF5V5u
# vZGtqa9FSL2RazArA+rDPuf6JGYz4HpgMZHB4S6szWSKYBv0VisCzfxgeU+dquXW
# 9bd0auYlOB58DPcOYKdc3Se94g+xL4pcEhbB54JOgAkwYTu/9dLeH2pDqeJZAABV
# DWRQCaXfO5LgyKwKCLYXpigrZYCjUSBcr+Ve8PFWMhVTQl0v4q8J/AUmQN5W4n10
# 1cY2L4A7GTQG1h32HHAvfQESWP0xghngMIIZ3AIBATBuMFcxCzAJBgNVBAYTAlVT
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jv
# c29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIdTRnITtcPV0gAAAAAAh0w
# DQYJYIZIAWUDBAIBBQCggZAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwLwYJ
# KoZIhvcNAQkEMSIEIEXKD8HjZUEM2kHgEFPLo7uGr+IeiNT9vKYqKS/uKudMMEIG
# CisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEBBQAEggEAspdW1CnzAwj70WgL
# Ra7pfCUWNvj/wmawhXzB7ENA1gICnqaWv5+Ws9PHrO6pnT+GL0KDdb+wo6z49Ift
# fmpCk+bj6FIWKsEOyVYDqKFr/FsCnxKBrlh8rAI1eBN3AmndWSlNxQyaX35X+HFv
# znS656wUcUp5bWi1udb/O5PDGXpKNSoHXkMqpkrVpoWsloSBsA+vziBtLIzpQ+rl
# CyqJVJRJdkeX0q1pbpaE/UIMxvWm858XhW+RH4mqcz7czSkge5+34Bi32trc5c/m
# mI+iIGZ7YWn/gOqth40jU7ZKwINjaCArSlUhyhblemmxDqbL+batKE3cLXvsKwt4
# Y9R6UaGCF7AwghesBgorBgEEAYI3AwMBMYIXnDCCF5gGCSqGSIb3DQEHAqCCF4kw
# gheFAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsqhkiG9w0BCRABBKCCAUkEggFF
# MIIBQQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQCAQUABCA4fk/FZo5Ug/ma
# c1iuyI3hqfzpCjH8SVQjpCjwyeZEWgIGaolQ9UqbGBMyMDI2MDkwMjE1MjU1Ny40
# NzNaMASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExp
# bWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjo1MjFBLTA1RTAtRDk0NzEl
# MCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaCCEf4wggcoMIIF
# EKADAgECAhMzAAACF3H7LqWvAR3qAAEAAAIXMA0GCSqGSIb3DQEBCwUAMHwxCzAJ
# BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jv
# c29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI1MDgxNDE4NDgyM1oXDTI2MTEx
# MzE4NDgyM1owgdMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# LTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEn
# MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjUyMUEtMDVFMC1EOTQ3MSUwIwYDVQQD
# ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEAwM82sEw+39vYR7iGCIFDnYNhRM+BzF2AYiq5dUpZpJFP
# RjCcipQ6RUbI+RAYNRApExx5ygrXbaWtuwvqsqAVSWbU/W6fecujjILkPqn9pngt
# WRkfQgbYgvaXALl6PY2yOH9f72MD+6AyxQenSpAMdUzY/Qk/jtjsHdFXVBe+tshl
# IkSJ3GZw8VVKqTg3GZElztwbJWNtrhBEvhf6anxMegQMJP7tO8/BJ7ITs4/AV3D2
# bv8eHk81Y+fOmQ8mQ61WLq2wItvlzIT5bzelK9LvEycf5x1lXxAwEw5a7dpS+CKT
# anhtv+Q2mwebAybjf9io4k48stTaq1rtcrOiDwddqVm1S9e8h1TszXFzjLLvE9Em
# jnNfIewsY+RChUaHnY4FFwwJEnEv/JS76oHT0oGdy7+J60fGOl7A1UoUyAkhpb2B
# ja+SwSIiHbQ4FDyJiLlZ6drZZ84MoJ852JSxM0hBjGO6FZlPO8iuNyk680Di8Vnb
# SNpIdJN+DhlepeTUMBDHqCmd0mVWRWZPm1pvgty93asNt/Ng6o4m2dnooWOdM3yK
# sJaWjyHqic9gfTrZBM+PCXqeTaO1oEiaQ+h4w0nHVdV+XSvI2m1yN4iibqjm5HPa
# AO3OJ+OmNLftNVmr4Z6U2T6pIcLBysoKcDUvCqycXj4C/+n1KFBpDGdDMw9gmu8C
# AwEAAaOCAUkwggFFMB0GA1UdDgQWBBRQrN9jlwNOoeE5ZQqnF5x8S1bJQzAfBgNV
# HSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQhk5o
# dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBU
# aW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBeMFwG
# CCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRz
# L01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAMBgNV
# HRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMA4GA1UdDwEB/wQEAwIH
# gDANBgkqhkiG9w0BAQsFAAOCAgEARmgFdhB7xIAIHEEg5I/5S+gx67aR6RiW8ZAw
# tE3mz8o0dyn+pIP+lidNR1IKQQ0r+RjYgI9cZ6mbvAyvh3e2q/BV8rjHE3ud9PyY
# yq32euFgdZ3vX4b5QXePWlpBAYrdziR27rHz6WwpH5dZsSypbXDBbQkWkNl6g82y
# Ty3AbBbKDXBdzxZsEauaOplatK7Er4dhglKBex8JQ2dMSkSZweCNDXqd9r/9W2Vd
# RZsDJKP/Xc4UyQlVsboBotKtYESXFkjwR1HVsH+Q0C69/N5CP/Tq3YgI1ub4b9+3
# MJFKWhJXCcJGFZkcLwUmYwoFg1XLo7DLJdGjrIH1jsI2NFXJFQHef6AdRe1ERvYQ
# eqtyrBvxIvR+P/83FNYyzx04inUT9TF2AwTOuqCC6Z67oNwR4pEEJyAIEREvkdhj
# jfWcgsk/nGTlfahvNY/SOHrNRKo49KDlccNzRCJQyQ+D59r7/qebNSyQPTfwI9++
# jEY0Q/UWKVNLhio55GYBseJ99s7NzkdxOr9Uftp597HEovbA69qGlZ3OpUE3H1RB
# GDVp/FvM2uXTum8LrMkPXx5Ap/kbPASsC9ju9oMCe2IEXO2SeD1aD3IqvAOdHFKH
# g1vpbPUQSWb6g2xfBV30wFcqaPYgzcbxPWPyZqK+S8l7zw64aO5hmJ7eQwoMfTu0
# Vay6r48wggdxMIIFWaADAgECAhMzAAAAFcXna54Cm0mZAAAAAAAVMA0GCSqGSIb3
# DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G
# A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIw
# MAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAx
# MDAeFw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMyMjVaMHwxCzAJBgNVBAYTAlVT
# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1l
# LVN0YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA
# 5OGmTOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51yMo1V/YBf2xK4OK9uT4XYDP/
# XE/HZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY6GB9alKDRLemjkZrBxTzxXb1
# hlDcwUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9cmmvHaus9ja+NSZk2pg7uhp7
# M62AW36MEBydUv626GIl3GoPz130/o5Tz9bshVZN7928jaTjkY+yOSxRnOlwaQ3K
# Ni1wjjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDuaRr3tpK56KTesy+uDRedGbsoy
# 1cCGMFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74kpEeHT39IM9zfUGaRnXNxF80
# 3RKJ1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2K26oElHovwUDo9Fzpk03dJQc
# NIIP8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5TI4CvEJoLhDqhFFG4tG9ahha
# YQFzymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZki1ugpoMhXV8wdJGUlNi5UPkL
# iWHzNgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9QBXpsxREdcu+N+VLEhReTwDwV
# 2xo3xwgVGD94q0W29R6HXtqPnhZyacaue7e3PmriLq0CAwEAAaOCAd0wggHZMBIG
# CSsGAQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUCBBYEFCqnUv5kxJq+gpE8RjUp
# zxD/LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJlpxtTNRnpcjBcBgNVHSAEVTBT
# MFEGDCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jv
# c29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0wEwYDVR0lBAwwCgYI
# KwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGG
# MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186a
# GMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3Br
# aS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsG
# AQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwDQYJKoZIhvcN
# AQELBQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/ypb+pcFLY+TkdkeLEGk5c9MTO1
# OdfCcTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulmZzpTTd2YurYeeNg2LpypglYA
# A7AFvonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM9W0jVOR4U3UkV7ndn/OOPcbz
# aN9l9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECWOKz3+SmJw7wXsFSFQrP8DJ6L
# GYnn8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4FOmRsqlb30mjdAy87JGA0j3m
# Sj5mO0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3UwxTSwethQ/gpY3UA8x1RtnWN0
# SCyxTkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPXfx5bRAGOWhmRaw2fpCjcZxko
# JLo4S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVXVAmxaQFEfnyhYWxz/gq77EFm
# PWn9y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGConsXHRWJjXD+57XQKBqJC482
# 2rpM+Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU5nR0W2rRnj7tfqAxM328y+l7
# vzhwRNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEGahC0HVUzWLOhcGbyoYIDWTCC
# AkECAQEwggEBoYHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExp
# bWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjo1MjFBLTA1RTAtRDk0NzEl
# MCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsO
# AwIaAxUAabKAFaKt2haUdqkHfFYzAzfgSMuggYMwgYCkfjB8MQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGlt
# ZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsFAAIFAO5CTsswIhgPMjAyNjA5
# MDIwNzI5MTVaGA8yMDI2MDkwMzA3MjkxNVowdzA9BgorBgEEAYRZCgQBMS8wLTAK
# AgUA7kJOywIBADAKAgEAAgIcIwIB/zAHAgEAAgITJTAKAgUA7kOgSwIBADA2Bgor
# BgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAID
# AYagMA0GCSqGSIb3DQEBCwUAA4IBAQBLqnQ0V+8Vt+f0zPH/LnPXwpafW3K5JZ1P
# 5NyzU6SRAEn49LMOWAN6QL3cqY7kWlRUb3VNtzmd+smlWIZNkLN6sRlIIrzCPFvG
# 4jFjqnrScow3Xak0GHKNbK/+r7OiftLIOV4HpSa4FF4zynbawOte3/dxJ/EHSvUg
# VH8cTIZ91U07Wo+GN980d7h9rBm1qYuP/bbhGD7BtRbJg/Qs92Mwz8W7m0Udl9El
# DabAYuRbifRyI6cyvpxudG0DeMuFlwG13VsTkrbnthNwYaPxNthfLPPMj+tcF83w
# +IKL7Cl9AmFm5HbsgbMegLxYvOCEe9d9m6ngccbPLvBaizKpeaoGMYIEDTCCBAkC
# AQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNV
# BAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQG
# A1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAIXcfsupa8B
# HeoAAQAAAhcwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG
# 9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQgs2mY8izXWGZZmJtK7WYob1hkUbuJ0LEc
# Y7JcCyGZr1cwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCDQ8lBgPl23yZ0S
# zUSt5phOIegHPywrkNwevxe2k+RaWzCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0
# YW1wIFBDQSAyMDEwAhMzAAACF3H7LqWvAR3qAAEAAAIXMCIEIKj6hTyZodonOU1g
# CWasxwxGKzcwRjJ44RpBNsnK6/C8MA0GCSqGSIb3DQEBCwUABIICAKTBiCHiCJmJ
# d2/P4rRODhWCyQvJMh+3TdgRAWI9j30eFKnZ1hJKrP1UdfmsQ9bmezo+HTiptrVc
# 1v/GUlgjZCR9MfEN755No1aESXGlPxf4cFGixvHYkq/7qQqnrZqTxq5jwdEEIH9H
# kNXafMon/5aoMg5+GrZd/PaFWfUq0p5STORT5jykaELOhop8oQkjDQ/XEZoAvT7f
# icePqTtNzHpWK8rz1s3STCPf5h03JZeAc513DfDsDwekvSc5eLS1MR/6SCI27cCX
# JMj581V3MpNY7eUpyPHE2kJeqYesKJjcNBclOibjUhUbQYE55sag9ErDKszRS7Zf
# Hf7FWYv8GoT9Ety0QRL/0RBMnoMcNYcTQgY88VgdExRRpxiXkBg33tlCMRZi+q0t
# jwqwqRjfz8WJgpQboT+n6KObL4wMv9cVyQmV48kWJFh2ruk67FdKwCQ/mn/7/0lS
# WF6ltZczAEoAZ9VJAYrMBGZK0o+Gkgao2wTUDpzN/U1C8RvdjRe3QbWS7Re+Jral
# kDWg4+o6JNMZnGYJW1og4QF5HalOvPvoQarhFhCOA4GvqsqYEq0Cmhf5jv/au4BI
# Lo67xcvxgQe7py7KQ1Xqo9H+pqSNZYtlOeKIZGfcbkco62SpaNlH8U7PkQ1atoPR
# mCatH7fQFrhPgACPF+juYvETmmo4fCUE
# SIG # End signature block