Обработка ошибок и логирование в 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' в начале скрипта. Это самая частая ошибка при написании PowerShell-скриптов.

Используйте 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.

Используйте параметры таймаута и перехватывайте конкретный тип исключения:

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

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

загрузка...