############################################################# #### Prometheus Windows Exporter Installer #### #### For Windows Server 2016+ and Windows 10/11 #### #### #### #### Author: Phil Connor #### #### Contact: contact@mylinux.work #### #### License: MIT #### #### Version: 1.0 #### #### #### #### Usage: .\install-windows-exporter.ps1 [OPTIONS] #### ############################################################# param( [string]$ServerType = "standard", # standard, iis, sql, ad, rdp, all [string]$ExtraCollectors = "", # additional collectors (comma-separated) [string]$RemoveCollectors = "", # collectors to remove from the profile [int]$Port = 9182, [string]$InstallDir = "C:\Program Files\windows_exporter", [string]$PrometheusIP = "", # restrict firewall to this IP [switch]$NoFirewall, [switch]$NoDefenderExclusion, [switch]$Update, [switch]$Uninstall, [switch]$DryRun, [switch]$Help ) $ErrorActionPreference = "Stop" # ============================================================================ # CONFIGURATION # ============================================================================ $ServiceName = "windows_exporter" $ServiceDisplayName = "Prometheus Windows Exporter" $FirewallRuleName = "Prometheus Windows Exporter (TCP-In)" $GitHubApiUrl = "https://api.github.com/repos/prometheus-community/windows_exporter/releases/latest" $TextFileDir = Join-Path $InstallDir "textfile_inputs" $TargetExe = Join-Path $InstallDir "windows_exporter.exe" # ============================================================================ # HELPER FUNCTIONS # ============================================================================ function Write-ColorOutput { param([string]$Message, [string]$Color = "Green") $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Write-Host "[$timestamp] $Message" -ForegroundColor $Color } function Show-Help { Write-Host @" Prometheus Windows Exporter Installer v1.0 https://mylinux.work USAGE: .\install-windows-exporter.ps1 [OPTIONS] OPTIONS: -ServerType Server profile: standard, iis, sql, ad, rdp, all (default: standard) -ExtraCollectors Additional collectors, comma-separated (e.g. "service,hyperv") -RemoveCollectors Remove collectors from the profile (e.g. "tcp,time") -Port Metrics listen port (default: 9182) -InstallDir Installation directory (default: C:\Program Files\windows_exporter) -PrometheusIP Restrict firewall rule to this source IP -NoFirewall Skip firewall rule creation -NoDefenderExclusion Skip Windows Defender exclusion -Update Update existing installation to latest version -Uninstall Remove windows_exporter completely -DryRun Show what would be done without making changes -Help Show this help SERVER PROFILES: standard [defaults],process,tcp,time iis [defaults],process,tcp,time,iis sql [defaults],process,tcp,time,mssql ad [defaults],process,tcp,time,ad,dns rdp [defaults],process,tcp,time,remote_fx all [defaults],process,tcp,time,iis,mssql,ad,dns,remote_fx [defaults] = cpu,cs,logical_disk,os,system,net,cache,logon,memory EXAMPLES: # Standard install .\install-windows-exporter.ps1 # IIS server with restricted firewall .\install-windows-exporter.ps1 -ServerType iis -PrometheusIP "10.0.0.5" # SQL server with extra service collector .\install-windows-exporter.ps1 -ServerType sql -ExtraCollectors "service" # Update to latest version .\install-windows-exporter.ps1 -Update # Uninstall .\install-windows-exporter.ps1 -Uninstall "@ } # ============================================================================ # ADMINISTRATOR CHECK # ============================================================================ function Test-Administrator { $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($currentUser) return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } # ============================================================================ # SERVER PROFILE BUILDER # ============================================================================ function Get-CollectorString { $defaultCollectors = @("cpu", "cs", "logical_disk", "os", "system", "net", "cache", "logon", "memory") $profileCollectors = @("process", "tcp", "time") switch ($ServerType.ToLower()) { "iis" { $profileCollectors += @("iis") } "sql" { $profileCollectors += @("mssql") } "ad" { $profileCollectors += @("ad", "dns") } "rdp" { $profileCollectors += @("remote_fx") } "all" { $profileCollectors += @("iis", "mssql", "ad", "dns", "remote_fx") } "standard" { } default { Write-ColorOutput "WARNING: Unknown server type '$ServerType', using 'standard'" "Yellow" } } $allCollectors = $defaultCollectors + $profileCollectors # Add extra collectors if ($ExtraCollectors) { $extras = $ExtraCollectors -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" } foreach ($col in $extras) { if ($allCollectors -notcontains $col) { $allCollectors += $col } } } # Remove unwanted collectors if ($RemoveCollectors) { $removals = $RemoveCollectors -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" } $allCollectors = $allCollectors | Where-Object { $removals -notcontains $_ } } return ($allCollectors | Select-Object -Unique) -join "," } # ============================================================================ # GITHUB RELEASE FUNCTIONS # ============================================================================ function Get-LatestRelease { Write-ColorOutput "Querying GitHub for latest release..." try { $release = Invoke-RestMethod -Uri $GitHubApiUrl -UseBasicParsing $asset = $release.assets | Where-Object { $_.name -match "amd64" -and $_.name -match "\.exe$" } | Select-Object -First 1 if (-not $asset) { Write-ColorOutput "ERROR: Could not find amd64 .exe asset in latest release" "Red" exit 1 } return @{ Version = $release.tag_name DownloadUrl = $asset.browser_download_url AssetName = $asset.name } } catch { Write-ColorOutput "ERROR: Failed to query GitHub API: $($_.Exception.Message)" "Red" exit 1 } } # ============================================================================ # SERVICE MANAGEMENT # ============================================================================ function Stop-ExistingService { $service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue if ($service) { Write-ColorOutput "Stopping existing service..." Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue Start-Sleep -Seconds 3 } } function Remove-ExistingService { $service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue if ($service) { Stop-ExistingService Write-ColorOutput "Deleting existing service..." sc.exe delete $ServiceName | Out-Null Start-Sleep -Seconds 2 } } function New-ExporterService { param([string]$CollectorString) $serviceArgs = @( "--log.file=eventlog", "--web.listen-address=0.0.0.0:$Port", "--collector.textfile.directories=`"$TextFileDir`"", "--collectors.enabled=$CollectorString" ) $binaryPath = "`"$TargetExe`" " + ($serviceArgs -join " ") Write-ColorOutput "Creating service with collectors: $CollectorString" "Cyan" if ($DryRun) { Write-ColorOutput "[DRY RUN] Would create service: $binaryPath" "Yellow" return } New-Service -Name $ServiceName ` -BinaryPathName $binaryPath ` -DisplayName $ServiceDisplayName ` -StartupType Automatic ` -Description "Prometheus metrics exporter for Windows (managed by install-windows-exporter.ps1)" | Out-Null Write-ColorOutput "Service created successfully" } # ============================================================================ # FIREWALL MANAGEMENT # ============================================================================ function Add-FirewallRule { if ($NoFirewall) { Write-ColorOutput "Skipping firewall rule (-NoFirewall specified)" "Yellow" return } # Remove existing rule if present $existing = Get-NetFirewallRule -DisplayName $FirewallRuleName -ErrorAction SilentlyContinue if ($existing) { Remove-NetFirewallRule -DisplayName $FirewallRuleName -ErrorAction SilentlyContinue Write-ColorOutput "Removed existing firewall rule" } if ($DryRun) { $scope = if ($PrometheusIP) { $PrometheusIP } else { "Any" } Write-ColorOutput "[DRY RUN] Would create firewall rule: port $Port, source: $scope" "Yellow" return } $params = @{ DisplayName = $FirewallRuleName Direction = "Inbound" Protocol = "TCP" LocalPort = $Port Action = "Allow" Profile = "Any" Description = "Allow Prometheus to scrape windows_exporter metrics on port $Port" } if ($PrometheusIP) { $params.RemoteAddress = $PrometheusIP Write-ColorOutput "Creating firewall rule: port $Port restricted to $PrometheusIP" } else { Write-ColorOutput "Creating firewall rule: port $Port open to all sources" } New-NetFirewallRule @params | Out-Null Write-ColorOutput "Firewall rule created" } function Remove-FirewallRule { $existing = Get-NetFirewallRule -DisplayName $FirewallRuleName -ErrorAction SilentlyContinue if ($existing) { Remove-NetFirewallRule -DisplayName $FirewallRuleName Write-ColorOutput "Firewall rule removed" } else { Write-ColorOutput "No firewall rule found to remove" "Yellow" } } # ============================================================================ # DEFENDER EXCLUSION # ============================================================================ function Add-DefenderExclusion { if ($NoDefenderExclusion) { Write-ColorOutput "Skipping Defender exclusion (-NoDefenderExclusion specified)" "Yellow" return } if ($DryRun) { Write-ColorOutput "[DRY RUN] Would add Defender exclusion for: $InstallDir" "Yellow" return } try { Add-MpPreference -ExclusionPath $InstallDir -ErrorAction Stop Write-ColorOutput "Windows Defender exclusion added for: $InstallDir" } catch { Write-ColorOutput "WARNING: Could not add Defender exclusion: $($_.Exception.Message)" "Yellow" } } function Remove-DefenderExclusion { try { Remove-MpPreference -ExclusionPath $InstallDir -ErrorAction Stop Write-ColorOutput "Windows Defender exclusion removed for: $InstallDir" } catch { Write-ColorOutput "WARNING: Could not remove Defender exclusion: $($_.Exception.Message)" "Yellow" } } # ============================================================================ # VERIFICATION # ============================================================================ function Test-MetricsEndpoint { Write-ColorOutput "Waiting for service to start..." Start-Sleep -Seconds 5 $metricsUrl = "http://localhost:$Port/metrics" try { $response = Invoke-WebRequest -Uri $metricsUrl -UseBasicParsing -TimeoutSec 10 if ($response.StatusCode -eq 200) { Write-ColorOutput "Metrics endpoint responding at $metricsUrl" "Green" return $true } } catch { Write-ColorOutput "WARNING: Metrics endpoint not responding at $metricsUrl" "Yellow" Write-ColorOutput "The service may still be starting. Check: Get-Service $ServiceName" "Yellow" return $false } return $false } # ============================================================================ # UNINSTALL MODE # ============================================================================ function Invoke-Uninstall { Write-ColorOutput "=== Uninstalling Windows Exporter ===" "Cyan" $service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue if (-not $service -and -not (Test-Path $InstallDir)) { Write-ColorOutput "Windows Exporter does not appear to be installed" "Yellow" exit 0 } Write-Host "" Write-Host "This will remove:" -ForegroundColor Yellow Write-Host " - Service: $ServiceName" Write-Host " - Directory: $InstallDir" Write-Host " - Firewall rule: $FirewallRuleName" Write-Host " - Defender exclusion: $InstallDir" Write-Host "" if (-not $DryRun) { $confirm = Read-Host "Are you sure? (y/N)" if ($confirm -ne "y" -and $confirm -ne "Y") { Write-ColorOutput "Uninstall cancelled" "Yellow" exit 0 } } if ($DryRun) { Write-ColorOutput "[DRY RUN] Would uninstall windows_exporter" "Yellow" return } # Stop and remove service Remove-ExistingService # Remove firewall rule Remove-FirewallRule # Remove Defender exclusion Remove-DefenderExclusion # Remove install directory if (Test-Path $InstallDir) { Remove-Item -Path $InstallDir -Recurse -Force Write-ColorOutput "Install directory removed: $InstallDir" } Write-Host "" Write-ColorOutput "=== Windows Exporter uninstalled successfully ===" "Green" } # ============================================================================ # UPDATE MODE # ============================================================================ function Invoke-Update { Write-ColorOutput "=== Updating Windows Exporter ===" "Cyan" $service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue if (-not $service) { Write-ColorOutput "ERROR: Service '$ServiceName' not found. Use install mode instead." "Red" exit 1 } $release = Get-LatestRelease Write-ColorOutput "Latest version: $($release.Version)" # Check installed version if binary exists if (Test-Path $TargetExe) { try { $installedVersion = (Get-Item $TargetExe).VersionInfo.ProductVersion if ($installedVersion) { Write-ColorOutput "Installed version: $installedVersion" $latestClean = $release.Version -replace '^v', '' if ($installedVersion -eq $latestClean) { Write-ColorOutput "Already running the latest version. No update needed." "Green" exit 0 } } } catch { Write-ColorOutput "Could not determine installed version, proceeding with update" "Yellow" } } if ($DryRun) { Write-ColorOutput "[DRY RUN] Would update to $($release.Version)" "Yellow" return } # Download new binary $tempExe = Join-Path $env:TEMP "windows_exporter_update.exe" Write-ColorOutput "Downloading $($release.AssetName)..." Invoke-WebRequest -Uri $release.DownloadUrl -OutFile $tempExe -UseBasicParsing # Stop service, replace binary, start service Stop-ExistingService Copy-Item -Path $tempExe -Destination $TargetExe -Force Remove-Item $tempExe -Force -ErrorAction SilentlyContinue Write-ColorOutput "Binary updated" Start-Service -Name $ServiceName $verified = Test-MetricsEndpoint Write-Host "" Write-ColorOutput "=== Update Summary ===" "Cyan" Write-ColorOutput "Version: $($release.Version)" Write-ColorOutput "Status: $(if ($verified) { 'Verified' } else { 'Started (verification pending)' })" Write-ColorOutput "Metrics URL: http://localhost:$Port/metrics" } # ============================================================================ # INSTALL MODE # ============================================================================ function Invoke-Install { Write-ColorOutput "=== Installing Windows Exporter ===" "Cyan" Write-ColorOutput "Server type: $ServerType" Write-ColorOutput "Port: $Port" Write-ColorOutput "Install dir: $InstallDir" # --- Create directories --- if (-not $DryRun) { if (-not (Test-Path $InstallDir)) { New-Item -Path $InstallDir -ItemType Directory -Force | Out-Null Write-ColorOutput "Created directory: $InstallDir" } if (-not (Test-Path $TextFileDir)) { New-Item -Path $TextFileDir -ItemType Directory -Force | Out-Null Write-ColorOutput "Created directory: $TextFileDir" } } else { Write-ColorOutput "[DRY RUN] Would create: $InstallDir" "Yellow" Write-ColorOutput "[DRY RUN] Would create: $TextFileDir" "Yellow" } # --- Defender exclusion --- Add-DefenderExclusion # --- Get latest release --- $release = Get-LatestRelease Write-ColorOutput "Latest version: $($release.Version)" # --- Check if already installed with same version --- if (Test-Path $TargetExe) { try { $installedVersion = (Get-Item $TargetExe).VersionInfo.ProductVersion $latestClean = $release.Version -replace '^v', '' if ($installedVersion -and $installedVersion -eq $latestClean) { Write-ColorOutput "Version $installedVersion already installed, skipping download" "Yellow" $skipDownload = $true } } catch { } } # --- Download binary --- $tempExe = Join-Path $env:TEMP "windows_exporter_install.exe" if (-not $skipDownload) { Write-ColorOutput "Downloading $($release.AssetName)..." if (-not $DryRun) { Invoke-WebRequest -Uri $release.DownloadUrl -OutFile $tempExe -UseBasicParsing Write-ColorOutput "Download complete" } else { Write-ColorOutput "[DRY RUN] Would download: $($release.DownloadUrl)" "Yellow" } } # --- Stop and remove existing service --- Remove-ExistingService # --- Copy binary --- if (-not $skipDownload -and -not $DryRun) { Copy-Item -Path $tempExe -Destination $TargetExe -Force Remove-Item $tempExe -Force -ErrorAction SilentlyContinue Write-ColorOutput "Binary installed to: $TargetExe" } # --- Build collector string --- $collectorString = Get-CollectorString # --- Create service --- New-ExporterService -CollectorString $collectorString # --- Firewall rule --- Add-FirewallRule # --- Start service --- if (-not $DryRun) { Write-ColorOutput "Starting service..." Start-Service -Name $ServiceName $verified = Test-MetricsEndpoint } # --- Summary --- Write-Host "" Write-ColorOutput "=== Installation Summary ===" "Cyan" Write-ColorOutput "Version: $($release.Version)" Write-ColorOutput "Binary: $TargetExe" Write-ColorOutput "Port: $Port" Write-ColorOutput "Collectors: $collectorString" Write-ColorOutput "Textfile dir: $TextFileDir" Write-ColorOutput "Firewall: $(if ($NoFirewall) { 'Skipped' } elseif ($PrometheusIP) { "Restricted to $PrometheusIP" } else { 'Open' })" Write-ColorOutput "Defender: $(if ($NoDefenderExclusion) { 'Skipped' } else { 'Exclusion added' })" if (-not $DryRun) { Write-ColorOutput "Status: $(if ($verified) { 'Verified - metrics endpoint responding' } else { 'Started (verification pending)' })" Write-ColorOutput "Verify URL: http://localhost:$Port/metrics" "Cyan" } Write-Host "" Write-ColorOutput "=== Installation complete ===" "Green" } # ============================================================================ # MAIN EXECUTION # ============================================================================ # Show help if ($Help) { Show-Help exit 0 } # Check administrator privileges if (-not (Test-Administrator)) { Write-ColorOutput "ERROR: This script must be run as Administrator" "Red" Write-Host "Right-click PowerShell and select 'Run as administrator'" -ForegroundColor Yellow exit 1 } if ($DryRun) { Write-ColorOutput "=== DRY RUN MODE - No changes will be made ===" "Yellow" Write-Host "" } # Route to the appropriate mode if ($Uninstall) { Invoke-Uninstall } elseif ($Update) { Invoke-Update } else { Invoke-Install }