internal/Write-Message.ps1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle#

function Write-Message {
    <#
        .SYNOPSIS
            This function acts as central information node for dbatools.
 
        .DESCRIPTION
            This function acts as central information node for dbatools.
            Other functions hand off all their information output for processing to this function.
 
            This function will then handle:
            - Warning output
            - Error management for non-terminating errors (For errors that terminate execution or continue on with the next object use "Stop-Function")
            - Logging
            - Verbose output
            - Message output to users
 
            At what complexity what path for the information is chosen is determined by the configuration settings:
            message.maximuminfo
            message.maximumverbose
            message.maximumdebug
            message.minimuminfo
            message.minimumverbose
            message.minimumdebug
            Which can be set to any level from 1 through 9
            Depending on the configuration it is very possible to have multiple paths chosen simultaneously
 
        .PARAMETER Message
            The message to write/log. The function name and timestamp will automatically be prepended.
 
        .PARAMETER Level
            This parameter represents the verbosity of the message. The lower the number, the more important it is for a human user to read the message.
            By default, the levels are distributed like this:
            - 1-3 Direct verbose output to the user (using Write-Host)
            - 4-6 Output only visible when requesting extra verbosity (using Write-Verbose)
            - 1-9 Debugging information, written using Write-Debug
            The specific level of verbosity preference can be configured using the settings of the message.maximum and message.minimum namespace.
 
            In addition, it is possible to select the level "Warning" which moves the message out of the configurable range:
            The user will always be shown this message, unless he silences the entire thing with -EnableException
 
            Possible levels:
            Critical (1), Important / Output (2), Significant (3), VeryVerbose (4), Verbose (5), SomewhatVerbose (6), System (7), Debug (8), InternalComment (9), Warning (666)
            Either one of the strings or its respective number will do as input.
 
        .PARAMETER EnableException
            By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message.
            This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting.
            Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch.
 
        .PARAMETER FunctionName
            The name of the calling function.
            Will be automatically set, but can be overridden when necessary.
 
        .PARAMETER ErrorRecord
            If an error record should be noted with the message, add the full record here.
            Especially designed for use with Warning-mode, it can legally be used in either mode.
            The error will be added to the $Error variable and enqued in the dbatools debugging system.
 
        .PARAMETER Warning
            Deprecated, do not use anymore
 
        .PARAMETER Once
            Setting this parameter will cause this function to write the message only once per session.
            The string passed here and the calling function's name are used to create a unique ID, which is then used to register the action in the configuration system.
            Thus will the lockout only be written if called once and not burden the system unduly.
            This lockout will be written as a hidden value, to see it use Get-DbaConfig -Force.
 
        .PARAMETER OverrideExceptionMessage
            Disables automatic appending of exception messages.
            Use in cases where you already have a speaking message interpretation and do not need the original message.
 
        .PARAMETER Target
            If an ErrorRecord was passed, it is possible to add the object on which the error occurred, in order to simplify debugging / troubleshooting.
 
        .EXAMPLE
            PS C:\> Write-Message -Message 'Connecting to Database1' -Level 4 -EnableException $EnableException
 
            Writes the message 'Connecting to Database1'. By default, this will be
            - Written to the in-memory message log
            - Written to the logfile
            - Written to the Verbose stream (Write-Verbose)
            - Written to the Debug stream (Write-Debug)
 
        .EXAMPLE
            PS C:\> Write-Message -Message "Connecting to Database 2 failed" -EnableException $EnableException -Warning -ErrorRecord $_ -Target $Database
 
            Writes the message "Connecting to Database 2 failed". By default, this will be
            - Written to the in-memory message log
            - Written to the in-memory error queue
            - Written to the $error variable
            - Written to the logfile
            - Written to the error log files
            - Written to the Warning stream (Write-Warning, not if silent)
            - Written to the Debug stream (Write-Debug)
 
        .NOTES
            Author: Friedrich Weinmann
            Tags: debug
 
        .NOTES
            For Implementers transitioning from previously used cmdlets, rule of thumb:
            - Write-Host: Level 2
            - Write-Verbose: Level 5
            - Write-Debug: Level 8
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidDefaultValueForMandatoryParameter", "")]
    [CmdletBinding(DefaultParameterSetName = 'Level')]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Message,

        [Parameter(Mandatory = $true, ParameterSetName = 'Level')]
        [Sqlcollaborative.Dbatools.dbaSystem.MessageLevel]
        $Level = "Warning",

        [bool]
        [Alias('Silent')]
        $EnableException = $EnableException,

        [string]
        $FunctionName = ((Get-PSCallStack)[0].Command),

        [System.Management.Automation.ErrorRecord[]]
        $ErrorRecord,

        [Parameter(Mandatory = $true, ParameterSetName = 'Warning')]
        [switch]
        $Warning,

        [string]
        $Once,

        [switch]
        $OverrideExceptionMessage,

        [object]
        $Target
    )

    # Since it's internal, I set it to always silent. Will show up in tests, but not bother the end users with a reminder over something they didn't do.
    Test-DbaDeprecation -DeprecatedOn "1.0.0" -Parameter "Warning" -CustomMessage "The parameter -Warning has been deprecated and will be removed on release 1.0.0. Please use '-Level Warning' instead." -EnableException $true

    $timestamp = Get-Date
    $developerMode = [Sqlcollaborative.Dbatools.dbaSystem.DebugHost]::DeveloperMode

    $max_info = [Sqlcollaborative.Dbatools.dbaSystem.MessageHost]::MaximumInformation
    $max_verbose = [Sqlcollaborative.Dbatools.dbaSystem.MessageHost]::MaximumVerbose
    $max_debug = [Sqlcollaborative.Dbatools.dbaSystem.MessageHost]::MaximumDebug
    $min_info = [Sqlcollaborative.Dbatools.dbaSystem.MessageHost]::MinimumInformation
    $min_verbose = [Sqlcollaborative.Dbatools.dbaSystem.MessageHost]::MinimumVerbose
    $min_debug = [Sqlcollaborative.Dbatools.dbaSystem.MessageHost]::MinimumDebug
    $info_color = [Sqlcollaborative.Dbatools.dbaSystem.MessageHost]::InfoColor
    $dev_color = [Sqlcollaborative.Dbatools.dbaSystem.MessageHost]::DeveloperColor

    #$coloredMessage = $Message
    $baseMessage = $Message
    foreach ($match in ($baseMessage | Select-String '<c=["''](.*?)["'']>(.*?)</c>' -AllMatches).Matches) {
        $baseMessage = $baseMessage -replace ([regex]::Escape($match.Value)), $match.Groups[2].Value
    }

    if ($developerMode) {
        $channels_future = @()
        if ((-not $EnableException) -and ($Level -eq [Sqlcollaborative.Dbatools.dbaSystem.MessageLevel]::Warning)) { $channels_future += "Warning" }
        if ((-not $EnableException) -and ($max_info -ge $Level) -and ($min_info -le $Level)) { $channels_future += "Information" }
        if (($max_verbose -ge $Level) -and ($min_verbose -le $Level)) { $channels_future += "Verbose" }
        if (($max_debug -ge $Level) -and ($min_debug -le $Level)) { $channels_future += "Debug" }

        if ((Test-Bound "Target") -and ($null -ne $Target)) {
            if ($Target.ToString() -ne $Target.GetType().FullName) { $targetString = " [T: $($Target.ToString())] " }
            else { $targetString = " [T: <$($Target.GetType().FullName.Split(".")[-1])>] " }
        }
        else { $targetString = "" }

        $newMessage = @"
[$FunctionName][$($timestamp.ToString("HH:mm:ss"))][L: $Level]$targetString[C: $channels_future][S: $EnableException][O: $($true -eq $Once)]
    $baseMessage
"@

    }
    else {
        $newMessage = "[$FunctionName][$($timestamp.ToString("HH:mm:ss"))] $baseMessage"
        $newColoredMessage = "[$FunctionName][$($timestamp.ToString("HH:mm:ss"))] $baseMessage"
    }
    if ($ErrorRecord -and ($Message -notmatch ([regex]::Escape("$($ErrorRecord[0].Exception.Message)"))) -and (-not $OverrideExceptionMessage)) {
        $baseMessage += " | $($ErrorRecord[0].Exception.Message)"
        $newMessage += " | $($ErrorRecord[0].Exception.Message)"
        $newColoredMessage += " | $($ErrorRecord[0].Exception.Message)"
    }

    #region Handle Input Objects
    if ($Target) {
        $targetType = $Target.GetType().FullName

        switch ($targetType) {
            "Sqlcollaborative.Dbatools.Parameter.DbaInstanceParameter" { $targetToAdd = $Target.InstanceName }
            "Microsoft.SqlServer.Management.Smo.Server" { $targetToAdd = ([Sqlcollaborative.Dbatools.Parameter.DbaInstanceParameter]$Target).InstanceName }
            default { $targetToAdd = $Target}
        }
        if ($targetToAdd.GetType().FullName -like "Microsoft.SqlServer.Management.Smo.*") { $targetToAdd = $targetToAdd.ToString() }
    }
    #endregion Handle Input Objects

    #region Handle Errors
    if ($ErrorRecord -and ((Get-PSCallStack)[1].Command -ne "Stop-Function")) {
        foreach ($record in $ErrorRecord) {
            $Exception = New-Object System.Exception($Message, $record.Exception)
            $newRecord = New-Object System.Management.Automation.ErrorRecord($Exception, "dbatools_$FunctionName", $record.CategoryInfo.Category, $targetToAdd)

            if ($EnableException) { Write-Error -Message $newRecord -Category $record.CategoryInfo.Category -TargetObject $targetToAdd -Exception $Exception -ErrorId "dbatools_$FunctionName" -ErrorAction Continue }
            else { $null = Write-Error -Message $newRecord -Category $record.CategoryInfo.Category -TargetObject $targetToAdd -Exception $Exception -ErrorId "dbatools_$FunctionName" -ErrorAction Continue 2>&1 }
        }
    }
    if ($ErrorRecord) {
        [Sqlcollaborative.Dbatools.dbaSystem.DebugHost]::WriteErrorEntry($ErrorRecord, $FunctionName, $timestamp, $baseMessage, $Host.InstanceId)
    }
    #endregion Handle Errors

    $channels = @()

    #region Warning Mode
    if ($Warning -or ($Level -like "Warning")) {
        if (-not $EnableException) {
            if ($PSBoundParameters.ContainsKey("Once")) {
                $OnceName = "MessageOnce.$FunctionName.$Once"

                if (-not (Get-DbaConfigValue -Name $OnceName)) {
                    Write-Warning $newMessage
                    Set-DbaConfig -Name $OnceName -Value $True -Hidden -EnableException -ErrorAction Ignore
                }
            }
            else {
                Write-Warning $newMessage
            }
            $channels += "Warning"
        }
        elseif ($developerMode) {
            Write-Host $newMessage -ForegroundColor $dev_color
        }

        Write-Debug $newMessage
        $channels += "Debug"
    }
    #endregion Warning Mode

    #region Message Mode
    else {
        if ((-not $EnableException) -and ($max_info -ge $Level) -and ($min_info -le $Level)) {
            if ($PSBoundParameters.ContainsKey("Once")) {
                $OnceName = "MessageOnce.$FunctionName.$Once"

                if (-not (Get-DbaConfigValue -Name $OnceName)) {
                    Write-HostColor -String $newColoredMessage -DefaultColor $info_color -ErrorAction Ignore
                    Set-DbaConfig -Name $OnceName -Value $True -Hidden -EnableException -ErrorAction Ignore
                }
            }
            else {
                Write-HostColor -String $newColoredMessage -DefaultColor $info_color -ErrorAction Ignore
            }
            $channels += "Information"
        }
        elseif ($developerMode) {
            Write-Host -Object $newMessage -ForegroundColor $dev_color
        }

        if (($max_verbose -ge $Level) -and ($min_verbose -le $Level)) {
            Write-Verbose $newMessage
            $channels += "Verbose"
        }

        if (($max_debug -ge $Level) -and ($min_debug -le $Level)) {
            Write-Debug $newMessage
            $channels += "Debug"
        }
    }
    #endregion Message Mode

    $channel_Result = $channels -join ", "
    if ($channel_Result) {
        [Sqlcollaborative.Dbatools.dbaSystem.DebugHost]::WriteLogEntry($Message, $channel_Result, $timestamp, $FunctionName, $Level, $Host.InstanceId, $targetToAdd)
    }
    else {
        [Sqlcollaborative.Dbatools.dbaSystem.DebugHost]::WriteLogEntry($Message, "None", $timestamp, $FunctionName, $Level, $Host.InstanceId, $targetToAdd)
    }
}