src/public/Logging/Search-AitherLog.ps1
|
#Requires -Version 7.0 <# .SYNOPSIS Search logs with advanced filtering and pattern matching .DESCRIPTION Advanced log search with regex support, multiple file search, and context lines. More powerful than Get-AitherLog for complex searches across multiple files and advanced pattern matching with regular expressions. This cmdlet searches through log files using regular expressions, supports context lines (showing surrounding log entries), and can search across multiple log files or date ranges. .PARAMETER Pattern Search pattern using regular expressions. This parameter is REQUIRED. Supports full .NET regular expression syntax. Examples: - 'error|failed' - Find entries containing "error" or "failed" - '^\[' - Find entries starting with "[" - 'timeout|timed out' - Find entries with timeout-related text - '\d{4}-\d{2}-\d{2}' - Find entries with date patterns - 'ERROR.*Invoke-AitherScript' - Find ERROR entries from Invoke-AitherScript Regular expression reference: - . matches any character - * matches zero or more - + matches one or more - | means OR - ^ means start of line - $ means end of line - \d matches digits - \w matches word characters .PARAMETER Path Log file or directory to search. If not specified, defaults to today's log file or the logs directory if -AllLogs is specified. Can be: - Single file: "aitherzero-2025-01-15.log" - Directory: "./library/logs" (searches all .log files in directory) - Absolute path: "C:\Logs\aitherzero.log" .PARAMETER Context Show N lines of context before and after matches. Default is 0 (no context). Useful for understanding the circumstances around log entries. Examples: - Context 3 - Shows 3 lines before and after each match - Context 5 - Shows 5 lines before and after each match Context lines help understand what happened before and after an error or event. .PARAMETER CaseSensitive Perform case-sensitive search. By default, searches are case-insensitive. Use this when case matters for your search pattern. .PARAMETER AllLogs Search all log files in the logs directory, not just today's log. When combined with -Days, searches logs from the specified number of days. Useful for: - Finding events across multiple days - Historical analysis - Comprehensive troubleshooting .PARAMETER Days Search logs from the last N days. Default is 1 (today only). Only applies when -AllLogs is specified or when searching a directory. Examples: - Days 7 - Search last week's logs - Days 30 - Search last month's logs .PARAMETER Export Export search results to a file. Results are exported as JSON format. Useful for: - Saving search results for later analysis - Sharing results with team members - Creating reports Example: -Export "./search-results.json" .INPUTS System.String You can pipe log file paths or search patterns to Search-AitherLog. .OUTPUTS PSCustomObject Returns search result objects with properties: - File: Log file name - LineNumber: Line number in the file - Line: Matching line content - ContextBefore: Lines before the match (if Context > 0) - ContextAfter: Lines after the match (if Context > 0) .EXAMPLE Search-AitherLog -Pattern 'error|failed' -Context 3 Searches for entries containing "error" or "failed" with 3 lines of context. .EXAMPLE Search-AitherLog -Pattern 'Invoke-AitherScript' -AllLogs -Days 7 Searches for all entries mentioning Invoke-AitherScript in the last 7 days. .EXAMPLE Search-AitherLog -Pattern '^\[' -CaseSensitive -Export './search-results.txt' Searches for lines starting with "[" (case-sensitive) and exports results. .EXAMPLE Search-AitherLog -Pattern '\d{4}-\d{2}-\d{2}.*ERROR' -AllLogs Searches for ERROR entries with date patterns across all logs. .EXAMPLE "error", "failed", "timeout" | Search-AitherLog -Context 2 Searches for multiple patterns by piping them. .NOTES More powerful than Get-AitherLog for complex searches across multiple files. Uses .NET regular expressions for pattern matching, providing full regex capabilities. Performance tips: - Use specific patterns to reduce search time - Limit -Days when searching large log archives - Use -Context sparingly as it increases processing time - Export results for large result sets instead of displaying .LINK Get-AitherLog Write-AitherLog Clear-AitherLog #> function Search-AitherLog { [OutputType([PSCustomObject])] [CmdletBinding()] param( [Parameter(Mandatory=$false, Position = 0, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string]$Pattern, [Parameter(ValueFromPipelineByPropertyName)] [string]$Path, [Parameter()] [ValidateRange(0, 100)] [int]$Context = 0, [Parameter()] [switch]$CaseSensitive, [Parameter()] [switch]$AllLogs, [Parameter()] [ValidateRange(1, 365)] [int]$Days = 1, [Parameter()] [string]$Export ) begin { $moduleRoot = Get-AitherModuleRoot $logsPath = Join-Path $moduleRoot 'AitherZero/library/logs' if (-not $Path) { if ($AllLogs) { $Path = $logsPath } else { $Path = Join-Path $logsPath "aitherzero-$(Get-Date -Format 'yyyy-MM-dd').log" } } } process { try { $files = @() if (Test-Path $Path -PathType Container) { # Directory - get log files $cutoffDate = (Get-Date).AddDays(-$Days) $files = Get-ChildItem -Path $Path -Filter '*.log' -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -ge $cutoffDate } } elseif (Test-Path $Path) { # Single file $files = @(Get-Item $Path) } else { # Only show warning if user explicitly specified a path (not using default) if ($PSBoundParameters.ContainsKey('Path')) { Write-AitherLog -Level Warning -Message "Path not found: $Path" -Source 'Search-AitherLog' } else { # Silent when default path doesn't exist (normal when no logs yet) Write-Verbose "Log file not found (no logs written yet): $Path" } return @() } $results = @() $regexOptions = if ($CaseSensitive) { [System.Text.RegularExpressions.RegexOptions]::None } else { [System.Text.RegularExpressions.RegexOptions]::IgnoreCase } foreach ($file in $files) { $lines = Get-Content -Path $file.FullName -ErrorAction SilentlyContinue $lineNumber = 0 foreach ($line in $lines) { $lineNumber++ if ([regex]::IsMatch($line, $Pattern, $regexOptions)) { $result = [PSCustomObject]@{ File = $file.Name LineNumber = $lineNumber Line = $line ContextBefore = @() ContextAfter = @() } # Add context if ($Context -gt 0) { $start = [Math]::Max(0, $lineNumber - $Context - 1) $end = [Math]::Min($lines.Count - 1, $lineNumber + $Context - 1) for ($i = $start; $i -le $end; $i++) { if ($i -lt $lineNumber - 1) { $result.ContextBefore += $lines[$i] } elseif ($i -gt $lineNumber - 1) { $result.ContextAfter += $lines[$i] } } } $results += $result } } } # Export if requested if ($Export) { $results | ConvertTo-Json -Depth 10 | Set-Content -Path $Export Write-AitherLog -Level Information -Message "Results exported to: $Export" -Source 'Search-AitherLog' } return $results } catch { Invoke-AitherErrorHandler -ErrorRecord $_ -Operation "Searching logs with pattern: $Pattern" -Parameters $PSBoundParameters -ThrowOnError } } } |