Обработка ошибок и логирование в PowerShell-скриптах — АйТи Фреш

Обработка ошибок и логирование в PowerShell-скриптах

Типы ошибок в PowerShell

PowerShell делит ошибки на два типа. Звучит просто, но именно непонимание этого различия ломает скрипты чаще всего.

Terminating errors (завершающие) — полностью останавливают выполнение команды или скрипта. Деление на ноль, вызов несуществующего метода, исключения .NET — всё это сюда. Перехватываются блоком try/catch.

Non-terminating errors (незавершающие) — скрипт выводит ошибку и спокойно идёт дальше. Типичные примеры: файл не найден в Get-Item, нет доступа к одному из файлов в списке. По умолчанию try/catch их не видит вообще.

Вот откуда берётся классическая головная боль: администратор честно обернул всё в try/catch, но ошибки всё равно проскакивают мимо — потому что они non-terminating.

# Эта ошибка НЕ будет перехвачена!
try {
    Get-Item 'C:\nonexistent.txt'  # Non-terminating error
} catch {
    Write-Host 'Ошибка перехвачена'  # Никогда не выполнится
}

Лечится это одним параметром — -ErrorAction Stop. Он буквально конвертирует non-terminating ошибки в terminating:

# Теперь ошибка будет перехвачена
try {
    Get-Item 'C:\nonexistent.txt' -ErrorAction Stop
} catch {
    Write-Host "Перехвачено: $($_.Exception.Message)"
}

Обработка ошибок: try/catch/finally

Блок try/catch/finally — это основа основ при обработке ошибок в PowerShell. Всё остальное строится вокруг него.

Базовый синтаксис

try {
    # Код, который может вызвать ошибку
    $content = Get-Content 'C:\config.txt' -ErrorAction Stop
    $data = $content | ConvertFrom-Json
}
catch [System.IO.FileNotFoundException] {
    # Обработка конкретного типа ошибки
    Write-Warning "Файл конфигурации не найден: $($_.Exception.Message)"
    $data = @{}  # Значение по умолчанию
}
catch [System.Management.Automation.RuntimeException] {
    Write-Warning "Ошибка парсинга JSON: $($_.Exception.Message)"
    exit 1
}
catch {
    # Обработка всех остальных ошибок
    Write-Error "Неожиданная ошибка: $($_.Exception.GetType().FullName)"
    Write-Error $_.Exception.Message
    Write-Error $_.ScriptStackTrace
    exit 1
}
finally {
    # Выполняется ВСЕГДА — для очистки ресурсов
    if ($connection) { $connection.Close() }
}

Внутри блока catch живёт объект $_. Он содержит всё, что нужно для диагностики:

  • $_.Exception — объект исключения .NET
  • $_.Exception.Message — текст ошибки
  • $_.ScriptStackTrace — стек вызовов PowerShell
  • $_.InvocationInfo.ScriptLineNumber — номер строки, где всё сломалось
  • $_.TargetObject — объект, который спровоцировал ошибку

ErrorAction и $ErrorActionPreference

Параметр -ErrorAction управляет поведением при ошибке точечно — для конкретной команды, не затрагивая остальной скрипт:

# Stop — превращает в terminating (перехватывается try/catch)
Get-Service 'FakeService' -ErrorAction Stop

# SilentlyContinue — подавляет вывод, продолжает выполнение
Get-Service 'FakeService' -ErrorAction SilentlyContinue

# Continue — выводит ошибку, продолжает (по умолчанию)
Get-Service 'FakeService' -ErrorAction Continue

# Inquire — спрашивает пользователя
Remove-Item 'C:\important' -ErrorAction Inquire

Если хочется задать поведение сразу для всего скрипта — есть глобальная переменная $ErrorActionPreference:

# В начале скрипта — все ошибки станут terminating
$ErrorActionPreference = 'Stop'

# Теперь try/catch перехватит любую ошибку
try {
    Get-Item 'C:\nonexistent.txt'
    Get-Service 'FakeService'
} catch {
    Write-Error "Ошибка: $_"
}

Из личного опыта: ставьте $ErrorActionPreference = 'Stop' первой строкой в каждом продакшен-скрипте. Буквально первой. Это одна строка, которая спасёт вас от нескольких часов отладки.

Логирование: простые подходы

В PowerShell есть несколько встроенных способов вести логи. Разберём каждый честно — с плюсами и минусами.

Start-Transcript

Start-Transcript — проще не придумаешь. Всё, что идёт в консоль, пишется в файл. Никаких зависимостей, работает из коробки:

$LogFile = "C:\Logs\script-$(Get-Date -Format 'yyyyMMdd-HHmmss').log"
Start-Transcript -Path $LogFile -Append

Write-Host "Скрипт запущен: $(Get-Date)"
# ... код скрипта ...

Stop-Transcript

Плюсы очевидны: нулевой порог входа, ничего настраивать не нужно. Минусы — тоже реальные: нет уровней логирования (DEBUG/INFO/WARN/ERROR), а в лог попадает много лишнего мусора, который потом мешает читать.

Функция Write-Log

На практике мы почти всегда пишем свою функцию логирования. Вот универсальный вариант, который работает у нас годами:

function Write-Log {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [string]$Message,

        [ValidateSet('INFO','WARN','ERROR','DEBUG')]
        [string]$Level = 'INFO',

        [string]$LogFile = $script:LogFile
    )

    $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
    $caller = (Get-PSCallStack)[1]
    $line = $caller.ScriptLineNumber
    $func = $caller.FunctionName

    $entry = "[$timestamp] [$Level] [$func:$line] $Message"

    # Вывод в файл
    Add-Content -Path $LogFile -Value $entry -Encoding UTF8

    # Вывод в консоль с цветом
    switch ($Level) {
        'ERROR' { Write-Host $entry -ForegroundColor Red }
        'WARN'  { Write-Host $entry -ForegroundColor Yellow }
        'DEBUG' { Write-Host $entry -ForegroundColor Gray }
        default { Write-Host $entry }
    }
}

Использование:

$script:LogFile = "C:\Logs\backup-$(Get-Date -Format 'yyyyMMdd').log"

Write-Log 'Запуск скрипта бэкапа'
Write-Log 'Подключение к серверу...' -Level DEBUG

try {
    Copy-Item 'D:\Data' 'E:\Backup\Data' -Recurse -ErrorAction Stop
    Write-Log 'Бэкап выполнен успешно'
} catch {
    Write-Log "Ошибка бэкапа: $($_.Exception.Message)" -Level ERROR
}

Продвинутое логирование

Для продакшена хочется чего-то понадёжнее текстового файла. Вот два подхода, которые мы реально применяем.

Логирование в Windows Event Log

Запись в журнал событий Windows — это интеграция с Zabbix, SCOM и другими системами мониторинга из коробки. Ничего дополнительно настраивать не нужно:

# Создание источника (один раз, от администратора)
New-EventLog -LogName Application -Source 'BackupScript'

# Запись событий
Write-EventLog -LogName Application -Source 'BackupScript' `
  -EventId 1000 -EntryType Information `
  -Message 'Бэкап завершён успешно. Размер: 15.2 ГБ'

Write-EventLog -LogName Application -Source 'BackupScript' `
  -EventId 1001 -EntryType Error `
  -Message "Ошибка бэкапа: Недостаточно места на диске E:\"

Просмотр записей:

Get-EventLog -LogName Application -Source 'BackupScript' -Newest 10

Ротация лог-файлов

Логи растут. Через полгода без ротации диск может неприятно удивить — добавьте автоматическую очистку:

function Initialize-Logging {
    param(
        [string]$LogDir = 'C:\Logs',
        [string]$ScriptName = $MyInvocation.ScriptName,
        [int]$RetainDays = 30
    )

    if (-not (Test-Path $LogDir)) {
        New-Item -Path $LogDir -ItemType Directory -Force | Out-Null
    }

    # Ротация: удаление логов старше N дней
    Get-ChildItem $LogDir -Filter '*.log' |
        Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$RetainDays) } |
        Remove-Item -Force

    # Имя лог-файла
    $baseName = [System.IO.Path]::GetFileNameWithoutExtension($ScriptName)
    $script:LogFile = Join-Path $LogDir "${baseName}-$(Get-Date -Format 'yyyyMMdd').log"

    Write-Log "=== Сессия логирования начата ==="
    Write-Log "Скрипт: $ScriptName"
    Write-Log "Пользователь: $env:USERNAME@$env:COMPUTERNAME"
    Write-Log "PowerShell: $($PSVersionTable.PSVersion)"
}

Паттерн: устойчивый скрипт

Собираем всё вместе. Вот шаблон, от которого мы отталкиваемся при написании любого продакшен-скрипта:

Шаблон продакшен-скрипта

#Requires -Version 5.1
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

# === Логирование ===
$script:LogFile = "C:\Logs\maintenance-$(Get-Date -Format 'yyyyMMdd-HHmmss').log"
if (-not (Test-Path 'C:\Logs')) { New-Item -Path 'C:\Logs' -ItemType Directory | Out-Null }

function Write-Log {
    param([string]$Message, [string]$Level = 'INFO')
    $entry = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
    Add-Content -Path $script:LogFile -Value $entry
    switch ($Level) {
        'ERROR' { Write-Host $entry -ForegroundColor Red }
        'WARN'  { Write-Host $entry -ForegroundColor Yellow }
        default { Write-Host $entry }
    }
}

# === Основная логика ===
try {
    Write-Log 'Начало обслуживания серверов'

    $servers = Get-Content 'C:\Config\servers.txt'
    $failed = @()

    foreach ($server in $servers) {
        try {
            Write-Log "Обработка $server..."

            $session = New-PSSession -ComputerName $server -ErrorAction Stop
            $result = Invoke-Command -Session $session -ScriptBlock {
                # Очистка temp
                $freed = (Get-ChildItem $env:TEMP -Recurse -ErrorAction SilentlyContinue |
                    Measure-Object -Property Length -Sum).Sum / 1MB
                Remove-Item "$env:TEMP\*" -Recurse -Force -ErrorAction SilentlyContinue
                [math]::Round($freed, 2)
            }

            Write-Log "$server: освобождено $result МБ"
            Remove-PSSession $session
        }
        catch {
            Write-Log "$server: ОШИБКА — $($_.Exception.Message)" -Level ERROR
            $failed += $server
        }
    }

    # Итог
    Write-Log "Обработано: $($servers.Count), ошибок: $($failed.Count)"
    if ($failed) {
        Write-Log "Проблемные серверы: $($failed -join ', ')" -Level WARN
        exit 1
    }
}
catch {
    Write-Log "КРИТИЧЕСКАЯ ОШИБКА: $($_.Exception.Message)" -Level ERROR
    Write-Log $_.ScriptStackTrace -Level ERROR
    exit 2
}
finally {
    Write-Log 'Скрипт завершён'
}

Отладка скриптов

Когда скрипт всё-таки ведёт себя непредсказуемо, помогают эти инструменты:

# Пошаговое выполнение
Set-PSDebug -Step

# Trace — показывает каждую выполняемую строку
Set-PSDebug -Trace 2

# Точки останова
Set-PSBreakpoint -Script 'C:\Scripts\myscript.ps1' -Line 42
Set-PSBreakpoint -Variable 'result' -Mode Write  # Останов при записи в переменную

# Просмотр стека ошибок
$Error[0] | Format-List * -Force
$Error[0].Exception.InnerException  # Вложенная ошибка
$Error[0].ScriptStackTrace          # Стек вызовов

Переменная $Error — массив последних ошибок сессии, по умолчанию хранит 256 штук. Удобно смотреть, что вообще происходило:

# Последняя ошибка
$Error[0]

# Очистить историю ошибок
$Error.Clear()

# Количество ошибок за сессию
$Error.Count

Для VSCode: поставьте расширение PowerShell — и получите нормальный отладчик. F5 запускает скрипт с отладкой, F9 ставит точку останова, F10 шагает по строкам не заходя в функции, F11 — заходя.

Часто задаваемые вопросы

Большинство командлетов PowerShell бросают non-terminating ошибки, которые try/catch просто игнорирует. Добавьте -ErrorAction Stop к команде или поставьте $ErrorActionPreference = 'Stop' в начало скрипта. Это, пожалуй, самая частая причина «почему catch не срабатывает» — мы разбираем такие случаи на каждом втором аудите клиентских скриптов.

Проще всего — Start-Transcript в начале скрипта. Или перенаправьте вывод при запуске: powershell -File script.ps1 *> C:\Logs\output.log. Оператор *> захватывает все потоки сразу: Output, Error, Warning, Verbose, Debug. Для Task Scheduler: в Actions укажите Program powershell.exe, в Arguments — -ExecutionPolicy Bypass -File C:\Scripts\script.ps1.

Уведомления об ошибках добавляются в блок catch или finally — в зависимости от того, нужно ли слать письмо только при падении или всегда:

catch {
    $body = "Скрипт: $($MyInvocation.MyCommand)\nОшибка: $($_.Exception.Message)\nСтек: $($_.ScriptStackTrace)"
    Send-MailMessage -From 'scripts@corp.local' -To 'admin@corp.local' -Subject 'Script Error' -Body $body -SmtpServer 'mail.corp.local'
}

На PowerShell 7+ лучше использовать REST API почтового сервиса — Send-MailMessage официально устарел и Microsoft не рекомендует его в новых скриптах.

Сетевые вызовы падают непредсказуемо. Добавьте таймаут и ловите конкретный тип исключения — так обработка ошибок будет точнее:

try {
    $session = New-PSSession -ComputerName $server -ErrorAction Stop
    Invoke-Command -Session $session -ScriptBlock { ... } -ErrorAction Stop
} catch [System.Management.Automation.Remoting.PSRemotingTransportException] {
    Write-Log "$server недоступен (таймаут)" -Level WARN
} catch {
    Write-Log "$server: $($_.Exception.Message)" -Level ERROR
} finally {
    if ($session) { Remove-PSSession $session }
}

Нужна помощь с настройкой?

Специалисты АйТи Фреш помогут с внедрением и настройкой — 15+ лет опыта, обслуживание от 15 000 ₽/мес

📞 Связаться с нами
#powershell обработка ошибок#powershell try catch#powershell логирование#powershell erroraction#powershell transcript#powershell скрипты ошибки#powershell log файл#powershell отладка
Комментарии 0

Оставить комментарий

загрузка...

Подпишитесь на рассылку ITfresh

Каждую неделю мы выпускаем практические гайды для руководителей IT и системных администраторов. Это не просто теория! Здесь вы найдёте всё: безопасность, 1С, миграции, резервные копии и проверенные лайфхаки из наших реальных проектов.

Реквизиты оператора персональных данных

ООО «АЙТИ-ФРЕШ», ИНН 7719418495, КПП 771901001. Юридический адрес: 105523, г. Москва, Щёлковское шоссе, д. 92, корп. 7. Контакт: info@itfresh.ru, +7 903 729-62-41. Оператор обрабатывает e-mail подписчика в целях рассылки информационных и рекламных материалов до момента отзыва согласия.