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.
