Chapter 10Lesson 05~155 minutes

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.

QuestionUseful 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 -Force

The 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-PSBreakpoint
VS Code: For PowerShell 6+ development, Visual Studio Code with the PowerShell extension provides a graphical debugging experience built around the same core ideas: breakpoints, stepping, variables, and call-stack inspection.

4. 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 -Off
Diagnostic scope: Broad tracing can expose values and produce noisy logs. Prefer the smallest reproducer and disable tracing after the investigation.

5. 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 -Force
Privacy and secrets: Transcripts can capture commands and displayed values. Never assume secret-bearing operations are safe to transcribe. Protect transcript files with appropriate access controls and retention rules.

7. 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 -Compress

You 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 ' -Verbose

Hypothesis: 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 -Name binds; do not confuse binding evidence with the later logic bug.
  • Refactor the condition to check $normalized and explain why the fix addresses the evidence.

11. Common mistakes and the underlying model

MistakeWhy it slows diagnosisBetter pattern
Add random Write-Host lines everywhereChanges output/noise without a hypothesisState hypothesis, collect targeted evidence
Leave breakpoints or Set-PSDebug tracing activeFuture runs stop or become noisy unexpectedlyRemove/disable diagnostics after investigation
Use transcripts as structured telemetryTranscript is a human session recordEmit structured log events for machine analysis
Log entire secret-bearing objectsDiagnostics become a data-exposure channelLog 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

Next chapter

Move into CSV, JSON, XML, CLIXML, YAML, and configuration data in Chapter 11

Chapter 11 moves from runtime behavior to structured data exchange: CSV, JSON, XML, CLIXML, YAML, serialization, configuration formats, and data fidelity.

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.