Windows’ta Scheduled Task ile Otomatik İzleme Scripti

Bir Windows sunucusunu 7/24 izlemek için her seferinde RDP açıp Task Manager’a bakmak ne kadar sürdürülebilir? Söyleyelim: hiç. Yıllarca üretim ortamlarında çalışırken öğrendiğim en değerli derslerden biri şu oldu: eğer bir şeyi iki kez manuel yapıyorsan, üçüncüsünde onu otomatikleştirmelisin. Windows Scheduled Task mekanizması bu noktada gerçek bir kurtarıcı. PowerShell ile birleşince ortaya ciddi, kurumsal seviyede izleme altyapısı çıkabiliyor. Bu yazıda sıfırdan başlayarak, gerçekten işe yarayan bir otomatik izleme scripti sistemi kuracağız.

Neden Scheduled Task ve PowerShell?

Piyasada onlarca monitoring aracı var. Zabbix var, Prometheus var, Datadog var. Ama bunların hepsinin ortak bir sorunu var: ya lisans maliyeti, ya kurulum karmaşıklığı, ya da organizasyonun mevcut altyapısına entegrasyon sancıları. Küçük ve orta ölçekli ortamlarda, hatta büyük ortamlarda bile bazen en pragmatik çözüm şu oluyor: elimizdeki araçları iyi kullanmak.

Windows’un kendi Task Scheduler’ı, doğru yapılandırıldığında oldukça güçlü. Saniyede bir çalışabiliyor, sistem başlangıcında tetiklenebiliyor, belirli olaylar gerçekleştiğinde aksiyon alabiliyor. PowerShell ise Windows’un kendi dili olduğu için WMI, CIM, .NET sınıfları üzerinden sisteme doğrudan erişim sağlıyor.

İzleme Scriptinin Mimarisi

Tek bir büyük script yazmak yerine modüler bir yapı kuralım. Bu yaklaşım hem bakımı kolaylaştırır hem de farklı sunucularda farklı modülleri devreye almayı mümkün kılar.

Temel yapımız şöyle olacak:

  • CollectMetrics.ps1: Metrikleri toplar, CSV ve JSON olarak kaydeder
  • AlertCheck.ps1: Eşik değerlerini kontrol eder, alarm üretir
  • SendReport.ps1: Günlük/haftalık raporları e-posta ile gönderir
  • Config.json: Eşik değerleri ve yapılandırma

Hepsini C:Monitoring altına toplayacağız.

Config.json ile Merkezi Yapılandırma

Önce yapılandırma dosyasını oluşturalım. Eşik değerlerini script içine gömmek yerine dışarı çıkarmak, gelecekte değişiklik yapmayı çok kolaylaştırır.

# Config.json oluştur
$config = @{
    Thresholds = @{
        CPU_Warning    = 70
        CPU_Critical   = 90
        RAM_Warning    = 80
        RAM_Critical   = 95
        Disk_Warning   = 75
        Disk_Critical  = 90
    }
    Monitoring = @{
        LogPath        = "C:MonitoringLogs"
        ReportPath     = "C:MonitoringReports"
        RetentionDays  = 30
        ServerName     = $env:COMPUTERNAME
    }
    Email = @{
        SmtpServer     = "smtp.sirket.local"
        Port           = 587
        From           = "[email protected]"
        To             = @("[email protected]", "[email protected]")
        UseSSL         = $true
    }
} | ConvertTo-Json -Depth 5

$config | Out-File -FilePath "C:MonitoringConfig.json" -Encoding UTF8

CollectMetrics.ps1 – Metrik Toplama Scripti

Bu script her 5 dakikada bir çalışacak ve anlık sistem durumunu kaydedecek. CPU, RAM, disk ve ağ metriklerini topluyoruz.

# CollectMetrics.ps1
param(
    [string]$ConfigPath = "C:MonitoringConfig.json"
)

$config    = Get-Content $ConfigPath | ConvertFrom-Json
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logFile   = Join-Path $config.Monitoring.LogPath "metrics_$(Get-Date -Format 'yyyyMMdd').csv"

# Log dizini yoksa oluştur
if (-not (Test-Path $config.Monitoring.LogPath)) {
    New-Item -ItemType Directory -Path $config.Monitoring.LogPath -Force | Out-Null
}

# CPU kullanimi (ortalama 3 okuma)
$cpuSamples = @()
1..3 | ForEach-Object {
    $cpuSamples += (Get-CimInstance -ClassName Win32_Processor |
        Measure-Object -Property LoadPercentage -Average).Average
    Start-Sleep -Seconds 1
}
$cpuUsage = [math]::Round(($cpuSamples | Measure-Object -Average).Average, 2)

# RAM kullanimi
$os          = Get-CimInstance -ClassName Win32_OperatingSystem
$totalRAM    = [math]::Round($os.TotalVisibleMemorySize / 1MB, 2)
$freeRAM     = [math]::Round($os.FreePhysicalMemory / 1MB, 2)
$usedRAM     = [math]::Round($totalRAM - $freeRAM, 2)
$ramPercent  = [math]::Round(($usedRAM / $totalRAM) * 100, 2)

# Disk kullanimi (tum sabit diskler)
$diskInfo = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType = 3" |
    ForEach-Object {
        $usedPercent = [math]::Round((($_.Size - $_.FreeSpace) / $_.Size) * 100, 2)
        "$($_.DeviceID):$usedPercent%"
    }
$diskSummary = $diskInfo -join "|"

# Network I/O (saniyedeki byte)
$netBefore = Get-CimInstance -ClassName Win32_PerfRawData_Tcpip_NetworkInterface |
    Where-Object { $_.Name -notlike "*Loopback*" } |
    Select-Object -First 1
Start-Sleep -Seconds 2
$netAfter  = Get-CimInstance -ClassName Win32_PerfRawData_Tcpip_NetworkInterface |
    Where-Object { $_.Name -notlike "*Loopback*" } |
    Select-Object -First 1

$networkMBps = 0
if ($netBefore -and $netAfter) {
    $bytesTotal  = ($netAfter.BytesTotalPersec - $netBefore.BytesTotalPersec)
    $networkMBps = [math]::Round($bytesTotal / 2 / 1MB, 3)
}

# CSV'ye yaz
$row = [PSCustomObject]@{
    Timestamp      = $timestamp
    CPU_Percent    = $cpuUsage
    RAM_Total_GB   = $totalRAM
    RAM_Used_GB    = $usedRAM
    RAM_Percent    = $ramPercent
    Disk_Usage     = $diskSummary
    Network_MBps   = $networkMBps
}

$csvExists = Test-Path $logFile
$row | Export-Csv -Path $logFile -Append -NoTypeInformation -Encoding UTF8

Write-Output "[$timestamp] Metrikler kaydedildi: CPU=$cpuUsage%, RAM=$ramPercent%"

AlertCheck.ps1 – Eşik Kontrol ve Alarm Scripti

Sadece veri toplamak yetmez. O verinin bizi uyarması gerekiyor. Bu script eşik değerlerini kontrol edip Windows Event Log’a yazar ve gerekirse e-posta gönderir.

# AlertCheck.ps1
param(
    [string]$ConfigPath = "C:MonitoringConfig.json"
)

$config    = Get-Content $ConfigPath | ConvertFrom-Json
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$alerts    = @()
$alertFile = Join-Path $config.Monitoring.LogPath "alerts_$(Get-Date -Format 'yyyyMMdd').log"

# Event Log kaynagi yoksa olustur
$sourceName = "CustomMonitoring"
if (-not [System.Diagnostics.EventLog]::SourceExists($sourceName)) {
    New-EventLog -LogName Application -Source $sourceName
}

function Write-Alert {
    param(
        [string]$Category,
        [string]$Level,
        [string]$Message,
        [double]$Value,
        [double]$Threshold
    )

    $alertMsg = "[$timestamp] [$Level] $Category: $Message (Deger: $Value, Esik: $Threshold)"
    Add-Content -Path $alertFile -Value $alertMsg

    $eventId = switch ($Level) {
        "WARNING"  { 1001 }
        "CRITICAL" { 1002 }
        default    { 1000 }
    }

    $entryType = if ($Level -eq "CRITICAL") { "Error" } else { "Warning" }
    Write-EventLog -LogName Application -Source $sourceName `
        -EventId $eventId -EntryType $entryType -Message $alertMsg

    return $alertMsg
}

# CPU kontrol
$cpuUsage = (Get-CimInstance -ClassName Win32_Processor |
    Measure-Object -Property LoadPercentage -Average).Average

if ($cpuUsage -ge $config.Thresholds.CPU_Critical) {
    $alerts += Write-Alert -Category "CPU" -Level "CRITICAL" `
        -Message "CPU kullanimi kritik seviyede!" `
        -Value $cpuUsage -Threshold $config.Thresholds.CPU_Critical
}
elseif ($cpuUsage -ge $config.Thresholds.CPU_Warning) {
    $alerts += Write-Alert -Category "CPU" -Level "WARNING" `
        -Message "CPU kullanimi yuksek" `
        -Value $cpuUsage -Threshold $config.Thresholds.CPU_Warning
}

# RAM kontrol
$os         = Get-CimInstance -ClassName Win32_OperatingSystem
$ramPercent = [math]::Round((1 - ($os.FreePhysicalMemory / $os.TotalVisibleMemorySize)) * 100, 2)

if ($ramPercent -ge $config.Thresholds.RAM_Critical) {
    $alerts += Write-Alert -Category "RAM" -Level "CRITICAL" `
        -Message "Bellek kullanimi kritik!" `
        -Value $ramPercent -Threshold $config.Thresholds.RAM_Critical
}
elseif ($ramPercent -ge $config.Thresholds.RAM_Warning) {
    $alerts += Write-Alert -Category "RAM" -Level "WARNING" `
        -Message "Bellek kullanimi yuksek" `
        -Value $ramPercent -Threshold $config.Thresholds.RAM_Warning
}

# Disk kontrol
Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType = 3" | ForEach-Object {
    $diskPercent = [math]::Round((($_.Size - $_.FreeSpace) / $_.Size) * 100, 2)
    $driveLetter = $_.DeviceID

    if ($diskPercent -ge $config.Thresholds.Disk_Critical) {
        $alerts += Write-Alert -Category "DISK-$driveLetter" -Level "CRITICAL" `
            -Message "$driveLetter surucusu dolmak uzere!" `
            -Value $diskPercent -Threshold $config.Thresholds.Disk_Critical
    }
    elseif ($diskPercent -ge $config.Thresholds.Disk_Warning) {
        $alerts += Write-Alert -Category "DISK-$driveLetter" -Level "WARNING" `
            -Message "$driveLetter disk dolulugu yuksek" `
            -Value $diskPercent -Threshold $config.Thresholds.Disk_Warning
    }
}

# Kritik alert varsa hemen mail gonder
if ($alerts | Where-Object { $_ -match "CRITICAL" }) {
    $body = "SUNUCU: $($config.Monitoring.ServerName)`n`n"
    $body += "KRITIK ALARMLAR:`n"
    $body += ($alerts | Where-Object { $_ -match "CRITICAL" }) -join "`n"

    try {
        $mailParams = @{
            SmtpServer = $config.Email.SmtpServer
            Port       = $config.Email.Port
            From       = $config.Email.From
            To         = $config.Email.To
            Subject    = "[KRITIK] $($config.Monitoring.ServerName) - Sistem Alarmi"
            Body       = $body
            UseSsl     = $config.Email.UseSSL
        }
        Send-MailMessage @mailParams
        Write-Output "Kritik alarm maili gonderildi."
    }
    catch {
        Write-Warning "Mail gonderilemedi: $_"
    }
}

Write-Output "Alert kontrolu tamamlandi. Toplam $($alerts.Count) uyari uretildi."

SendReport.ps1 – Gunluk Ozet Rapor

Her sabah saat 08:00’de bir önceki günün özetini iletmek için bu scripti kullanıyoruz. Yöneticiler işe geldiğinde gece boyunca ne olduğunu bir bakışta görüyor.

# SendReport.ps1
param(
    [string]$ConfigPath = "C:MonitoringConfig.json",
    [int]$ReportDays    = 1
)

$config      = Get-Content $ConfigPath | ConvertFrom-Json
$reportDate  = (Get-Date).AddDays(-$ReportDays)
$csvFile     = Join-Path $config.Monitoring.LogPath "metrics_$(($reportDate).ToString('yyyyMMdd')).csv"
$alertFile   = Join-Path $config.Monitoring.LogPath "alerts_$(($reportDate).ToString('yyyyMMdd')).log"

if (-not (Test-Path $csvFile)) {
    Write-Warning "Rapor icin veri bulunamadi: $csvFile"
    exit 1
}

$data = Import-Csv $csvFile

# Istatistikler
$cpuAvg  = [math]::Round(($data | Measure-Object -Property CPU_Percent -Average).Average, 2)
$cpuMax  = [math]::Round(($data | Measure-Object -Property CPU_Percent -Maximum).Maximum, 2)
$ramAvg  = [math]::Round(($data | Measure-Object -Property RAM_Percent -Average).Average, 2)
$ramMax  = [math]::Round(($data | Measure-Object -Property RAM_Percent -Maximum).Maximum, 2)

# Alert ozeti
$alertCount    = 0
$criticalCount = 0
if (Test-Path $alertFile) {
    $alertLines    = Get-Content $alertFile
    $alertCount    = ($alertLines | Where-Object { $_ -match "WARNING" }).Count
    $criticalCount = ($alertLines | Where-Object { $_ -match "CRITICAL" }).Count
}

$reportBody = @"
GUNLUK SISTEM RAPORU
Sunucu : $($config.Monitoring.ServerName)
Tarih  : $($reportDate.ToString('dd.MM.yyyy'))
Rapor  : $($data.Count) olcum noktasi analiz edildi.

PERFORMANS OZETI:
- CPU Ortalama   : %$cpuAvg
- CPU Maksimum   : %$cpuMax
- RAM Ortalama   : %$ramAvg
- RAM Maksimum   : %$ramMax

ALARM OZETI:
- Uyari (WARNING) : $alertCount adet
- Kritik (CRITICAL): $criticalCount adet

$(if ($criticalCount -gt 0) { "DIKKAT: Dun $criticalCount kritik alarm uretildi. Log dosyalarini incelemeniz onerilir." })

Bu rapor otomatik olarak olusturulmustur.
"@

$subject = "[Rapor] $($config.Monitoring.ServerName) - $(($reportDate).ToString('dd.MM.yyyy'))"
if ($criticalCount -gt 0) {
    $subject = "[KRITIK ALARM] " + $subject
}

try {
    Send-MailMessage -SmtpServer $config.Email.SmtpServer `
        -Port $config.Email.Port `
        -From $config.Email.From `
        -To $config.Email.To `
        -Subject $subject `
        -Body $reportBody `
        -UseSsl:$config.Email.UseSSL
    Write-Output "Gunluk rapor gonderildi."
}
catch {
    Write-Error "Rapor gonderilemedi: $_"
}

Scheduled Task Kurulumu

Scriptleri oluşturduk, şimdi sıra bunları Task Scheduler’a eklemekte. Bunu GUI üzerinden de yapabilirsiniz ama PowerShell ile yapmak hem daha hızlı hem de tekrarlanabilir. Yeni bir sunucuya geçtiğinizde tek komutla her şeyi kurabilirsiniz.

# Task Scheduler'a kayit - Admin olarak calistir

# 1. Metrik toplama - Her 5 dakikada bir
$action1 = New-ScheduledTaskAction `
    -Execute "powershell.exe" `
    -Argument "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File C:MonitoringCollectMetrics.ps1"

$trigger1 = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Minutes 5) `
    -Once -At (Get-Date)

$settings1 = New-ScheduledTaskSettingsSet `
    -ExecutionTimeLimit (New-TimeSpan -Minutes 4) `
    -RestartCount 3 `
    -RestartInterval (New-TimeSpan -Minutes 1) `
    -StartWhenAvailable `
    -RunOnlyIfNetworkAvailable:$false

Register-ScheduledTask `
    -TaskName "MonitoringCollectMetrics" `
    -TaskPath "CustomMonitoring" `
    -Action $action1 `
    -Trigger $trigger1 `
    -Settings $settings1 `
    -RunLevel Highest `
    -User "SYSTEM" `
    -Force

# 2. Alert kontrolu - Her 10 dakikada bir
$action2 = New-ScheduledTaskAction `
    -Execute "powershell.exe" `
    -Argument "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File C:MonitoringAlertCheck.ps1"

$trigger2 = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Minutes 10) `
    -Once -At (Get-Date)

Register-ScheduledTask `
    -TaskName "MonitoringAlertCheck" `
    -TaskPath "CustomMonitoring" `
    -Action $action2 `
    -Trigger $trigger2 `
    -Settings $settings1 `
    -RunLevel Highest `
    -User "SYSTEM" `
    -Force

# 3. Gunluk rapor - Her sabah 08:00
$action3 = New-ScheduledTaskAction `
    -Execute "powershell.exe" `
    -Argument "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File C:MonitoringSendReport.ps1"

$trigger3 = New-ScheduledTaskTrigger -Daily -At "08:00AM"

Register-ScheduledTask `
    -TaskName "MonitoringDailyReport" `
    -TaskPath "CustomMonitoring" `
    -Action $action3 `
    -Trigger $trigger3 `
    -Settings $settings1 `
    -RunLevel Highest `
    -User "SYSTEM" `
    -Force

Write-Output "Tum monitoring task'lari basariyla olusturuldu."

Log Temizleme ve Bakım

30 gün sonra log dosyaları birikmeye başlar. Bu scriptle eski logları otomatik olarak temizleyebilirsiniz. Bunu da haftalık bir task olarak ekleyin.

# CleanupLogs.ps1
param(
    [string]$ConfigPath   = "C:MonitoringConfig.json",
    [int]$RetentionDays   = 30
)

$config  = Get-Content $ConfigPath | ConvertFrom-Json
$logPath = $config.Monitoring.LogPath
$cutoff  = (Get-Date).AddDays(-$RetentionDays)

Get-ChildItem -Path $logPath -File | Where-Object { $_.LastWriteTime -lt $cutoff } | ForEach-Object {
    Remove-Item $_.FullName -Force
    Write-Output "Silindi: $($_.FullName)"
}

# Bos klasorleri de temizle
Get-ChildItem -Path $logPath -Directory | Where-Object {
    (Get-ChildItem $_.FullName).Count -eq 0
} | Remove-Item -Force

Write-Output "Log temizleme tamamlandi. Esik: $($cutoff.ToString('dd.MM.yyyy'))"

Pratik Ipuclari ve Sık Karşılaşılan Sorunlar

Üretim ortamlarında bu yapıyı kurduktan sonra birkaç noktada takılabilirsiniz.

Execution Policy sorunu: Script çalışmıyor diye task başarısız görünüyorsa ilk bakılacak yer execution policy. Task’ı -ExecutionPolicy Bypass parametresiyle çağırdığınızdan emin olun. Bu sistem genelinde policy’yi değiştirmiyor, sadece o oturum için bypass ediyor.

SYSTEM hesabıyla e-posta gönderme: SMTP kimlik doğrulaması gerektiren durumlarda SYSTEM hesabı sıkıntı çıkarabilir. Bu durumda ayrı bir servis hesabı oluşturup task’ı onunla çalıştırın. Şifreyi Get-Credential ile güvenli şekilde saklayıp Send-MailMessage -Credential parametresine verebilirsiniz.

Task çalışıyor ama sonuç yok: Task Scheduler’da görevin “Last Run Result” sütununa bakın. 0x1 dönüyorsa script hata vermiş demektir. Scripte Start-Transcript ekleyerek her çalışmada bir log oluşturun, hatayı yakalamak kolaylaşır.

Çoklu sunucu yönetimi: Aynı yapıyı 10-15 sunucuya yaymak için Invoke-Command ile remote olarak kurulum yapabilirsiniz. Config.json içindeki ServerName otomatik $env:COMPUTERNAME alıyor, bu yüzden her sunucuda ayrı yapılandırma gerekmez.

WMI timeout: Yoğun sunucularda WMI sorguları zaman zaman timeout yapabiliyor. CIM cmdlet’leri WMI’dan daha kararlı çalışıyor, bu yüzden Get-WmiObject yerine Get-CimInstance tercih edin. Zaten scriptlerde biz de bu yaklaşımı kullandık.

Disk doluluk alarmı çok sık geliyor: Eğer bir sürücü sürekli uyarı eşiğinin üzerinde kalıyorsa ve hemen müdahale edemiyorsanız, Config.json’da o sürücü için özel eşik tanımlayacak şekilde scripti genişletebilirsiniz. Ya da bilinen geçici bir durum için alertleri kısa süreliğine Disable-ScheduledTask ile susturabilirsiniz.

Gercek Dunya Senaryosu

Bir üretim sunucusunda bu yapıyı devreye aldıktan iki hafta sonra ilginç bir şeyle karşılaşmıştık. Gece 02:30’da RAM kullanımı %94’e fırlayıp 03:10’a kadar orada kalmış, sonra normale dönmüş. Kritik alarm maili gelmiş ama nöbetçi müdahale etmeden sorun kendiliğinden geçmiş. Ertesi sabah logları incelediğimizde bu pattern’i gördük. Tam o saatte çalışan bir backup process’inin belleği şişirdiği ortaya çıktı. Backup süresini ayarlayarak ve process’e bellek limiti koyarak sorunu çözdük. Eğer bu izleme sistemi olmasaydı, o gece ne olduğunu hiç bilemeyecektik.

İşte scheduled task tabanlı izlemenin en büyük değeri bu: olayları yaşanırken değil, yaşandıktan sonra analiz etmenizi sağlayan bir hafıza oluşturması.

Sonuc

Bu yazıda anlattığımız yapı, sıfır lisans maliyetiyle, tamamen Windows’un yerleşik araçlarıyla kurulabiliyor. Metrik toplama, eşik bazlı alarm, günlük raporlama ve log yönetimi olmak üzere dört temel bileşen bir araya gelince ortaya ciddi bir izleme altyapısı çıkıyor.

Tabii bu yapının sınırlılıkları var. Merkezi dashboard yok, trend analizi basit, çok sayıda sunucuyu yönetmek için ek otomasyon gerekiyor. Ama küçük-orta ölçekli ortamlar için, ya da büyük bir monitoring çözümünün henüz kurulmadığı geçiş dönemlerinde bu sistem son derece değerli.

Başlangıç olarak sadece CollectMetrics.ps1 ve tek bir scheduled task ile başlayın. Verinin biriktiğini, sistemin çalıştığını gördükçe diğer bileşenleri ekleyin. Mükemmeli bekleyerek başlamamak, bir şeyler eksikken bile başlamaktan her zaman daha iyi.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir