· 16 мин чтения

Обработка ошибок и логирование в PowerShell: как писать скрипты, которые не падают молча

Обработка ошибок и логирование в PowerShell: как писать скрипты, которые не падают молча

Меня зовут Семёнов Евгений Сергеевич, я директор АйТи Фреш. За 15+ лет сопровождения Windows-инфраструктур я прочитал столько чужих PowerShell-скриптов, что выработал рефлекс: первое, что смотрю — как автор обработал ошибки и ведёт логи. Потому что 80% «проблем с автоматизацией» — это не ошибка в логике, а скрипт, который упал молча, на каком-то нестандартном узле, и никто этого не заметил. В этой статье — всё, что я сам применяю в боевых скриптах: от try/catch до структурных JSON-логов через PSFramework.

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

PowerShell различает два типа ошибок:

Главная ловушка новичка: писать try/catch вокруг Get-ChildItem и ждать, что catch сработает на несуществующий путь. Не сработает. Нужно либо -ErrorAction Stop на команде, либо $ErrorActionPreference = 'Stop' на весь скрипт.

ErrorAction и ErrorActionPreference

ЗначениеПоведениеКогда использовать
ContinueПишет ошибку и продолжает — дефолтДля интерактивной работы
StopПревращает в терминирующую — можно ловить try/catchПродакшен-скрипты
SilentlyContinueГлотает ошибку без звукаКогда вы 100% готовы к отсутствию объекта
IgnoreНе пишет даже в $ErrorПочти никогда
InquireСпрашивает пользователяТолько интерактивные скрипты

Я всегда начинаю боевой скрипт со строки $ErrorActionPreference = 'Stop' и Set-StrictMode -Version Latest. Это превращает PowerShell из «сам думаю, как поступить» в предсказуемый инструмент.

Try/Catch/Finally — основной инструмент

function Copy-ReportFile {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Source,
        [Parameter(Mandatory)][string]$Destination
    )
    try {
        if (-not (Test-Path $Source)) {
            throw [System.IO.FileNotFoundException]::new("Источник не найден: $Source")
        }
        Copy-Item -Path $Source -Destination $Destination -Force -ErrorAction Stop
        Write-Verbose "OK: $Source -> $Destination"
    }
    catch [System.IO.FileNotFoundException] {
        Write-Warning $_.Exception.Message
        return $false
    }
    catch [System.UnauthorizedAccessException] {
        Write-Warning "Нет прав на $Destination"
        return $false
    }
    catch {
        Write-Error "Непредвиденная ошибка: $($_.Exception.Message)" -ErrorAction Continue
        Write-Error $_.ScriptStackTrace -ErrorAction Continue
        throw
    }
    finally {
        Write-Verbose "Завершение операции копирования"
    }
    return $true
}

Разберу по частям. Специфичные catch-блоки (по типу исключения) — идут первыми, общий catch — последним. В finally — то, что должно выполниться всегда: закрытие соединений, освобождение ресурсов, запись окончания в лог. Операция throw в общем catch пробрасывает ошибку выше — это позволяет верхнему уровню тоже среагировать.

Write-Error, throw и $PSCmdlet.ThrowTerminatingError

Три разных способа сообщить об ошибке:

function Get-Config {
    [CmdletBinding()]
    param([Parameter(Mandatory)][string]$Path)
    if (-not (Test-Path $Path)) {
        $ex  = [System.IO.FileNotFoundException]::new("Config not found: $Path")
        $rec = [System.Management.Automation.ErrorRecord]::new(
            $ex, 'ConfigNotFound', 'ObjectNotFound', $Path)
        $PSCmdlet.ThrowTerminatingError($rec)
    }
    Get-Content $Path -Raw | ConvertFrom-Json
}

Потоки (streams) в PowerShell

PowerShell имеет 7 потоков:

  1. Success (1) — основной выход, пайплайн.
  2. Error (2) — ошибки через Write-Error / $Error.
  3. Warning (3) — Write-Warning.
  4. Verbose (4) — Write-Verbose, видно при -Verbose.
  5. Debug (5) — Write-Debug, для отладки.
  6. Information (6) — Write-Information, с PS 5.0.
  7. Progress — Write-Progress (отдельно, не перенаправляется).

Перенаправление: script.ps1 *> all.log пишет всё в файл, 2>&1 сливает Error в Success для захвата в переменную. Пример полного логирования:

.\deploy.ps1 -Verbose 4>deploy.verbose.log 2>deploy.error.log 3>deploy.warn.log

Structured logging через PSFramework

Транскрипт годится для коротких скриптов. Для продакшена — модуль PSFramework от Фридриха Вайнмана. Он даёт уровни логов, ротацию, разные провайдеры (file, EventLog, Splunk, Syslog), структурированные поля.

Install-Module PSFramework -Scope CurrentUser

# В начале скрипта
Set-PSFLoggingProvider -Name logfile -InstanceName daily -FilePath 'C:\Logs\daily-%date%.log' `
    -Enabled $true -FileType CMTrace
Set-PSFLoggingProvider -Name eventlog -Enabled $true

Write-PSFMessage -Level Host -Message "Start daily maintenance"
try {
    Write-PSFMessage -Level Verbose -Message "Processing server {0}" -StringValues 'srv-db01'
    # работа
} catch {
    Write-PSFMessage -Level Warning -Message "Fail on {0}: {1}" `
        -StringValues 'srv-db01',$_ -ErrorRecord $_
}
Wait-PSFMessage   # flush буфера перед выходом

Файлы пишутся в формате CMTrace или JSON, которые открываются в Config Manager Trace Log Tool или импортируются в Grafana Loki одним фильтром.

Мини-кейс: ночные скрипты без контроля

Март 2025. Клиент — логистика в Подмосковье, 22 сервера. У них ночью по расписанию запускалось 14 PowerShell-скриптов: бэкапы SQL, проверки AD, ротация логов, экспорт 1С в XLS, синхронизация с контрагентами. Жалоба: периодически утром обнаруживаются последствия — бэкап не сделан за два дня, файл из 1С не выгрузился. Причина — в скриптах было «try/catch» без ErrorAction Stop, ошибки нетерминирующие молчали, а успешное завершение скрипта отчитывалось как «всё хорошо».

За три дня прошли по всем скриптам, добавили $ErrorActionPreference = 'Stop', try/catch вокруг ключевых операций, логирование через PSFramework в файл + EventLog + алерт в Telegram на любой уровень Warning и выше. Сервер автоматизации — Dell PowerEdge R740 с Xeon Platinum 8280 и NVMe под логи, 40G Mellanox до файлового хранилища. За первую неделю поймали три реальных инцидента, которые раньше проходили незамеченными: сертификат истёк на SMB, учётка заблокирована, диск на 98% на одном сервере. Все три починили до того, как пользователи заметили. Стоимость работ 38 тыс. руб.

Централизованный сбор логов

Локальные логи полезны, но когда у вас 20+ серверов — нужен центральный stack. Варианты:

У нас на практике Loki + Promtail с 2–3 метками (host, script, level) покрывает 90% потребностей. 50 ГБ логов за 30 дней занимают около 5 ГБ в Loki благодаря компрессии.

Чек-лист качественного скрипта

Перед тем как пустить скрипт в продакшен, я прохожу по списку:

Проведём аудит ваших PowerShell-скриптов

Загляну в папку Scripts, найду скрипты, которые падают молча, перепишу с нормальным error handling и структурными логами, подниму централизованный сбор логов. Отлажу автоматизацию ночных задач на серверах 1С, AD, Exchange. Опыт 15+ лет с Windows-парками от 10 до 800 рабочих мест.

Телефон: +7 903 729-62-41
Telegram: @ITfresh_Boss
Семёнов Евгений Сергеевич, директор АйТи Фреш

FAQ — частые вопросы по обработке ошибок

Почему мой catch не ловит ошибку?
Потому что ошибка нетерминирующая. Добавьте -ErrorAction Stop к проблемной команде или на весь скрипт $ErrorActionPreference = 'Stop'. Без этого catch увидит только терминирующие ошибки.
Чем throw отличается от Write-Error?
throw всегда выбрасывает терминирующую ошибку и прерывает выполнение. Write-Error по умолчанию пишет в поток Error, но не прерывает — нужна -ErrorAction Stop. throw — для контроля потока выполнения, Write-Error — для информирования.
Как записать ошибку в Windows Event Log?
Создайте source через New-EventLog -LogName Application -Source 'PS-Scripts' и пишите через Write-EventLog -LogName Application -Source 'PS-Scripts' -EntryType Error -EventId 1001 -Message "текст ошибки". Source регистрируется один раз на машине.
Нужен ли Start-Transcript?
Для быстрых скриптов достаточно, для продакшена — слабо. Transcript пишет всё подряд без структуры. Используйте PSFramework, NLog или свой писатель JSON-логов с timestamp, level, message, context.
Как найти полный стек ошибки?
$Error[0] | Format-List -Force покажет ScriptStackTrace, InvocationInfo.PositionMessage, InnerException. В catch используйте $_.ScriptStackTrace и $_.InvocationInfo.ScriptName для локализации.

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

Раз в неделю — практические гайды для руководителя IT и сисадмина: безопасность, 1С, миграции, резервные копии, лайфхаки из реальных проектов.

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

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