Обработка ошибок и логирование в PowerShell: как писать скрипты, которые не падают молча
Меня зовут Семёнов Евгений Сергеевич, я директор АйТи Фреш. За 15+ лет сопровождения Windows-инфраструктур я прочитал столько чужих PowerShell-скриптов, что выработал рефлекс: первое, что смотрю — как автор обработал ошибки и ведёт логи. Потому что 80% «проблем с автоматизацией» — это не ошибка в логике, а скрипт, который упал молча, на каком-то нестандартном узле, и никто этого не заметил. В этой статье — всё, что я сам применяю в боевых скриптах: от try/catch до структурных JSON-логов через PSFramework.
Типы ошибок в PowerShell
PowerShell различает два типа ошибок:
- Терминирующие (Terminating) — прерывают выполнение. Например, синтаксическая ошибка, деление на ноль, throw. Их ловит try/catch.
- Нетерминирующие (Non-terminating) — пишутся в поток Error и продолжают выполнение. Большинство командлетов — Get-ChildItem, Invoke-WebRequest, Copy-Item — по умолчанию кидают нетерминирующие.
Главная ловушка новичка: писать 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
Три разных способа сообщить об ошибке:
- Write-Error "текст" — нетерминирующая ошибка, попадает в $Error и в поток Error. По умолчанию не ломает выполнение. Используется в функциях, которые обрабатывают коллекции — одна плохая строка не должна валить весь batch.
- throw "текст" — терминирующая ошибка, пробрасывается вверх. Объект ErrorRecord создаётся автоматически. Простой способ прервать выполнение.
- $PSCmdlet.ThrowTerminatingError($errorRecord) — правильный способ в advanced-функциях. Отличается тем, что стек трейса указывает на вызывающего, а не на место throw внутри.
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 потоков:
- Success (1) — основной выход, пайплайн.
- Error (2) — ошибки через Write-Error / $Error.
- Warning (3) — Write-Warning.
- Verbose (4) — Write-Verbose, видно при -Verbose.
- Debug (5) — Write-Debug, для отладки.
- Information (6) — Write-Information, с PS 5.0.
- 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. Варианты:
- Windows Event Forwarding (WEF) — встроен в Windows, настраивается через GPO. Собирает события в один коллектор. Бесплатно, без агентов, но ограниченно для структурированных данных.
- Grafana Loki + Promtail — разворачивается на Linux, лёгкий, JSON-логи индексируются по меткам. Мой любимый stack последние два года.
- Elastic Stack (ELK) — тяжеловес, много возможностей, но требует обслуживания.
- Splunk — если деньги не проблема и нужен enterprise-продукт.
У нас на практике Loki + Promtail с 2–3 метками (host, script, level) покрывает 90% потребностей. 50 ГБ логов за 30 дней занимают около 5 ГБ в Loki благодаря компрессии.
Чек-лист качественного скрипта
Перед тем как пустить скрипт в продакшен, я прохожу по списку:
- В шапке —
$ErrorActionPreference = 'Stop'иSet-StrictMode -Version Latest. - Основная логика обёрнута в try/catch с конкретными типами исключений.
- Write-PSFMessage на старт, на ключевые этапы, на завершение.
- Ошибки пишутся в EventLog с уникальным EventId (удобно для алертов).
- Alert в Telegram/Slack на любой уровень Warning+.
- Код возврата при ошибке — exit 1, при успехе — exit 0.
- Секреты из SecretManagement, а не в коде.
- Timeout на внешние вызовы (Invoke-WebRequest, Invoke-Command).
- Ротация логов — штатно через PSFramework.
- Документированная шапка: Synopsis, Description, Examples.
Проведём аудит ваших 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 для локализации.