Get-NixLog.ps1

function Get-NixLog
{
    <#
    .Synopsis
        Gets log information from journalctl or syslog formatted logs.
    .Description
        Gets log via journalctl produced by the journald service which is standard on all distros that use systemd.
        If the -LogFilePath parameter is used then the command uses regex to parse in the Syslog format.
        Otherwise
    .Example
        # returns all logs unfiltered from journald
        Get-NigLog

        CURSOR : s=1032b59ef7f247819e0500ebe21424d3;i=19ac95;b=cab0af3b917244288fe2a15fc3b01
                                    a42;m=1767c381e6c;t=5c65195d4a866;x=2e08686703b082cb
        REALTIME_TIMESTAMP : 1625428591945830
        MONOTONIC_TIMESTAMP : 1608401821292
        BOOT_ID : <someBootID>
        MACHINE_ID : <someGuid>
        HOSTNAME : ubuntu
        SELINUX_CONTEXT : unconfined

        SYSTEMD_SLICE : system.slice
        SYSLOG_FACILITY : 3
        TRANSPORT : journal
        PRIORITY : 4
        CODE_FILE : ../src/resolve/resolved-dns-transaction.c
        CODE_LINE : 1047
        CODE_FUNC : dns_transaction_process_reply
        SYSLOG_IDENTIFIER : systemd-resolved
        MESSAGE : Server returned error NXDOMAIN, mitigating potential DNS violation
                                    DVE-2018-0001, retrying transaction with reduced feature level UDP.
        PID : 899
        UID : 101
        GID : 103
        COMM : systemd-resolve
        EXE : /lib/systemd/systemd-resolved
        CMDLINE : /lib/systemd/systemd-resolved
        CAP_EFFECTIVE : 0
        SYSTEMD_CGROUP : /system.slice/systemd-resolved.service
        SYSTEMD_UNIT : systemd-resolved.service
        SYSTEMD_INVOCATION_ID : <someSystemD_ID>
        SOURCE_REALTIME_TIMESTAMP : 1625428591945762

    .Example
        # Returns logs from the /var/log/syslog in the following format
        Get-NixLog -LogFilePath /var/log/syslog

        DATE : Jun 20 19:56:14
        HOSTNAME : ubuntu
        PROCESS : powershell
        PID : 16195
        MESSAGE : {(7.1.3:1:80)
                [Perftrack_ConsoleStartupStart:PowershellConsoleStartup.WinStart.Informational] PowerShell
                console is starting up, }
    .Example
        # Uses native journalctl filtering to limit logs to a specifc time and priority
        # Like other PowerShell commands filtering on the cmdlet is faster than filtering later in the pipeline
        Get-NixLog -After "2021-06-21 00:00:00" -Priority err,warning | select -first 10

    .Example
        # Group all logs in the current syslog file by minute
        Get-NixLog -LogFilePath '/var/log/syslog' | Group-Object {$_.date.minute}

    #>

    [OutputType([Nullable], [string])]
    [Cmdletbinding(DefaultParameterSetName='Journalctl')]
    param(
    # The path to the logfile read by.
    [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='LogFile')]
    [Alias("Fullname")]
    [ValidateScript(
        {
            #Faster method for Resolve-Path
            $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($PSItem) -as [bool]
        }
    )]
    [string]
    $LogFilePath,

    # A Time based filter for logs before a date. Example timestamp is "2020-04-08 17:12:00"
    # Could also be yesterday, today, tomorrow
    # [datetime]::now.ToString('s').Replace('T',' ')
    [Parameter(ParameterSetName='Journalctl',ValueFromPipelineByPropertyName)]
    [string]
    [alias('until')]
    $Before,

    # A Time based filter for logs after a date. Example timestamp is "2020-04-08 17:12:00"
    # Could also be yesterday, today, tomorrow
    # [datetime]::now.ToString('s').Replace('T',' ')
    [Parameter(ParameterSetName='Journalctl',ValueFromPipelineByPropertyName)]
    [string]
    [alias('since')]
    $After,

    # Explicitly require logs in UTC
    [Parameter(ParameterSetName='Journalctl',ValueFromPipelineByPropertyName)]
    [switch]
    $UTC,

    # Request only kernel logs like dmesg
    [Parameter(ParameterSetName='Journalctl',ValueFromPipelineByPropertyName)]
    [switch]
    [alias('dmesg')]
    $KernalOnly,

    # Offset number of the boot logs to look at. 0 is current boot, -1 is previous, etc.
    [Parameter(ParameterSetName='Journalctl',ValueFromPipelineByPropertyName)]
    [int]
    [alias('b')]
    $Boot,

    # Number of lines to show
    [Parameter(ParameterSetName='Journalctl',ValueFromPipelineByPropertyName)]
    [uint]
    [alias('n')]
    $LineNumber,

    # The specific syslog identifier you are looking at filtering. Can provided multiple identifiers
    [Parameter(ParameterSetName='Journalctl',ValueFromPipelineByPropertyName)]
    [string[]]
    $Identifier,

    # The specific Systemd unit you are looking at filtering. Can provided multiple units
    [Parameter(ParameterSetName='Journalctl',ValueFromPipelineByPropertyName)]
    [string[]]
    $Unit,

    # The priority of the logs you want to retrieve.
    # "emerg" (0), "alert" (1), "crit" (2), "err" (3), "warning" (4), "notice" (5), "info" (6), "debug" (7).
    [Parameter(ParameterSetName='Journalctl',ValueFromPipelineByPropertyName)]
    [string[]]
    $Priority
    )

    begin{
        # Contains patterns to identify the logging type
        # Name captures will be declared as variables
        # for example (?<hostname>\w+) will populate hostname
        $logPatterns = [ordered]@{
            Syslog = '(?<date>^\w.+:\d{2})\s(?<hostname>[a-zA-Z0-9][-a-zA-Z0-9_]+)\s(?<process>.+?:)\s(?<message>.*)'
        }
        # Contains the script blocks to handle a given log type
        $logReaders = [ordered]@{
            Syslog = {  #Match a pid for the format [<number>] and assign it to the group process_pid
                        $process_pid = if($process -match '(?<process_pid>(?<=\[)(?>.+\d)(?=\]))' ){$matches.process_pid -as [int]} else{$null}
                        #Match a process until a [ or :
                        $process_name = if($process -match '(?<process_name>^.+\w(?=\[|:))'){$matches.process_name}
                        # Single digit days of the month have a leading space instead of a zero
                        $date_parse = if($date -match '\s{2}\d'){'MMM d HH:mm:ss'} else { 'MMM dd HH:mm:ss' }
                        [PSCustomObject][ordered]@{
                            PsTypeName = "PowerNix.Log.Syslog"
                            DATE =  [datetime]::ParseExact($date, $date_parse, [CultureInfo]::InvariantCulture)
                            HOSTNAME = $hostname
                            PROCESS = $process_name
                            PID = $process_pid
                            MESSAGE = $message
                        }
                     }
        }
    }
    process {
        if($LogFilePath) {
            $isfirstline = $true
            $logReader = ''
            $LogFileContent = Get-Content -Path $LogFilePath
            $LogFileContent |
            & { process {
                if($isfirstline) {
                    $isfirstline = $false
                    # look through each log file pattern
                    foreach ($kv in $logPatterns.GetEnumerator()){
                        # if the pattern matches select the log type
                        if( $PSItem -match $kv.Value ) {
                            $logReader = $kv.Key
                            break
                        }
                    }
                    if (-not $logReader) {
                        Write-Error -Message "Unable to parse log" -TargetObject $LogFilePath
                    }
                }

                if (-not $logPatterns[$logReader] -or -not $logReaders[$logReader]){
                    return
                }

                foreach ($match in [regex]::Matches($PSItem, $logPatterns[$logReader]) ){
                    foreach ($group in $match.groups){
                        $ExecutionContext.SessionState.PSVariable.Set($group.name, $group.value)
                    }
                    & $logReaders[$logReader]
                }
            } }
        } else {
            # check that the system is using systemd
            if (-not (pidof systemd)) {
                Write-Error "Systemd not detected must use -LogFilePath" -ErrorId File.Missing
                return
            }
            $journalArgs = @('-r','-o','json'
            if($UTC){"--utc"}
            if($KernalOnly){"--dmesg"}
            if($Boot){"-b","$Boot"}
            if($LineNumber){"-n","$LineNumber"}
            if($Identifier){foreach($i in $Identifier){"-t","$i"}}
            if($Unit){foreach($u in $Unit){"-u","$u"}}
            if($Before){"-U","`"$Before`""}
            if($After){"-S","`"$After`""}
            if($Priority)
            {
                if ($Priority.Count -gt 1)
                {
                    "-p","$($priority[0])..$($priority[-1])"
                } else
                {
                    "-p","$Priority"
                }
            }
            )#end of the array
            # Invoking journalctl with arguements from parameters.
            $results = journalctl @journalArgs
            convertfrom-json -InputObject $results |
            & {
                process {
                    $journalcltLogs = [Ordered]@{PSTypeName='PowerNix.Logs'} # create a dictionary to hold logs.
                    foreach($v in $psitem.psobject.properties) {
                        $name = $($v.name -replace '^_{1,2}') # Removing all leading underscores
                        $value = $v.value
                        $journalcltLogs[$name] = $value
                    }
                    [PSCustomObject]$journalcltLogs
                }
            }
        }
    }
}