PowerShell на практике — безопасная автоматизация анализа логов, архивирования и отчётов
· Обновлено: · Го Комура · PowerShell, Windows, Автоматизация, Анализ логов, Эксплуатация, Использование существующих активов
История изменений (2 обновлений, последнее 30 Aug 2026)
Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.
- Комментарии в примере Get-ScheduledTask переведены на русский. Утверждения статьи не менялись.
- Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
- Первая публикация
Цитирование статьи(DOI (зарегистрированный архив): 10.5281/zenodo.21619867)
Приведённые ниже DOI относятся к ранее зарегистрированным архивным версиям, которые могут отличаться от текущего текста. Для ссылки на текущий текст используйте URL этой страницы.
Го Комура (2026). PowerShell на практике — безопасная автоматизация анализа логов, архивирования и отчётов. KomuraSoft LLC. https://comcomponent.com/ru/blog/2026/06/02/000-powershell-script-log-maintenance-automation/
- DOI (зарегистрированный архив)
- 10.5281/zenodo.21619867
- DOI (последняя зарегистрированная версия)
- 10.5281/zenodo.21619868
1. Сначала зафиксируйте рамки
В предыдущей статье мы разобрали базовые команды PowerShell, конвейер, вывод в CSV, JSON, скрипты .ps1 и безопасную проверку через -WhatIf. Здесь продолжаем ту же линию и пишем чуть более крупный скрипт, которым реально пользоваться в работе. Тема — такая эксплуатационная задача:
- Разобрать логи
- Собрать строки с ошибками в CSV
- Составить список старых логов как кандидатов на архивирование
- При необходимости переместить старые логи
- Оставить результаты запуска как артефакты
В прикладном PowerShell важно не удлинять команды, а выстроить такой поток:
- Отделить настройки
- Сделать этап только чтения
- Сохранить вывод
- Вынести изменяющие операции в функции
- Сделать пробный прогон через
-WhatIf - К автоматическому запуску переходить в последнюю очередь
Автоматизация — это не «убрать проверку человеком». Это зафиксировать, что именно нужно проверять, и сделать так, чтобы при каждом запуске оставались одни и те же артефакты.
Код из статьи опубликован на GitHub как готовый к запуску набор примеров: финальный скрипт, файл конфигурации, скрипт, который поднимает окружение с тестовыми логами, и тесты Pester на анализ, пробный прогон и перемещение.
powershell-script-log-maintenance-automation - komurasoft-blog-samples (GitHub)
Как эта статья стыкуется с двумя предыдущими
По PowerShell идут три связанные статьи. Это не нумерованная серия, но если читать в таком порядке, линия становится цельной.
| Порядок | Статья | Что закрывает |
|---|---|---|
| 1 | Основы команд PowerShell — операции, которые стоит выучить первыми, и как использовать их безопасно | Поиск команд, конвейер, CSV / JSON, .ps1, -WhatIf |
| 2 | Практические команды PowerShell — мелкие приёмы для повседневной работы | Блоки для агрегации, сравнения, выборки и записи артефактов |
| 3 | Эта статья | Собрать блоки в один эксплуатационный скрипт: настройки, артефакты, пробный прогон, регулярный запуск |
Из обязательного достаточно материала первой статьи. Вторую можно не читать — эту всё равно можно пройти. «Предыдущая статья» в начале как раз про первую.
На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 16, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle
2. Что получится на выходе
На этот раз пишем скрипт Invoke-LogMaintenance.ps1.
Основные возможности такие.
| Возможность | Что делает |
|---|---|
| Поиск логов | Ищет файлы .log в указанной папке и во вложенных |
| Ограничение по периоду | Разбирает только логи, изменённые за последние N дней |
| Выборка ошибок | Достаёт строки с ERROR, WARN, FATAL и т. п. |
| Вывод в CSV | Сохраняет результаты поиска в log-hits.csv |
| Список старых логов | Пишет логи старше N дней в archive-targets.csv |
| Архивирование | Перемещает старые логи в отдельную папку |
| Пробный прогон | При -Preview файлы не перемещаются, показывается только план |
| Запись запуска | Сохраняет transcript, summary JSON и результирующие CSV |
Удаления нет. В этой первой прикладной части мы останавливаемся на более безопасном «перемещении», а не на удалении.
3. Структура папок
Для примера возьмём такую раскладку.
C:\Ops
Invoke-LogMaintenance.ps1
log-maintenance.json
C:\App\Logs
app.log
batch.log
old
app-202401.log
C:\App\Reports
20260602-030000
log-hits.csv
archive-targets.csv
archive-result.csv
summary.json
transcript.txt
C:\App\Archive
20260602-030000
old
app-202401.log
Если тело скрипта и файл конфигурации разведены, проще подменять окружения. Можно менять только пути: в разработке C:\Test\Logs, в production — D:\App\Logs.
4. Файл конфигурации
Сначала создайте log-maintenance.json.
{
"LogPath": "C:\\App\\Logs",
"OutputPath": "C:\\App\\Reports",
"Days": 7,
"Patterns": [
"ERROR",
"WARN",
"FATAL"
],
"ArchiveDays": 90,
"ArchivePath": "C:\\App\\Archive"
}
Поля означают следующее.
| Параметр | Смысл |
|---|---|
LogPath |
Папка логов, которую нужно разобрать |
OutputPath |
Куда писать отчёты |
Days |
За сколько последних дней смотреть логи |
Patterns |
Строки и шаблоны для поиска |
ArchiveDays |
С какого возраста лог становится кандидатом на архивирование |
ArchivePath |
Папка назначения архива |
JSON позволяет менять условия, не правя тело скрипта.
В PowerShell ConvertFrom-Json превращает JSON в объект. Обратно, чтобы сохранить результат обработки в JSON, используют ConvertTo-Json. ConvertTo-Json — командлет, который сериализует объект в строку JSON; для глубокой вложенности важно задавать -Depth.
Patterns трактуется как регулярное выражение
Одно место лучше оговорить сразу. Значения Patterns в итоге уходят в Select-String -Pattern. По умолчанию Select-String читает это значение как регулярное выражение.
То есть ERROR. означает «ERROR и любой один символ следом» и попадёт на ERRORS и ERROR:. Строка, где есть только ERROR, наоборот, не совпадёт. Строка вроде C:\App с обратными слэшами тоже, если оставить как есть, воспринимается как управляющие символы регулярного выражения.
- Нужно именно регулярное выражение: пишите как есть. Можно
"ERROR|FATAL"или"\[ERROR\]" - Нужно буквальное совпадение строки: положите в JSON результат
[regex]::Escape("ERROR.")либо добавьте-SimpleMatchкSelect-Stringв скрипте
Кроме того, Select-String по умолчанию не различает регистр. Строка с error попадёт так же, как с ERROR. Если регистр важен, добавьте -CaseSensitive.
5. Сначала только чтение
Не начинайте с перемещения файлов. Сначала найдите логи и сохраните их в CSV.
$config = Get-Content .\log-maintenance.json -Raw -Encoding UTF8 | ConvertFrom-Json
$since = (Get-Date).AddDays(-[int]$config.Days)
$files = Get-ChildItem -LiteralPath $config.LogPath -Filter *.log -File -Recurse |
Where-Object { $_.LastWriteTime -ge $since }
$files |
Select-Object FullName, Length, LastWriteTime
Дальше ищите по содержимому логов.
$patterns = [string[]]$config.Patterns
Select-String -LiteralPath ($files | Select-Object -ExpandProperty FullName) -Pattern $patterns |
Select-Object Path, LineNumber, Pattern, Line |
Export-Csv .\log-hits.csv -NoTypeInformation -Encoding UTF8
До этого места всё только читается. Даже на папке из production стоит остановиться именно здесь.
6. Список старых логов
Дальше составьте список кандидатов на архивирование.
$limit = (Get-Date).AddDays(-[int]$config.ArchiveDays)
$targets = Get-ChildItem -LiteralPath $config.LogPath -Filter *.log -File -Recurse |
Where-Object { $_.LastWriteTime -lt $limit } |
Sort-Object LastWriteTime
$targets |
Select-Object FullName, Length, LastWriteTime |
Export-Csv .\archive-targets.csv -NoTypeInformation -Encoding UTF8
На этом шаге файлы ещё не перемещаются. Откройте archive-targets.csv и проверьте, не слишком ли много кандидатов и не ошиблись ли вы папкой.
7. Изменяющие операции вынесите в функции
Перемещение файлов и другие изменяющие операции отделите от чтения.
Если у функции в PowerShell указан SupportsShouldProcess, она умеет -WhatIf и -Confirm. -WhatIf не выполняет действие, а показывает, что собирались изменить; -Confirm спрашивает подтверждение перед выполнением. Подробности — в статье Microsoft Learn about_Functions_CmdletBindingAttribute.
function Move-OldLogFile {
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Medium")]
param(
[Parameter(Mandatory)]
[System.IO.FileInfo[]]$File,
[Parameter(Mandatory)]
[string]$SourceRoot,
[Parameter(Mandatory)]
[string]$ArchiveRoot
)
foreach ($item in $File) {
$relativePath = [System.IO.Path]::GetRelativePath($SourceRoot, $item.FullName)
$destination = Join-Path $ArchiveRoot $relativePath
$destinationDirectory = Split-Path -Path $destination -Parent
if ($PSCmdlet.ShouldProcess($item.FullName, "Move to $destination")) {
if (-not [System.IO.Directory]::Exists($destinationDirectory)) {
[System.IO.Directory]::CreateDirectory($destinationDirectory) | Out-Null
}
Move-Item -LiteralPath $item.FullName -Destination $destination -ErrorAction Stop
[pscustomobject]@{
Source = $item.FullName
Destination = $destination
Status = "Moved"
Message = ""
}
}
else {
[pscustomobject]@{
Source = $item.FullName
Destination = $destination
Status = "Preview"
Message = ""
}
}
}
}
Главное: $PSCmdlet.ShouldProcess() стоит непосредственно перед Move-Item. Решение «менять или нет» принимается не снаружи функции, а в последний момент перед самим изменением.
8. Готовый скрипт
Ниже — собранная версия. Имя файла — Invoke-LogMaintenance.ps1.
# Invoke-LogMaintenance.ps1
#Requires -Version 7.0
[CmdletBinding()]
param(
[ValidateNotNullOrEmpty()]
[string]$ConfigPath = ".\log-maintenance.json",
[switch]$Preview,
[switch]$SkipArchive,
[switch]$SkipTranscript
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Ensure-Directory {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Path
)
if (-not [System.IO.Directory]::Exists($Path)) {
[System.IO.Directory]::CreateDirectory($Path) | Out-Null
}
}
function Import-LogMaintenanceConfig {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Path
)
if (-not (Test-Path -LiteralPath $Path)) {
throw "Config file not found: $Path"
}
$config = Get-Content -LiteralPath $Path -Raw -Encoding UTF8 | ConvertFrom-Json
foreach ($name in @("LogPath", "OutputPath", "Days", "Patterns", "ArchiveDays", "ArchivePath")) {
if (-not ($config.PSObject.Properties.Name -contains $name)) {
throw "Config value missing: $name"
}
}
if ([string]::IsNullOrWhiteSpace([string]$config.LogPath)) {
throw "LogPath is empty."
}
if (-not (Test-Path -LiteralPath $config.LogPath)) {
throw "LogPath not found: $($config.LogPath)"
}
if ([string]::IsNullOrWhiteSpace([string]$config.OutputPath)) {
throw "OutputPath is empty."
}
if ([string]::IsNullOrWhiteSpace([string]$config.ArchivePath)) {
throw "ArchivePath is empty."
}
if (@($config.Patterns).Count -eq 0) {
throw "Patterns is empty."
}
if ([int]$config.Days -lt 1) {
throw "Days must be 1 or greater."
}
if ([int]$config.ArchiveDays -lt 1) {
throw "ArchiveDays must be 1 or greater."
}
return $config
}
function Export-CsvWithHeader {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[object[]]$InputObject,
[Parameter(Mandatory)]
[string]$Path,
[Parameter(Mandatory)]
[string[]]$Header
)
if ($InputObject.Count -gt 0) {
$InputObject |
Export-Csv -LiteralPath $Path -NoTypeInformation -Encoding UTF8
}
else {
($Header -join ",") |
Set-Content -LiteralPath $Path -Encoding UTF8
}
}
function Get-LogHit {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$LogPath,
[Parameter(Mandatory)]
[ValidateRange(1, 3650)]
[int]$Days,
[Parameter(Mandatory)]
[string[]]$Pattern
)
$since = (Get-Date).AddDays(-$Days)
$files = @(
Get-ChildItem -LiteralPath $LogPath -Filter *.log -File -Recurse -ErrorAction Stop |
Where-Object { $_.LastWriteTime -ge $since }
)
Write-Verbose "Recent log files: $($files.Count)"
if ($files.Count -eq 0) {
return @()
}
$paths = $files | Select-Object -ExpandProperty FullName
Select-String -LiteralPath $paths -Pattern $Pattern -ErrorAction Stop |
ForEach-Object {
[pscustomobject]@{
Path = $_.Path
LineNumber = $_.LineNumber
Pattern = $_.Pattern
Line = $_.Line.Trim()
}
}
}
function Get-OldLogFile {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$LogPath,
[Parameter(Mandatory)]
[ValidateRange(1, 3650)]
[int]$ArchiveDays
)
$limit = (Get-Date).AddDays(-$ArchiveDays)
Get-ChildItem -LiteralPath $LogPath -Filter *.log -File -Recurse -ErrorAction Stop |
Where-Object { $_.LastWriteTime -lt $limit } |
Sort-Object LastWriteTime
}
function Move-OldLogFile {
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Medium")]
param(
[Parameter(Mandatory)]
[System.IO.FileInfo[]]$File,
[Parameter(Mandatory)]
[string]$SourceRoot,
[Parameter(Mandatory)]
[string]$ArchiveRoot
)
foreach ($item in $File) {
$relativePath = [System.IO.Path]::GetRelativePath($SourceRoot, $item.FullName)
$destination = Join-Path $ArchiveRoot $relativePath
$destinationDirectory = Split-Path -Path $destination -Parent
if (Test-Path -LiteralPath $destination) {
$name = [System.IO.Path]::GetFileNameWithoutExtension($item.Name)
$ext = $item.Extension
$destination = Join-Path $destinationDirectory ("{0}_{1:yyyyMMddHHmmss}{2}" -f $name, $item.LastWriteTime, $ext)
}
if ($PSCmdlet.ShouldProcess($item.FullName, "Move to $destination")) {
Ensure-Directory -Path $destinationDirectory
Move-Item -LiteralPath $item.FullName -Destination $destination -ErrorAction Stop
[pscustomobject]@{
Source = $item.FullName
Destination = $destination
Status = "Moved"
Message = ""
}
}
else {
[pscustomobject]@{
Source = $item.FullName
Destination = $destination
Status = "Preview"
Message = ""
}
}
}
}
$config = Import-LogMaintenanceConfig -Path $ConfigPath
$runStamp = Get-Date -Format "yyyyMMdd-HHmmss"
$reportDir = Join-Path ([string]$config.OutputPath) $runStamp
Ensure-Directory -Path $reportDir
$transcriptStarted = $false
$transcriptPath = Join-Path $reportDir "transcript.txt"
try {
if (-not $SkipTranscript) {
Start-Transcript -Path $transcriptPath -Force | Out-Null
$transcriptStarted = $true
}
Write-Host "Report directory: $reportDir"
$hits = @(
Get-LogHit `
-LogPath ([string]$config.LogPath) `
-Days ([int]$config.Days) `
-Pattern ([string[]]$config.Patterns)
)
$hitCsv = Join-Path $reportDir "log-hits.csv"
Export-CsvWithHeader `
-InputObject $hits `
-Path $hitCsv `
-Header @("Path", "LineNumber", "Pattern", "Line")
$oldFiles = @(
Get-OldLogFile `
-LogPath ([string]$config.LogPath) `
-ArchiveDays ([int]$config.ArchiveDays)
)
$archiveTargets = @(
$oldFiles |
Select-Object FullName, Length, LastWriteTime
)
$archiveTargetCsv = Join-Path $reportDir "archive-targets.csv"
Export-CsvWithHeader `
-InputObject $archiveTargets `
-Path $archiveTargetCsv `
-Header @("FullName", "Length", "LastWriteTime")
$moveResults = @()
if ($SkipArchive) {
Write-Host "Archive skipped."
}
elseif ($oldFiles.Count -eq 0) {
Write-Host "No archive targets."
}
else {
$archiveRunRoot = Join-Path ([string]$config.ArchivePath) $runStamp
$moveResults = @(
Move-OldLogFile `
-File $oldFiles `
-SourceRoot ([string]$config.LogPath) `
-ArchiveRoot $archiveRunRoot `
-WhatIf:$Preview
)
}
$archiveResultCsv = Join-Path $reportDir "archive-result.csv"
Export-CsvWithHeader `
-InputObject $moveResults `
-Path $archiveResultCsv `
-Header @("Source", "Destination", "Status", "Message")
$summary = [pscustomobject]@{
CheckedAt = (Get-Date).ToString("s")
ComputerName = $env:COMPUTERNAME
LogPath = [string]$config.LogPath
ReportDirectory = $reportDir
HitCount = $hits.Count
ArchiveTargetCount = $oldFiles.Count
ArchiveResultCount = $moveResults.Count
Preview = [bool]$Preview
SkipArchive = [bool]$SkipArchive
}
$summaryPath = Join-Path $reportDir "summary.json"
$summary |
ConvertTo-Json -Depth 5 |
Set-Content -LiteralPath $summaryPath -Encoding UTF8
Write-Host "Finished."
Write-Host "Hits: $($hits.Count)"
Write-Host "Archive targets: $($oldFiles.Count)"
}
catch {
$errorPath = Join-Path $reportDir "error.txt"
$_ | Out-String | Set-Content -LiteralPath $errorPath -Encoding UTF8
Write-Error "Failed: $($_.Exception.Message)"
exit 1
}
finally {
if ($transcriptStarted) {
Stop-Transcript | Out-Null
}
}
9. Примеры запуска
Сначала разберите логи, не архивируя.
.\Invoke-LogMaintenance.ps1 -ConfigPath .\log-maintenance.json -SkipArchive
Дальше посмотрите план архивирования.
.\Invoke-LogMaintenance.ps1 -ConfigPath .\log-maintenance.json -Preview
Если указан -Preview, перемещение старых логов обрабатывается как -WhatIf.
На этом этапе смотрите три файла:
log-hits.csvarchive-targets.csvarchive-result.csv
Если всё в порядке, запускайте без -Preview.
.\Invoke-LogMaintenance.ps1 -ConfigPath .\log-maintenance.json
Чтобы увидеть подробности, добавьте -Verbose.
.\Invoke-LogMaintenance.ps1 -ConfigPath .\log-maintenance.json -Preview -Verbose
10. Какие файлы появляются
При запуске под OutputPath создаётся папка с меткой даты и времени, например C:\App\Reports\20260602-030000.
Внутри появляются такие файлы.
| Файл | Содержимое |
|---|---|
log-hits.csv |
Строки, отмеченные как ошибки или предупреждения |
archive-targets.csv |
Старые логи, попавшие в кандидаты на архивирование |
archive-result.csv |
Результат перемещения или результат режима Preview |
summary.json |
Сводка по количеству и условиям запуска |
transcript.txt |
Запись сеанса PowerShell |
error.txt |
Подробности, если произошла ошибка |
Start-Transcript записывает команды сеанса PowerShell и вывод консоли в текстовый файл. В эксплуатационном скрипте потом проще понять, когда, при каких условиях и что именно получилось.
Как должен выглядеть «правильный» результат
В первый раз важнее не то, что файлы появились, а то, что внутри они такие, как задумано. Содержимое логов здесь учебное, но набор столбцов и полей совпадает с тем, что выдаёт скрипт из статьи.
В log-hits.csv четыре столбца: Path / LineNumber / Pattern / Line. Export-Csv по умолчанию берёт каждое поле в ".
"Path","LineNumber","Pattern","Line"
"C:\App\Logs\app.log","128","ERROR","2026-06-02 02:14:51 [ERROR] OrderService: timeout while calling /api/stock"
"C:\App\Logs\app.log","301","WARN","2026-06-02 02:41:03 [WARN] OrderService: retry 1/3"
"C:\App\Logs\batch.log","57","FATAL","2026-06-02 03:00:12 [FATAL] nightly batch aborted"
В столбец Pattern попадает строка того шаблона, который сработал. Она соответствует значениям из Patterns в файле конфигурации, поэтому по ней можно считать, «какой ключ сколько раз сработал».
В archive-targets.csv три столбца: FullName / Length / LastWriteTime.
"FullName","Length","LastWriteTime"
"C:\App\Logs\old\app-202401.log","10485760","2024/01/31 23:59:58"
В archive-result.csv четыре столбца: Source / Destination / Status / Message. При -Preview в Status будет Preview, при реальном запуске — Moved.
"Source","Destination","Status","Message"
"C:\App\Logs\old\app-202401.log","C:\App\Archive\20260602-030000\old\app-202401.log","Moved",""
Формат даты зависит от того, как Export-Csv строит строку по локали ОС, поэтому в разных средах он может отличаться. Если CSV уходит в следующую систему, это стоит проверить заранее.
summary.json — сводка по количеству и условиям запуска. Пути по правилам JSON экранируются как \\.
{
"CheckedAt": "2026-06-02T03:00:07",
"ComputerName": "OPS-01",
"LogPath": "C:\\App\\Logs",
"ReportDirectory": "C:\\App\\Reports\\20260602-030000",
"HitCount": 3,
"ArchiveTargetCount": 1,
"ArchiveResultCount": 1,
"Preview": false,
"SkipArchive": false
}
Для мониторинга и ежедневной проверки удобно смотреть только summary.json. CSV можно открывать в дни, когда подскочил HitCount или резко вырос ArchiveTargetCount.
CSV создаётся и тогда, когда попаданий нет. Export-CsvWithHeader в этом случае пишет файл только с строкой заголовка. Так не путают «файла нет» и «совпадений было 0».
11. Регулярный запуск через Планировщик заданий
Если ручной запуск прошёл без проблем, скрипт можно ставить на расписание в Планировщике заданий.
Сначала — пример ежедневного запуска в 3:00 ночи.
$scriptPath = "C:\Ops\Invoke-LogMaintenance.ps1"
$configPath = "C:\Ops\log-maintenance.json"
$action = New-ScheduledTaskAction `
-Execute "pwsh.exe" `
-Argument "-NoProfile -File `"$scriptPath`" -ConfigPath `"$configPath`"" `
-WorkingDirectory "C:\Ops"
$trigger = New-ScheduledTaskTrigger -Daily -At 3:00
Register-ScheduledTask `
-TaskName "AppLogMaintenance" `
-Action $action `
-Trigger $trigger `
-Description "Collect app log errors and archive old logs"
New-ScheduledTaskAction создаёт объект команды, которую выполнит задание, а New-ScheduledTaskTrigger задаёт условие запуска: ежедневно, еженедельно, при входе в систему и т. д. Затем Register-ScheduledTask регистрирует задание на локальном компьютере.
Проверьте, что задание действительно зарегистрировано
Если команда регистрации прошла, это ещё не значит, что задание «будет работать». Перед тем как ждать срабатывания, посмотрите глазами, что именно записалось.
В графическом интерфейсе откройте Планировщик заданий (taskschd.msc) и в левой панели выберите «Библиотека планировщика заданий» — в списке должен появиться только что зарегистрированный AppLogMaintenance. Смотрите такие столбцы.
| Столбец | На что смотреть |
|---|---|
| Состояние | Если «Готово», задание включено. Если «Отключено», оно не стартует даже при срабатывании триггера |
| Триггеры | Совпадает ли условие с задуманным, например «ежедневно в 3:00» |
| Следующий запуск | Ближайшее запланированное время. Если пусто, проверьте триггер и то, включено ли задание |
| Результат последнего запуска | Появляется после выполнения. 0x0 — успех, 0x1 — обычная ошибка, 0x41301 — задание ещё выполняется |
Если этих столбцов нет в списке, щёлкните правой кнопкой по заголовку столбца и добавьте нужные.
То же самое можно проверить из PowerShell. На машине без GUI так обычно быстрее.
# Проверить регистрацию задания
Get-ScheduledTask -TaskName "AppLogMaintenance" |
Select-Object TaskName, TaskPath, State
# Проверить результат выполнения
Get-ScheduledTaskInfo -TaskName "AppLogMaintenance" |
Select-Object TaskName, LastRunTime, LastTaskResult, NextRunTime
Если LastTaskResult не 0, сначала смотрите error.txt и transcript.txt в папке отчётов.
Но если само задание не стартовало, скрипт не выполнил ни одной строки, и папки отчётов может не быть вовсе. Тогда смотрите не свойства, открытые через правый щелчок, а вкладку «Журнал» в нижней части окна, когда задание выбрано в списке. Журнал часто выключен по умолчанию: включите его командой «Включить журнал всех заданий» в правой панели, затем правым щелчком выберите «Выполнить» и проверьте ручной запуск.
В production дополнительно проверьте следующее.
- у учётной записи задания есть право читать папку логов
- есть право писать в папку архива
pwsh.exeнаходится в PATH- скрипт проходит политику выполнения и правила подписи
- ручной запуск и запуск заданием дают один и тот же результат
- при сбое можно посмотреть
error.txtи журнал задания
12. Типичные сбои
| Симптом | Причина | Что делать |
|---|---|---|
| Логи не находятся | Неверно указан LogPath |
Проверьте через Test-Path и Get-ChildItem |
| CSV получается пустым | За выбранный период подходящих логов нет | Расширьте Days и проверьте снова |
| Японский текст отображается некорректно | Кодировка лога не та, что ожидалась | Проверьте кодировки ввода и вывода |
| Как задание не работает | Другая учётная запись или другая рабочая папка | Проверьте WorkingDirectory и права |
| Слишком много кандидатов на архивирование | Слишком маленькое ArchiveDays |
Посмотрите archive-targets.csv и подберите значение |
| Путь назначения не тот | Не учли правило сохранения относительного пути | Проверьте Destination через -Preview |
| Падает только в production | Другие права, политика или заблокированные файлы | Смотрите error.txt и transcript.txt |
Особенно при запуске через Планировщик заданий учётная запись «под которой запускали вы» и «под которой работает задание» часто различаются. Если вручную скрипт идёт, а как задание падает, сначала проверьте права и рабочую папку.
13. Как дорабатывать скрипт
Этот скрипт рассчитан не столько на запуск «как есть», сколько на постепенную подгонку под площадку. Типичные доработки такие.
| Что нужно | Что менять |
|---|---|
Захватить ещё и .txt |
Изменить Get-ChildItem -Filter *.log |
Посмотреть несколько строк вокруг ERROR |
По результату Select-String взять соседние строки через Get-Content |
| Сжимать перед архивированием | Добавить Compress-Archive перед Move-OldLogFile |
| Слать уведомление по почте | Добавить отправку на основе summary.json |
| Развести настройки по приложениям | Подготовить несколько JSON и разнести задания |
| Автоматизировать вплоть до удаления | Сначала какое-то время поработать в режиме перемещения, потом решать |
Но безопаснее не класть всё сразу в первую версию. Для эксплуатационного скрипта важнее не число функций, а возможность разобраться, когда что-то сломалось.
Для почтовых уведомлений сначала выберите способ
«Хотим почтовое уведомление» просят часто, но именно здесь способ нужно выбрать до реализации.
В Microsoft Learn у Send-MailMessage прямо написано obsolete (устарел): безопасное подключение к SMTP-серверу он не гарантирует, поэтому им пользоваться не рекомендуют. Прямого преемника среди командлетов PowerShell нет; как замена указаны библиотеки вроде MailKit, а в среде Exchange Online — Send-MgUserMail из Microsoft Graph PowerShell SDK.
Способ зависит от среды.
| Среда | Как уведомлять |
|---|---|
| Есть внутренний SMTP-релей, аутентификация и TLS не нужны | Send-MailMessage ещё отработает, но на случай будущего удаления спрячьте его в одну функцию |
| Нужны SMTP с аутентификацией или TLS | Берите библиотеку вроде MailKit |
| Среда Microsoft 365 | Идите через Microsoft Graph (Send-MgUserMail) |
| Почта не обязательна | Webhook в Teams или выгрузка файла в систему мониторинга |
В любом варианте уведомление должно только читать summary.json и отправлять его, отдельно от основной работы. На практике важно, чтобы анализ логов и архивирование уже были завершены, даже если уведомление не ушло.
14. Чек-лист перед регулярным запуском
Перед тем как ставить скрипт PowerShell на расписание, проверьте следующее.
- сначала запускали только чтение с
-SkipArchive - затем смотрели план перемещения через
-Preview - кандидаты в
archive-targets.csvвыглядят правдоподобно - значения
Destinationвarchive-result.csvсовпадают с ожиданием - в папке вывода остались артефакты с меткой даты и времени
- при ошибке появляется
error.txt - проверили права учётной записи, от которой работает задание
- проверили политику выполнения, подпись и внутренние правила
- начинаете с перемещения, а не сразу с удаления
- заранее решено, куда возвращать файлы при откате
15. Итог
Прикладной PowerShell не требует набора сложных конструкций. На работе помогает заранее собранный шаблон.
- Вынести настройки в JSON
- Сначала сделать этап только чтения
- Оставлять артефакты в CSV и JSON
- Изменяющие операции выносить в функции
- Держать пробный прогон на уровне
-WhatIf - Оставлять возможность разобрать запуск по transcript и
error.txt - Сначала проверить вручную, потом ставить на расписание
Скрипт в статье посвящён анализу логов и архивированию, но тот же подход годится и для других задач.
- Наведение порядка в файлах
- Формирование отчётов
- Агрегация CSV
- Замена пакетной обработки
- Инвентаризация старых активов
- Ежедневные и ежемесячные эксплуатационные проверки
PowerShell удобен и однострочником, но в рабочей задаче безопаснее держать такой порядок:
Посмотреть → Записать → Сделать пробный прогон → Выполнить → Оставить артефакты
Если придерживаться этой формы, PowerShell перестаёт быть просто способом сократить ручную работу и становится небольшим рабочим приложением, которое стабилизирует эксплуатацию.
Справочные ссылки
- Полный набор примеров кода к этой статье (готовый скрипт, файл конфигурации, тесты Pester) - komurasoft-blog-samples (GitHub)
- PowerShell Documentation - Microsoft Learn
- ConvertTo-Json - Microsoft Learn
- about_Functions_CmdletBindingAttribute - Microsoft Learn
- Start-Transcript - Microsoft Learn
- New-ScheduledTaskAction - Microsoft Learn
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Политика выполнения PowerShell и подпись скриптов — как отказаться от Bypass в роли постоянной заглушки
Политика выполнения PowerShell — не граница безопасности, а защитный механизм. В статье разобраны различия RemoteSigned и других политик,...
Практические команды PowerShell — мелкие приёмы для повседневной работы
Разбираем практические команды PowerShell для повседневной работы: где применять Measure-Object, Group-Object, Select-String, Compare-Obj...
Основы команд PowerShell — операции, которые стоит выучить первыми, и как использовать их безопасно
Чтобы в реальной работе не теряться с PowerShell, разбираем поиск командлетов, конвейер, операции с файлами, обработку CSV, политику выпо...
Автоматизация настройки ПК с помощью winget + PowerShell ── делаем регламент исполняемым
Как сделать настройку ПК для новых сотрудников воспроизводимой: установка приложений через winget и export/import, декларативная WinGet C...
Обработка ошибок и retry в PowerShell — от ловушки нерабочего try/catch до exit code и типовых схем повтора
На практике разбираем различие завершающих и незавершающих ошибок PowerShell, ловушку, из-за которой не срабатывает try/catch, стандартны...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Бизнес-приложения, интеграция оборудования и средства связи — от требований до разработки.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- С чего начать автоматизацию анализа логов в PowerShell?
- Не пишите сразу изменяющие операции вроде перемещения или удаления — сначала сделайте только чтение. Доведите до конца поиск нужных логов через Get-ChildItem, выборку строк с ошибками через Select-String и вывод в CSV. Даже если пробуете на папке из production, остановитесь на этом этапе чтения и проверьте результат. Дальше безопаснее идти поэтапно: сначала список кандидатов на архивирование, затем вынести перемещение в отдельную функцию.
- Как встроить -WhatIf в PowerShell-скрипт?
- Если в атрибут CmdletBinding функции добавить SupportsShouldProcess, у неё появляются -WhatIf и -Confirm. Главное — вызвать $PSCmdlet.ShouldProcess() непосредственно перед изменяющей операцией (например, Move-Item), чтобы решение «менять или нет» принималось в последний момент. В скрипте из этой статьи переключатель -Preview передаётся как -WhatIf:$Preview, поэтому план перемещения можно проверить, не двигая файлы.
- Почему скрипт PowerShell работает вручную, но падает в Планировщике заданий?
- Типичная причина — учётная запись задания не та, под которой вы запускали скрипт сами. Проверьте, есть ли у неё право читать папку логов и писать в папку архива, верна ли рабочая папка (WorkingDirectory), находится ли pwsh.exe в PATH и проходит ли скрипт политику выполнения и правила подписи. Важно заранее сделать так, чтобы при сбое причину можно было найти по error.txt, transcript.txt и журналу задания.
- Можно ли сразу автоматизировать и удаление старых логов?
- Автоматизировать удаление с первого дня не стоит. Скрипт из этой статьи тоже останавливается на более безопасном «перемещении», а не удалении. Сначала поработайте в режиме перемещения, посмотрите по артефактам вроде archive-targets.csv и archive-result.csv, что сюрпризов нет, и только потом думайте об удалении. Заранее решите, куда возвращать файлы, если понадобится откат.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.