Aplicación de scripts PowerShell — automatizar de forma segura la investigación de logs, el archivado y la generación de informes
· Actualizado el: · Go Komura · PowerShell, Windows, Automatización, Investigación de logs, Mejora operativa, Aprovechamiento de activos existentes
Historial de revisiones (1 actualizaciones, última el 22 Aug 2026)
Registro de los cambios realizados en este artículo. Cuando se archivó una versión previa, sigue siendo legible mediante un enlace permanente con DOI.
- Se ha sustituido la traducción, que estaba abreviada, por una traducción completa del artículo japonés en su versión actual: el texto crece un 57 %. Se han incorporado 5 apartados, 17 filas de tabla y 5 bloques de código que no estaban en la edición anterior. El contenido no cambia respecto al original japonés; esta edición simplemente ya no lo resume. Además, los enlaces a otros artículos que ya tienen edición en español apuntan ahora a esa edición en lugar de a la japonesa. Leer la versión anterior a esta actualización (DOI: 10.5281/zenodo.21638207)
- Primera publicación
Citar este artículo(DOI: 10.5281/zenodo.21638206)
Este artículo está archivado en Zenodo. A continuación se muestran tanto el DOI que siempre resuelve a la última versión como el DOI fijado a la versión que está leyendo.
Go Komura (2026). Aplicación de scripts PowerShell — automatizar de forma segura la investigación de logs, el archivado y la generación de informes. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21638206 https://comcomponent.com/es/blog/2026/06/02/000-powershell-script-log-maintenance-automation/
- DOI (última versión)
- 10.5281/zenodo.21638206
- DOI (esta versión)
- 10.5281/zenodo.22053412
1. Lo primero que conviene tener claro
En el artículo anterior organizamos los comandos básicos de PowerShell, las canalizaciones (pipelines), la salida en CSV, JSON, los scripts .ps1 y la comprobación segura mediante -WhatIf. En esta continuación, vamos a crear un script algo más grande y práctico para el trabajo real. El tema son las siguientes tareas operativas:
- Investigar los logs
- Recopilar las líneas de error en un CSV
- Listar los logs antiguos como candidatos a archivado
- Mover los logs antiguos cuando sea necesario
- Dejar constancia del resultado de la ejecución como evidencia
Lo importante en la aplicación práctica de PowerShell no es alargar los comandos, sino construir el siguiente flujo:
- Separar la configuración
- Construir el procesamiento de lectura
- Dejar constancia de la salida
- Separar el procesamiento de cambios en una función
- Hacer un ensayo con
-WhatIf - Por último, considerar la ejecución automática
Automatizar no significa «eliminar la verificación humana». Significa fijar los puntos que se deben comprobar, de modo que cada ejecución deje siempre la misma clase de evidencia.
El código que aparece en este artículo está publicado en GitHub como un conjunto de muestras que se puede ejecutar tal cual (el script completo, el archivo de configuración, un script para crear un entorno de logs ficticios y pruebas Pester que verifican la investigación, el ensayo y el movimiento).
powershell-script-log-maintenance-automation - komurasoft-blog-samples (GitHub)
El lugar de este artículo en la serie
Los artículos sobre PowerShell forman una serie de 3 entregas. No es una serie numerada de forma oficial, pero leerlos en este orden mantiene la continuidad.
| Orden | Artículo | Contenido tratado |
|---|---|---|
| 1 | Comandos básicos de PowerShell — las operaciones que hay que aprender primero y su uso seguro | Cómo buscar comandos, canalizaciones, CSV / JSON, .ps1, -WhatIf |
| 2 | Recetario de comandos prácticos de PowerShell — ampliar las funciones pequeñas que se usan a diario | Piezas para agregación, comparación, extracción y generación de evidencias |
| 3 | Este artículo | Reunir las piezas en un único script operativo, hasta la configuración, la evidencia, el ensayo y la ejecución periódica |
Como requisito previo, solo hace falta el contenido del artículo 1. Aunque no haya leído el 2, puede seguir este artículo sin problema. La expresión «el artículo anterior» al principio se refiere al artículo 1.
2. Lo que vamos a construir
En este artículo vamos a crear un script llamado Invoke-LogMaintenance.ps1.
Sus funciones principales son las siguientes:
| Función | Contenido |
|---|---|
| Búsqueda de logs | Busca archivos .log dentro de la carpeta indicada |
| Selección de periodo | Investiga solo los logs actualizados en los últimos N días |
| Extracción de errores | Extrae líneas como ERROR, WARN, FATAL |
| Salida en CSV | Guarda los resultados de búsqueda en log-hits.csv |
| Listado de logs antiguos | Guarda en archive-targets.csv los logs de más de N días |
| Archivado | Mueve los logs antiguos a otra carpeta |
| Ejecución de ensayo | Con -Preview, no mueve nada y solo muestra el plan |
| Registro de ejecución | Guarda el transcript, el JSON de resumen y el CSV de resultados |
Cabe señalar que no se elimina nada. En esta primera entrega de aplicación práctica, nos limitamos al «movimiento», que es más seguro que la eliminación.
3. Estructura de carpetas
Como ejemplo, usaremos la siguiente estructura:
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
Separar el script principal del archivo de configuración facilita el cambio entre entornos. Puede operar cambiando solo la ruta, por ejemplo C:\Test\Logs en el entorno de desarrollo y D:\App\Logs en producción.
4. Crear el archivo de configuración
Primero creamos log-maintenance.json.
{
"LogPath": "C:\\App\\Logs",
"OutputPath": "C:\\App\\Reports",
"Days": 7,
"Patterns": [
"ERROR",
"WARN",
"FATAL"
],
"ArchiveDays": 90,
"ArchivePath": "C:\\App\\Archive"
}
Su significado es el siguiente:
| Elemento | Significado |
|---|---|
LogPath |
Carpeta de logs a investigar |
OutputPath |
Destino de salida de los informes |
Days |
Cuántos días recientes de logs investigar |
Patterns |
Cadenas o patrones de búsqueda |
ArchiveDays |
A partir de cuántos días de antigüedad se archiva un log |
ArchivePath |
Carpeta de destino del archivado |
Al usar JSON, puede cambiar las condiciones sin editar el script principal.
En PowerShell, ConvertFrom-Json permite tratar el JSON como un objeto. A la inversa, cuando se quiere guardar el resultado del procesamiento en JSON, se usa ConvertTo-Json. ConvertTo-Json es un cmdlet que convierte un objeto en una cadena JSON; cuando se trabaja con jerarquías profundas, es importante especificar -Depth.
Patterns se interpreta como expresión regular
Hay un punto que conviene advertir de antemano. El valor de Patterns se pasa finalmente a Select-String -Pattern. Por defecto, Select-String interpreta este valor como una expresión regular.
Es decir, si escribe ERROR., esto significa «ERROR seguido de cualquier carácter», por lo que también coincide con ERRORS o ERROR:. En cambio, no coincide con una línea que solo contenga ERROR. Del mismo modo, una cadena que contenga barras invertidas, como C:\App, se interpreta tal cual como caracteres de control de la expresión regular.
- Si quiere usarlo como expresión regular: escríbalo tal cual. Puede escribir, por ejemplo,
"ERROR|FATAL"o"\[ERROR\]" - Si quiere que coincida con la cadena literal: introduzca en el JSON el resultado de
[regex]::Escape("ERROR."), o añada-SimpleMatchalSelect-Stringdel lado del script
Además, por defecto Select-String no distingue entre mayúsculas y minúsculas: recoge tanto las líneas con error como las que tienen ERROR. Si desea que se distingan, añada -CaseSensitive.
5. Empezar solo con la lectura
En lugar de escribir directamente el procesamiento de movimiento, empezamos limitándonos a buscar los logs y volcarlos en un 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
A continuación, buscamos dentro del contenido de los logs.
$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
Hasta aquí, todo es solo lectura. Incluso al probarlo contra la carpeta de producción, deténgase primero en esta etapa.
6. Listar los logs antiguos
A continuación, listamos los objetos a archivar.
$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
Tampoco en esta etapa se mueve nada todavía. Revise archive-targets.csv para comprobar que los objetos no son demasiados y que la carpeta no está equivocada.
7. Separar el procesamiento de cambios en una función
Los procesos de cambio, como el movimiento, se separan del procesamiento de lectura.
En PowerShell, al añadir SupportsShouldProcess a una función, esta puede usar -WhatIf y -Confirm. -WhatIf muestra «qué se va a cambiar» sin ejecutarlo realmente, y -Confirm es el mecanismo para pedir confirmación antes de ejecutar. Puede consultar los detalles en about_Functions_CmdletBindingAttribute de Microsoft Learn.
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 = ""
}
}
}
}
El punto clave es colocar $PSCmdlet.ShouldProcess() justo antes de Move-Item. La decisión de si se realiza el cambio se ubica justo antes del cambio, no fuera de la función.
8. Script completo
Esta es la versión completa que reúne todo lo anterior. El nombre del archivo es 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. Ejemplos de ejecución
Primero, realizamos solo la investigación de logs sin ejecutar el archivado.
.\Invoke-LogMaintenance.ps1 -ConfigPath .\log-maintenance.json -SkipArchive
A continuación, comprobamos el plan de archivado.
.\Invoke-LogMaintenance.ps1 -ConfigPath .\log-maintenance.json -Preview
Al añadir -Preview, el procesamiento de movimiento de los logs antiguos se trata como -WhatIf.
En este punto, los archivos que hay que revisar son estos tres:
log-hits.csvarchive-targets.csvarchive-result.csv
Si no hay problemas, ejecute quitando -Preview.
.\Invoke-LogMaintenance.ps1 -ConfigPath .\log-maintenance.json
Si desea ver más detalle, añada -Verbose.
.\Invoke-LogMaintenance.ps1 -ConfigPath .\log-maintenance.json -Preview -Verbose
10. Archivos generados
Al ejecutarse, se crea debajo de OutputPath una carpeta con fecha y hora, similar a C:\App\Reports\20260602-030000.
Dentro se generan los siguientes archivos:
| Archivo | Contenido |
|---|---|
log-hits.csv |
Líneas detectadas como error o advertencia |
archive-targets.csv |
Logs antiguos que son objeto de archivado |
archive-result.csv |
Resultado del movimiento, o resultado del modo Preview |
summary.json |
Resumen de las cantidades y de las condiciones de ejecución |
transcript.txt |
Registro de la sesión de PowerShell |
error.txt |
Detalles en caso de error |
Start-Transcript es un cmdlet que registra en un archivo de texto los comandos y la salida de consola de una sesión de PowerShell. En un script operativo, esto facilita comprobar después «cuándo, en qué condiciones y qué resultado se obtuvo».
La «forma correcta» de los archivos generados
La primera vez, más que comprobar si se generó el archivo, hay que revisar si el contenido es el esperado. El contenido de los logs es ficticio, pero el orden de las columnas y los elementos es tal como lo produce el script de este artículo.
log-hits.csv tiene 4 columnas: Path / LineNumber / Pattern / Line. Por defecto, Export-Csv rodea todos los campos con ".
"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"
En la columna Pattern se guarda tal cual la cadena de patrón que produjo la coincidencia. Como corresponde con los valores escritos en Patterns del archivo de configuración, se puede usar para contabilizar «con qué palabra clave y cuántos resultados aparecieron».
archive-targets.csv tiene 3 columnas: FullName / Length / LastWriteTime.
"FullName","Length","LastWriteTime"
"C:\App\Logs\old\app-202401.log","10485760","2024/01/31 23:59:58"
archive-result.csv tiene 4 columnas: Source / Destination / Status / Message. Cuando se usa -Preview, Status es Preview; cuando se ejecuta realmente, es Moved.
"Source","Destination","Status","Message"
"C:\App\Logs\old\app-202401.log","C:\App\Archive\20260602-030000\old\app-202401.log","Moved",""
La notación de fecha y hora varía según el entorno, porque Export-Csv la convierte a texto siguiendo la configuración regional del sistema operativo. Si va a pasar el CSV a un sistema posterior, compruebe este punto de antemano.
summary.json es un resumen de las cantidades y de las condiciones de ejecución. Las rutas se escapan como \\ según la especificación de 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
}
Para la monitorización o la revisión diaria, resulta cómodo que baste con mirar este summary.json. Puede operar de forma que solo abra el CSV los días en que HitCount se dispare o ArchiveTargetCount aumente de repente.
Cabe señalar que el CSV se crea incluso cuando no hay ningún objeto. Esto se debe a que Export-CsvWithHeader está diseñada para escribir un archivo que solo contiene la fila de cabecera. Es un diseño pensado para no confundir «no hay archivo» con «hubo 0 coincidencias».
11. Ejecutar periódicamente con el Programador de tareas
Si la ejecución manual no presenta problemas, puede programar la ejecución periódica con el Programador de tareas.
Primero, un ejemplo que se ejecuta todos los días a las 3:00 de la madrugada.
$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 crea un objeto que representa el comando que ejecutará la tarea, y New-ScheduledTaskTrigger crea la condición de inicio (diaria, semanal, al iniciar sesión, etc.). Por último, Register-ScheduledTask registra la tarea en el equipo local.
Comprobar que el registro se realizó correctamente
Que el comando de registro se ejecute sin errores no significa todavía que «funcione». Antes de esperar a la ejecución, conviene revisar visualmente el contenido registrado.
Si usa la interfaz gráfica, abra el Programador de tareas (taskschd.msc) y seleccione «Biblioteca del Programador de tareas» en el panel izquierdo; ahí aparecerá en la lista la tarea AppLogMaintenance que acaba de registrar. Las columnas que hay que revisar son las siguientes:
| Columna | Cómo interpretarla |
|---|---|
| Estado | Si dice «Preparado», está activa. Si dice «Deshabilitada», no se ejecutará aunque llegue el desencadenador |
| Desencadenador | Comprueba si la condición es la deseada, por ejemplo «Todos los días a las 3:00» |
| Próxima ejecución | La hora prevista más cercana. Si aparece vacía, sospeche del desencadenador o del estado habilitado/deshabilitado |
| Resultado de la última ejecución | Se rellena después de ejecutarse. 0x0 significa éxito, 0x1 es un error general, y 0x41301 significa que está en ejecución |
Si estas columnas no aparecen en la lista, haga clic con el botón derecho sobre los encabezados de columna y añada las columnas que desee mostrar.
Lo mismo se puede comprobar desde PowerShell. En equipos donde no se puede abrir la interfaz gráfica, esta vía es más rápida.
# Ver el contenido registrado
Get-ScheduledTask -TaskName "AppLogMaintenance" |
Select-Object TaskName, TaskPath, State
# Ver el resultado de la ejecución
Get-ScheduledTaskInfo -TaskName "AppLogMaintenance" |
Select-Object TaskName, LastRunTime, LastTaskResult, NextRunTime
Si LastTaskResult no es 0, primero revise error.txt y transcript.txt en la carpeta de informes.
Ahora bien, si la propia tarea no ha llegado a iniciarse, el script no ha ejecutado ni una sola línea, por lo que ni siquiera se crea la carpeta de informes. En ese caso, en lugar de las propiedades que se abren al hacer clic derecho sobre la tarea, revise la pestaña «Historial» que aparece en la parte inferior al seleccionar la tarea en la lista. Como el historial puede estar deshabilitado por defecto, actívelo con «Habilitar todo el historial de tareas» en el panel derecho, y luego compruébelo iniciándolo manualmente con «Ejecutar» del clic derecho.
En el uso en producción, conviene comprobar también los siguientes puntos:
- El usuario de ejecución tiene permiso de lectura sobre la carpeta de logs
- Tiene permiso de escritura sobre el destino de archivado
- La ruta de
pwsh.exeestá correctamente resuelta - Cumple con la directiva de ejecución y las reglas de firma del script
- La ejecución manual y la ejecución por tarea producen el mismo resultado
- En caso de fallo, se puede revisar
error.txty el historial de la tarea
12. Problemas frecuentes
| Síntoma | Causa | Solución |
|---|---|---|
| No se encuentran los logs | LogPath está equivocado |
Comprobar con Test-Path y Get-ChildItem |
| El CSV queda vacío | No hay logs en el periodo indicado | Ampliar Days y comprobar |
| El texto aparece con caracteres corruptos | La codificación del log es distinta de la esperada | Comprobar la codificación de entrada y salida |
| No funciona con la tarea | El usuario de ejecución o la carpeta de trabajo son distintos | Comprobar WorkingDirectory y los permisos |
| Hay demasiados objetos a archivar | ArchiveDays es demasiado corto |
Revisar archive-targets.csv y ajustar |
| El destino de movimiento no es el esperado | No se ha entendido la regla de conservación de la ruta relativa | Comprobar Destination con -Preview |
| Solo falla en producción | Diferencias de permisos, directivas o archivos bloqueados | Comprobar error.txt y transcript.txt |
En particular, al ejecutarlo con el Programador de tareas, «cuando lo ejecuta usted mismo» y «el usuario de ejecución de la tarea» pueden ser distintos. Si funciona de forma manual pero falla como tarea, sospeche primero de los permisos y de la carpeta de trabajo.
13. Consideraciones al modificar el script
Este script está pensado más para adaptarlo poco a poco al entorno real que para usarlo tal cual. A continuación se presentan ejemplos habituales de modificación.
| Qué se quiere hacer | Dónde modificar |
|---|---|
También incluir .txt |
Cambiar Get-ChildItem -Filter *.log |
Ver también unas líneas antes y después de ERROR |
Obtener las líneas cercanas con Get-Content a partir del resultado de Select-String |
| Comprimir antes de archivar | Añadir Compress-Archive antes de Move-OldLogFile |
| Enviar notificación por correo | Añadir un procesamiento de notificación basado en summary.json |
| Separar la configuración por aplicación | Preparar varios JSON y separar las tareas |
| Automatizar también la eliminación | Confirmar primero con la operación de movimiento durante un tiempo, y luego considerarlo |
No obstante, es más seguro no incorporarlo todo desde el principio. En un script operativo, lo importante no es que tenga muchas funciones, sino que se pueda rastrear qué ocurrió cuando falla.
Solo para la notificación por correo, decida antes el método
«Quiero notificación por correo» es una petición frecuente, pero este es el único punto en el que hay que decidir el método antes de implementarlo.
Send-MailMessage de PowerShell está marcado explícitamente como «obsolete» (obsoleto) en Microsoft Learn, y se recomienda no usarlo porque no garantiza una conexión segura al servidor SMTP. No existe un cmdlet sucesor directo dentro de PowerShell; como alternativas se mencionan bibliotecas como MailKit, o bien, en entornos de Exchange Online, Send-MgUserMail del SDK de Microsoft Graph PowerShell.
El método varía según el entorno.
| Entorno | Método de notificación |
|---|---|
| Hay un relé SMTP interno y no se requiere autenticación ni TLS | Send-MailMessage funciona, pero conviene encapsularlo en una sola función previendo su futura eliminación |
| Se requiere SMTP con autenticación o TLS | Usar una biblioteca como MailKit |
| Entorno Microsoft 365 | Usar Microsoft Graph (Send-MgUserMail) |
| No es necesario que sea correo | Usar un webhook como el de Teams, o integrar mediante archivos con la plataforma de monitorización |
Sea cual sea el método, limite el procesamiento de notificación a leer summary.json y enviarlo, y manténgalo separado del procesamiento principal. En la práctica, resulta eficaz dejarlo de modo que, aunque la notificación falle, la investigación de logs y el archivado ya se hayan completado.
14. Lista de verificación operativa en el terreno
Antes de ejecutar el script de PowerShell de forma periódica, compruebe los siguientes puntos:
- Se ejecutó primero solo el procesamiento de lectura con
-SkipArchive - A continuación, se comprobó el plan de movimiento con
-Preview - Los objetos de
archive-targets.csveran razonables - El
Destinationdearchive-result.csvera el esperado - Quedó constancia con fecha y hora en la carpeta de destino
- Se comprobó que
error.txtse genera en caso de error - Se comprobaron los permisos del usuario de ejecución de la tarea
- Se comprobaron la directiva de ejecución, la firma y las normas internas
- Se opera primero con el movimiento, no con la eliminación directa
- Se ha decidido el destino de restauración por si hace falta recuperar algo
15. Resumen
Aunque hablemos de «aplicación práctica» de PowerShell, no hace falta usar mucha sintaxis complicada. Lo que resulta eficaz en el trabajo real es construir un patrón como este:
- Separar la configuración en JSON
- Construir primero el procesamiento de lectura
- Dejar constancia con CSV y JSON
- Separar el procesamiento de cambios en una función
- Preparar un ensayo equivalente a
-WhatIf - Hacer que se pueda rastrear con el transcript y
error.txt - Confirmar con la ejecución manual antes de programar la ejecución periódica
El script de este artículo tiene como tema la investigación de logs y el archivado, pero el enfoque se puede aplicar a otras tareas.
- Organización de archivos
- Generación de informes
- Agregación de CSV
- Sustitución de procesos por lotes
- Inventario de activos antiguos
- Verificación operativa diaria y mensual
PowerShell resulta útil incluso con comandos de una sola línea, pero si se va a usar en el trabajo, es más seguro respetar el siguiente orden:
Observar → Registrar → Ensayar → Ejecutar → Dejar constancia
Si mantiene esta forma de trabajar, PowerShell deja de ser una simple herramienta para acortar tareas y pasa a funcionar como una pequeña aplicación de negocio para estabilizar la operación.
Referencias
- El conjunto de código de muestra de este artículo (script completo, archivo de configuración, pruebas 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
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Diseño de parámetros y modularización de scripts de PowerShell — de un «script que funciona» a un «script que se puede entregar»
Explicamos cómo llevar un script de PowerShell a una calidad entregable: param, [CmdletBinding()], validación, pipeline, -WhatIf, módulos...
Diferencias entre Windows PowerShell 5.1 y PowerShell 7 ── Guía práctica de migración de scripts internos
Explicamos la relación entre Windows PowerShell 5.1 y PowerShell 7 (coexistencia y pwsh.exe), la política oficial de no añadir funciones ...
Automatizar el kitting de PC con winget + PowerShell ── Convertir el manual de procedimientos en algo ejecutable
Resumimos cómo hacer reproducible la configuración de PC para nuevos empleados: instalación de aplicaciones con winget, export/import, co...
Integrar PowerShell con una API REST ── el uso práctico de Invoke-RestMethod
Resumen práctico para llamar a APIs internas y REST de SaaS desde PowerShell: cabeceras de autenticación, JSON en japonés sin errores de ...
Dónde mirar cuando un script de PowerShell es lento — claves de arrays, pipeline y cruces de datos
Analizamos las causas típicas de la lentitud en PowerShell: += en arrays, pipeline vs. foreach, cruces con tablas hash, E/S de archivos y...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
Aplicaciones empresariales, integración de dispositivos y herramientas de comunicación, de los requisitos al desarrollo.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- Al automatizar la investigación de logs con PowerShell, ¿por dónde conviene empezar?
- No conviene escribir directamente procesos de cambio como mover o eliminar archivos; primero se construye solo el procesamiento de lectura. Complete primero la parte que busca los logs objetivo con Get-ChildItem, extrae las líneas de error con Select-String y las exporta a un CSV. Incluso al probarlo contra la carpeta de producción, deténgase en esta etapa de lectura y revise los resultados. Después, avance de forma gradual hacia la elaboración del listado de objetos a archivar y, finalmente, hacia la creación de una función para el procesamiento de movimiento.
- ¿Cómo se incorpora -WhatIf de PowerShell en un script?
- Al agregar SupportsShouldProcess al atributo CmdletBinding de una función, esa función puede usar -WhatIf y -Confirm. El punto clave es colocar $PSCmdlet.ShouldProcess() justo antes del procesamiento que realiza el cambio real (como Move-Item), de modo que la decisión de si se cambia o no se tome justo antes del cambio. En el script de este artículo, el switch -Preview se pasa como -WhatIf:$Preview, lo que permite comprobar solo el plan sin mover realmente los archivos.
- ¿Por qué un script de PowerShell funciona al ejecutarlo manualmente pero falla en el Programador de tareas?
- La causa típica es que el usuario con el que se ejecuta la tarea es distinto del usuario con el que usted la probó manualmente. Compruebe si ese usuario tiene permiso de lectura sobre la carpeta de logs y de escritura sobre el destino de archivado, si la carpeta de trabajo (WorkingDirectory) es correcta, si la ruta a pwsh.exe está resuelta y si el script cumple con la política de ejecución y las reglas de firma. Es importante que, en caso de fallo, se pueda investigar mediante error.txt, transcript.txt y el historial de la tarea.
- ¿Es recomendable automatizar también la eliminación de logs antiguos?
- No se recomienda automatizar la eliminación desde el principio. El script de este artículo también se limita al «movimiento», que es más seguro que la eliminación. Primero, opere con el movimiento durante un período determinado, revise evidencias como archive-targets.csv y archive-result.csv, y solo después de confirmar que no hay problemas, considere automatizar la eliminación. También es importante decidir de antemano el destino de restauración por si hace falta recuperar algo.
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.