PowerShell 脚本进阶 ── 安全地实现日志排查、归档与报表自动化
· 小村 豪 · PowerShell, Windows, 自动化, 日志排查, 运维改善, 存量资产利用
1. 首先应该掌握的内容
上一篇文章整理了 PowerShell 的基本命令、管道、CSV 输出、JSON、.ps1 脚本,以及通过 -WhatIf 进行的安全确认。本文作为续篇,来制作一个在实务中更好用、规模稍大一些的脚本。题材是下面这类运维工作。
- 排查日志
- 把错误行汇总到 CSV
- 把旧日志列为归档候选清单
- 必要时移动旧日志
- 把执行结果作为留存记录保存下来
PowerShell 进阶的重点,并不是把命令写得更长,而是搭建出下面这样的流程。
- 拆分配置
- 编写读取处理
- 留存输出结果
- 把变更处理拆分为函数
- 用
-WhatIf进行预演 - 最后再考虑自动执行
所谓自动化,并不是「省去人工确认」,而是把该确认的位置固定下来,让每次都能留下同样的记录。
另外,本文中出现的代码,已作为可直接运行的完整示例(完成版脚本、配置文件、用于创建虚拟日志环境的脚本,以及验证排查、预演、移动的 Pester 测试)发布在 GitHub 上。
powershell-script-log-maintenance-automation - komurasoft-blog-samples (GitHub)
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,生产环境使用 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 字符串的 Cmdlet,在处理较深的层级结构时,指定 -Depth 很重要。
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
到这里为止都只是读取。即使要在生产文件夹中尝试,也先在这一步停下来。
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 来处理。
这个阶段要确认的文件有以下 3 个。
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 会话中的命令与控制台输出记录到文本文件中的 Cmdlet。对于运维脚本,这样便于事后确认「什么时候、在什么条件下、输出了什么」。
11. 通过任务计划程序定期执行
手动执行没有问题之后,就可以通过任务计划程序定期执行。
首先是一个每天凌晨 3 点执行的示例。
$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 把任务注册到本地计算机上。
在生产环境中运维时,还要确认以下几点。
- 执行用户拥有日志文件夹的读取权限
- 拥有归档目标位置的写入权限
pwsh.exe的路径可用- 符合脚本的执行策略与签名规则
- 手动执行与任务执行的结果一致
- 失败时可以查看
error.txt与任务历史
12. 常见问题
| 症状 | 原因 | 对策 |
|---|---|---|
| 找不到日志 | LogPath 配置有误 |
用 Test-Path 与 Get-ChildItem 确认 |
| CSV 为空 | 目标期间内没有对应的日志 | 扩大 Days 的范围后再确认 |
| 日文出现乱码 | 日志的字符编码与预期不同 | 确认输入输出的字符编码 |
| 在任务计划程序中无法运行 | 执行用户或工作文件夹不同 | 确认 WorkingDirectory 与权限 |
| 归档对象过多 | ArchiveDays 设置得太短 |
查看 archive-targets.csv 后再调整 |
| 移动位置与预期不同 | 未理解相对路径的保留规则 | 用 -Preview 确认 Destination |
| 只有生产环境失败 | 权限、策略、文件被锁定等差异 | 确认 error.txt 与 transcript.txt |
尤其是在任务计划程序中运行时,「自己手动执行」和「任务的执行用户」可能并不相同。如果手动执行没问题、但在任务中失败,请先怀疑权限和工作文件夹。
13. 二次改造时的思路
这个脚本的前提是要根据现场情况逐步改造,而不是直接照搬使用。下面列举一些常见的改造示例。
| 想实现的效果 | 改造位置 |
|---|---|
也想把 .txt 纳入对象 |
修改 Get-ChildItem -Filter *.log |
也想查看 ERROR 前后的几行 |
基于 Select-String 的结果,用 Get-Content 获取前后行 |
| 想先压缩再归档 | 在 Move-OldLogFile 之前加入 Compress-Archive |
| 想发送邮件通知 | 基于 summary.json 添加通知处理 |
| 想按应用分别配置 | 准备多份 JSON,并把任务拆开 |
| 想把删除也自动化 | 先以移动方式运行一段时间确认后再考虑 |
不过,一开始不要把所有功能都塞进去,会更安全。对于运维脚本来说,重要的不是功能多,而是失败时能够追溯。
14. 现场运维检查清单
在定期执行 PowerShell 脚本之前,确认以下几点。
- 先用
-SkipArchive只执行了读取处理 - 接着用
-Preview确认了移动的预定操作 archive-targets.csv中的对象是合理的archive-result.csv中的Destination与预期一致- 输出目标文件夹中留下了带日期时间的记录
- 确认了出错时会保留
error.txt - 确认了任务执行用户的权限
- 确认了执行策略、签名以及公司内部规则
- 不直接删除,先以移动方式运维
- 已确定需要恢复时的还原位置
15. 小结
即便是 PowerShell 的进阶用法,也不需要用到很多复杂的语法。真正在实务中起作用的,是提前搭建好这样的套路。
- 把配置拆分为 JSON
- 先编写读取处理
- 用 CSV 与 JSON 留存记录
- 把变更处理拆分为函数
- 准备相当于
-WhatIf的预演 - 让 transcript 与
error.txt具备可追溯性 - 先手动执行确认,再定期执行
本次的脚本以日志排查与归档为题材,但这种思路同样适用于其他业务。
- 文件整理
- 报表输出
- CSV 汇总
- 批处理替换
- 旧资产盘点
- 日常、月度的运维确认
PowerShell 即便只用一行命令也很方便,但如果要用在业务中,遵守下面的顺序会更安全。
查看 → 记录 → 预演 → 执行 → 留存记录
保持这种模式,PowerShell 就不只是一个缩短作业时间的工具,而能成为一个用来稳定运维的小型业务应用。
参考链接
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
用 Pester 完善 PowerShell 测试 ── 让运维脚本不易损坏的实务方法
本文整理用 Pester v5 测试 PowerShell 脚本的实务步骤,涵盖日期处理、文件操作、删除处理、Mock,一直到 CI 执行,帮助安全地打好测试基础。
PowerShell 命令基础 ── 应先掌握的操作与安全使用方法
为了让 PowerShell 初学者在实际工作中不会迷失方向,本文整理了如何查找 Cmdlet、管道、文件操作、CSV 处理、执行策略,以及避免事故的确认步骤。
在 C#(CSharp)中执行 PowerShell 并以对象形式接收结果
本文从实务角度整理如何从 C# 启动 PowerShell,并以 PSObject 而非字符串来接收结果,涵盖 PowerShell SDK、AddCommand、AddParameter、BaseObject、Properties 直至错误处理。
PowerShell 实用命令合集 —— 积累日常工作中常用的小工具
本文整理 PowerShell 日常工作中常用的实用命令,说明 Measure-Object、Group-Object、Select-String、Compare-Object、Tee-Object、Start-Transcript 等命令的使用场景和时机。
任务计划程序的任务不执行、以 0x1 结束 ── 原因排查与安全的运维设计
本文整理 Windows 任务计划程序的执行账户与登录类型、「不管用户是否登录都要运行」的含义、以 0x1 结束的典型原因、启用历史记录、防止多重启动、PowerShell 脚本的正确调用方式,直到把定期执行真正落实到运维上的设计方针。
常见问题
汇总了咨询这一主题时常见的问题。
- 用 PowerShell 自动化日志排查时,应该从什么开始入手?
- 不要一开始就编写移动、删除等变更处理,而是先只制作读取处理。先用 Get-ChildItem 查找目标日志,用 Select-String 提取错误行并输出为 CSV,把这一步先完整做出来。即使要在生产文件夹中试运行,也要先在这个只读阶段停下来确认结果。之后再逐步推进到归档对象清单化、把移动处理封装为函数,这样比较安全。
- PowerShell 的 -WhatIf 该如何嵌入脚本?
- 只要在函数的 CmdletBinding 属性中加上 SupportsShouldProcess,该函数就能使用 -WhatIf 与 -Confirm。要点是把 $PSCmdlet.ShouldProcess() 放在真正执行变更的处理(例如 Move-Item)之前,让「是否变更」的判断就在变更发生前那一刻做出。本文的脚本把 -Preview 开关以 -WhatIf:$Preview 的形式传入,这样就能在不实际移动的情况下,只确认预定要执行的操作。
- PowerShell 脚本手动运行没问题,放到任务计划程序里却失败,是什么原因?
- 典型原因是自己手动执行时和任务的执行用户不一致。需要确认执行用户是否拥有日志文件夹的读取权限、归档目标位置的写入权限,工作文件夹(WorkingDirectory)是否正确,pwsh.exe 的路径是否可用,以及是否符合执行策略与签名规则。失败时应确保可以通过 error.txt、transcript.txt 以及任务历史进行追踪,这一点很重要。
- 可以把旧日志的删除也自动化吗?
- 从一开始就自动化删除并不推荐。本文的脚本也只做到比删除更安全的「移动」为止。建议先以移动的方式运行一段时间,查看 archive-targets.csv、archive-result.csv 等留存记录,确认没有问题之后,再考虑把删除自动化。同时也要事先决定好需要恢复时的还原位置。
作者简介
本文作者的个人简介页面。
Go Komura
小村软件有限公司 代表
以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。