Debugging, Breakpoints, Trace-Command, Transcripts, and Diagnostic Logging
Finish Chapter 10 with hypothesis-driven debugging, breakpoints, Trace-Command, transcripts, and structured logs that preserve useful evidence without leaking secrets.
Learning objectives
- Use hypothesis-driven debugging instead of random console prints.
- Configure and manage PowerShell breakpoints safely.
- Inspect variables and call stacks and understand the VS Code debugging model.
- Use Set-PSDebug sparingly and Trace-Command for narrow engine diagnostics.
- Use transcripts with explicit privacy and retention warnings.
- Emit structured timestamped log events with levels, correlation IDs, and safe machine-readable fields.
1. Debugging is a sequence of hypotheses and evidence
Randomly adding Write-Host until a bug disappears is not a diagnostic method. Start with a falsifiable hypothesis: “the wrong configuration value reaches this function,” “parameter binding chooses the wrong parameter,” or “the loop mutates state one iteration too early.” Then collect the smallest evidence that can confirm or reject that hypothesis.
| Question | Useful evidence |
|---|---|
| What input reached this line? | Breakpoint + variable inspection |
| How did parameters bind? | Trace-Command -Name ParameterBinding |
| Which call path reached the failure? | Call stack / ErrorRecord ScriptStackTrace |
| What happened across a longer session? | Transcript or structured log |
| Can events across components be correlated? | Timestamp + correlation ID + structured fields |
2. Breakpoints pause execution at a precise line, command, or variable event
Set-PSBreakpoint can stop a script on a line, command, or variable access/write. When the breakpoint is hit, the debugger lets you inspect current variables and the call stack before continuing.
$script = Join-Path ([System.IO.Path]::GetTempPath()) 'breakpoint-demo.ps1'
@'
param([int]$Count = 3)
$total = 0
for ($i = 0; $i -lt $Count; $i++) {
$total += $i
}
$total
'@ | Set-Content -LiteralPath $script -Encoding utf8
Set-PSBreakpoint -Script $script -Variable total -Mode Write
Get-PSBreakpoint
# Run interactively when you are ready to enter the debugger:
# & $script -Count 3
Remove-PSBreakpoint -Breakpoint (Get-PSBreakpoint)
Remove-Item -LiteralPath $script -ForceThe commented invocation avoids unexpectedly dropping an automated lab runner into an interactive debugger. Run it manually in a terminal when practicing.
3. Inspect values and the call stack before changing code
At a breakpoint, inspect the variables that matter and use the call stack to understand how execution arrived there. This is faster than adding permanent diagnostic output everywhere and later forgetting to remove it.
# Useful debugger-oriented commands:
Get-PSCallStack
Get-Variable i,total,Count -ErrorAction SilentlyContinue
# Breakpoint lifecycle:
Get-PSBreakpoint
Get-PSBreakpoint | Disable-PSBreakpoint
Get-PSBreakpoint | Enable-PSBreakpoint
Get-PSBreakpoint | Remove-PSBreakpoint4. Set-PSDebug is powerful but intentionally blunt
Set-PSDebug -Trace can trace script lines/commands and -Strict can enable strict checking modes. It affects a broad execution scope and can generate substantial output, so use it as a targeted diagnostic instrument rather than leaving it enabled in ordinary production runs.
# Run these interactively when diagnosing a small script.
Set-PSDebug -Trace 1
# ... execute the narrow reproduction ...
Set-PSDebug -Off5. Trace-Command exposes internal diagnostic sources such as parameter binding
When the symptom suggests an engine-level decision—especially parameter binding—Trace-Command can reveal details that ordinary output does not show. It configures tracing only around the specified expression/command.
Trace-Command -Name ParameterBinding -PSHost -Expression {
'powershell' | Get-Process -ErrorAction SilentlyContinue
}The trace is intentionally verbose. Use it after simpler checks have narrowed the question to binding behavior. PowerShell 7.3 also extended native-command parameter-binding tracing, which can help at cross-tool boundaries.
6. Transcripts preserve a human-readable session record
Start-Transcript records all or part of a PowerShell session to a text file until Stop-Transcript or session termination. A transcript can be invaluable for reproducing operator steps, but it is not a structured telemetry system.
$transcript = Join-Path ([System.IO.Path]::GetTempPath()) 'ps-session-transcript.txt'
Start-Transcript -Path $transcript -Force
Get-Date
$PSVersionTable.PSVersion
Stop-Transcript
Get-Item -LiteralPath $transcript | Select-Object FullName,Length
# Inspect manually, then clean up:
Remove-Item -LiteralPath $transcript -Force7. Production logs should carry fields, not only prose
A machine-readable log event should answer at least when, what level, which operation, and which correlation identifier. A correlation ID lets you connect events belonging to one deployment or request across functions and systems.
function New-LogEvent {
param(
[ValidateSet('Debug','Info','Warning','Error')]
[string]$Level,
[string]$Message,
[string]$CorrelationId,
[hashtable]$Data = @{}
)
[pscustomobject]@{
TimestampUtc = [DateTimeOffset]::UtcNow
Level = $Level
Message = $Message
CorrelationId = $CorrelationId
Data = $Data
}
}
New-LogEvent -Level Info -Message 'validation started' -CorrelationId 'DEPLOY-42' -Data @{ Target='api-01' } |
ConvertTo-Json -CompressYou can render the same object for humans or serialize it for a log collector without changing the underlying event contract.
8. Log error context without flattening it too early
try {
Get-Content -LiteralPath './missing-config.json' -ErrorAction Stop
}
catch {
New-LogEvent -Level Error -Message 'configuration load failed' -CorrelationId 'DEPLOY-42' -Data @{
ErrorId = $_.FullyQualifiedErrorId
ExceptionType = $_.Exception.GetType().FullName
Category = [string]$_.CategoryInfo.Category
Target = [string]$_.TargetObject
} | ConvertTo-Json -Compress
}The operator-facing message gives context; the fields preserve searchable evidence. Be selective: do not serialize entire arbitrary objects or secret-bearing request payloads into logs.
9. Troubleshoot from observable evidence, not from a guess
The following function has a subtle logic bug: it checks the wrong variable after computing a normalized value. Build a hypothesis, inspect state, and prove the cause.
function Test-DeploymentName {
param([string]$Name)
$normalized = $Name.Trim().ToLowerInvariant()
Write-Verbose "normalized=$normalized"
# BUG: checks the original value, not the normalized one.
if ($Name -match '^[a-z0-9-]+$') {
return $true
}
return $false
}
Test-DeploymentName -Name ' API-01 ' -VerboseHypothesis: normalization is correct but the condition evaluates the unnormalized input. Evidence: Verbose output or a breakpoint shows the two variables differ. Fix: test $normalized, then add a regression test later in the course.
10. Lab: diagnose a broken script from layered evidence
Use a disposable script and three evidence sources: Verbose output, breakpoint metadata, and a ParameterBinding trace. Do not modify system state.
$script = Join-Path ([System.IO.Path]::GetTempPath()) 'diagnostic-lab.ps1'
@'
[CmdletBinding()]
param([string]$Name)
$normalized = $Name.Trim().ToLowerInvariant()
Write-Verbose "normalized=$normalized"
if ($Name -match '^[a-z0-9-]+$') { $true } else { $false }
'@ | Set-Content -LiteralPath $script -Encoding utf8
# Evidence 1: controlled diagnostic stream
& $script -Name ' API-01 ' -Verbose
# Evidence 2: configure, inspect, then remove a breakpoint
$bp = Set-PSBreakpoint -Script $script -Variable normalized -Mode Write
Get-PSBreakpoint | Select-Object Id,Script,Variable,AccessMode
Remove-PSBreakpoint -Breakpoint $bp
# Evidence 3: engine parameter-binding trace around invocation
Trace-Command -Name ParameterBinding -PSHost -Expression {
& $script -Name ' API-01 '
}
Remove-Item -LiteralPath $script -Force- State the hypothesis before collecting evidence.
- Use the Verbose message to compare raw and normalized values.
- Explain what a variable-write breakpoint would let you inspect interactively.
- Use Trace-Command only to verify how
-Namebinds; do not confuse binding evidence with the later logic bug. - Refactor the condition to check
$normalizedand explain why the fix addresses the evidence.
11. Common mistakes and the underlying model
| Mistake | Why it slows diagnosis | Better pattern |
|---|---|---|
| Add random Write-Host lines everywhere | Changes output/noise without a hypothesis | State hypothesis, collect targeted evidence |
| Leave breakpoints or Set-PSDebug tracing active | Future runs stop or become noisy unexpectedly | Remove/disable diagnostics after investigation |
| Use transcripts as structured telemetry | Transcript is a human session record | Emit structured log events for machine analysis |
| Log entire secret-bearing objects | Diagnostics become a data-exposure channel | Log minimum safe fields and protect retention/access |
12. Knowledge check
Question 1. What should come before choosing a debugging tool?
Question 2. What can Set-PSBreakpoint target?
Question 3. What is Trace-Command useful for in this chapter?
Question 4. What is the primary purpose of a transcript?
Question 5. Why add a correlation ID to structured logs?
13. Summary
Reliable diagnostics begin with a hypothesis. Use breakpoints and debugger inspection for live state, Trace-Command for narrow engine-level evidence such as binding, Set-PSDebug sparingly for broad tracing, and transcripts for human session records. Production logging should be structured, timestamped, leveled, correlated, and careful about secrets. The goal is not more output—it is enough trustworthy evidence to explain what happened and why.
14. Further reading
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this address.