CM.Logger.psm1

#region Enums
Enum LogEntryLevel {
    Verbose = 0
    Information = 1
    Warning = 2
    Error = 3
}

Enum Where {
    Default = 0
    First = 1
    Last = 2
    SkipUntil = 3
    Until = 4
    Split = 5
}

Enum LogEntryType {
    Message
    Data
}
#endregion

#region Attributes
[AttributeUsage(
    [AttributeTargets]::All, 
    AllowMultiple
)]
class LogFamilyAttribute : Attribute {
    [string] $Family = 
        "DefaultLogFamily"
    [string] $Path
}
#endregion

#region Classes
    #region Log Class
Class Log {
        #region Hidden Properties
    hidden [string] $Name
    hidden [string] $Path
    hidden [bool] $IsFamily
    hidden [int] $MaxMessageSize
        #endregion

        #region Constructor(s)
    hidden Log(
        [string] $N, 
        [string] $P, 
        [bool] $I, 
        [int] $M, 
        [bool] $Append
    ) {         
        $this.
            Name =
                (
                    $N = 
                        $N -replace 
                            '<|>', 
                            '_'
                )
        $JoinPath =
            @{
                Path =
                    $P
                ChildPath = 
                    "$N.log"
            }
        $this.
            Path =
                Join-Path @JoinPath
        $this.
            IsFamily =
                $I
        $this.
            MaxMessageSize = 
                $M
        if (
            -not 
                $Append
        ) { 
            $RemoveItem =
                @{
                    Path =
                        $this.
                            Path 
                    Force =
                        $true
                }
            Remove-Item @RemoveItem
        }
    }
        #endregion

        #region Hidden Methods
    hidden [void] TestAndRollOver(        
    ) {
        $GetItem =
            @{
                Path =
                    $this.Path
            }
        if (
            (
                Get-Item @GetItem
            ).
                Length -ge 
                    [Logger]::RollOverSize
        ) {
            $GetDate =
                @{ 
                    Format =
                        'yyyyMMdd-HHmmss'
                }
            $RenameItem =
                @{
                    Path =
                        $this.
                            Path 
                    NewName =
                        (
                            $this.
                                Path -replace 
                                    '\.log$', 
                                    "-$(
                                        Get-Date @GetDate
                                    ).log"

                        ) 
                    Force =
                        $true
                }
            Rename-Item @RenameItem
        }
    }
    
    hidden [string] Write(
        [string] $Message, 
        [object] $Data, 
        [LogEntryLevel] $EntryLevel
    ) { 
        $Return = 
            $this.
                Write(
                    $Message,
                    $Data,
                    $EntryLevel,
                    $this.
                        Name
                ) 
        [Logger]::ClearCalls(            
        )
        return $Return
    }

    hidden [string] Write(
        [string] $Message, 
        [object] $Data, 
        [LogEntryLevel] $EntryLevel, 
        [string] $Component
    ) {
        $this.
            TestAndRollOver(                
            )
        $Message = 
            $Message -replace 
                '"', 
                '`"'
        $AddContent =
            @{
                Path =
                    $this.
                        Path
                Value =
                    $this.
                        NewLogLine(
                            $Message,
                            [LogEntryType]::Message,
                            $EntryLevel,
                            $Component,
                            [Logger]::GetCall(                    
                            )
                        )
            } 
        Add-Content @AddContent
        if (
            $null -ne 
                $Data
        ) { 
            $ConvertToJson =
                @{
                    InputObject =
                        $Data
                }
            $AddContent =
                @{
                    Path =
                        $this.
                            Path
                    Value =
                        $this.
                            NewLogLine(
                                (
                                    ConvertTo-Json @ConvertToJson
                                ),
                                [LogEntryType]::Data,
                                $EntryLevel,
                                $Component,
                                [Logger]::GetCall(                    
                                )
                            )
                } 
            Add-Content @AddContent
        }
        if (
            !$this.
                IsFamily
        ) { 
            $ForEachObject =
                @{
                    InputObject =
                        [Logger]::Family(                            
                        )
                    Process =
                        {
                            $_.
                                Write(
                                    $Message,
                                    $Data,
                                    $EntryLevel,
                                    $this.
                                        Name
                                )
                        }
                }
            ForEach-Object @ForEachObject
        }
        return $Message
    }

    hidden [string] NewLogLine(
        [string] $Entry, 
        [LogEntryType] $EntryType, 
        [LogEntryLevel] $EntryLevel, 
        [string] $component, 
        [System.Management.Automation.CallStackFrame] $Caller 
    ) {
        $Now = 
            [DateTime]::Now
        $SB = 
            [System.Text.StringBuilder]::new(                
            )
        $Index = 
            -1
        while (
            (
                ++$Index
            ) * 
                $this.
                    MaxMessageSize -lt 
                        $Entry.
                            Length
        ) {
            $IndexTimesMaxMessageSize =
                $Index *
                    $this.
                        MaxMessageSize
            $SubEntry = 
                if (
                    $Entry.
                        Length - 
                            $IndexTimesMaxMessageSize -ge 
                                $this.
                                    MaxMessageSize
                ) {
                    $Entry.
                        Substring(
                            $IndexTimesMaxMessageSize,
                            $this.
                                MaxMessageSize
                        ).
                            PadRight(
                                $this.
                                    MaxMessageSize + 
                                        3, 
                                '.'
                            )
                }
                else {
                    $Entry.
                        Substring(
                            $IndexTimesMaxMessageSize,
                            $Entry.
                                Length - 
                                    $IndexTimesMaxMessageSize
                        )
                }
                $SB.
                    Append(
                        "<![LOG[[$(
                            $EntryLevel.
                                ToString(
                                ).
                                    ToUpper(
                                    )
                        )]:`t$EntryType->$SubEntry]LOG]!>"

                    ).
                    Append(
                        "<"
                    ).
                    Append(
                        "time=`"$(
                            $Now.
                                ToString(
                                    'hh:mm:ss.ffzz'
                                )
                        )`" "

                    ).
                    Append(
                        "date=`"$(
                            $Now.
                                ToString(
                                    'MM-dd-yyyy'
                                )
                        )`" "

                    ).
                    Append(
                        "component=`"$Component`" "
                    ).
                    Append(
                        "context=`"`" "
                    ).
                    Append(
                        "type=`"$(
                            [int]$EntryLevel
                        )`" "

                    ).
                    Append(
                        "thread=`"0`" "
                    ).
                    Append(
                        "file=`"$(
                            $Caller.
                                FunctionName
                        ): ($(
                            $Caller.
                                Position.
                                    StartLineNumber
                        ),$(
                            $Caller.
                                Position.
                                    StartColumnNumber
                        ))`""

                    ).
                    Append(
                        ">"
                    )
            if (
                $Entry.
                    Length - 
                        (
                            $Index * 
                                $this.
                                    MaxMessageSize
                        ) -ge 
                            $this.
                                MaxMessageSize
            ) { 
                $SB.
                    AppendLine(                        
                    ) 
            }
        }
        return $SB.
            ToString(                
            )
    }
        #endregion
}
    #endregion
    #region Logger Class
Class Logger {
        #region Static Public Properties
    static [string] $Path
    static [bool] $Append
    static [int] $RollOverSize
    static [int] $MaxMessageSize
        #endregion

        #region Static Hidden Properties
    hidden static [System.Management.Automation.CallStackFrame[]] $_CallStack
    hidden static [System.Collections.Generic.Dictionary[String,Log]] $_Logger
        #endregion

        #region Static Constructor(s)
    static Logger(        
    ) { 
        [Logger]::ClearCalls(            
        )
        [Logger]::_Logger = 
            [System.Collections.Generic.Dictionary[string,Log]]::new(                
            ) 
        [Logger]::Setup(            
        )
    }
        #endregion

        #region Static Public Methods
    static Setup(        
    ) { 
        [Logger]::Setup(
            "$env:TEMP\Logger"
        ) 
    }

    static Setup(
        [string] $Path
    ) { 
        [Logger]::Setup(
            $Path,
            $true
        ) 
    }

    static Setup(
        [string] $Path, 
        [bool] $Append
    ) { 
        [Logger]::Setup(
            $Path, 
            $Append, 
            2621476
        ) 
    }

    static Setup(
        [string] $Path, 
        [bool] $Append, 
        [int] $RollOverSize
    ) { 
        [Logger]::Setup(
            $Path, 
            $Append,
            $RollOverSize,
            5000
        ) 
    }

    static Setup(
        [string] $Path, 
        [bool] $Append, 
        [int] $RollOverSize, 
        [int] $MaxMessageSize
    ) {
        [Logger]::CreatePath(
            $Path
        )
        [Logger]::Path = 
            $Path
        [Logger]::Append = 
            $Append
        [Logger]::RollOverSize = 
            $RollOverSize
        [Logger]::MaxMessageSize = 
            $MaxMessageSize
    }

            #region Simple Writers
    static [string] Information(
        [string] $Message
    ) { 
        return [Logger]::Information(
            $Message,
            $null
        ) 
    }

    static [string] Warning(
        [string] $Message
    ) { 
        return [Logger]::Warning(
            $Message,
            $null
        ) 
    }
            
    static [string] Error(
        [string] $Message
    ) { 
        return [Logger]::Error(
            $Message,
            $null
        ) 
    }
            
    static [string] Verbose(
        [string] $Message
    ) { 
        return [Logger]::Verbose(
            $Message,
            $null
        ) 
    }
            #endregion
            
            #region Data Writers
    static [string] Information(
        [string] $Message, 
        [object] $Data
    ) { 
        return [Logger]::Get(            
        ).
            Write(
                $Message,
                $Data,
                [LogEntryLevel]::Information
            ) 
    }
            
    static [string] Warning(
        [string] $Message, 
        [object] $Data
    ) { 
        return [Logger]::Get(            
        ).
            Write(
                $Message,
                $Data,
                [LogEntryLevel]::Warning
            ) 
    }
            
    static [string] Error(
        [string] $Message, 
        [object] $Data
    ) { 
        return [Logger]::Get(            
        ).
            Write(
                $Message,
                $Data,
                [LogEntryLevel]::Error
            ) 
    }
            
    static [string] Verbose(
        [string] $Message, 
        [object] $Data
    ) { 
        return [Logger]::Get(            
        ).
            Write(
                $Message,
                $Data,
                [LogEntryLevel]::Verbose
            ) 
    }
            #endregion
            
        #endregion

        #region Static Hidden Methods
    hidden static [Log] Get(        
    ) { 
        return [Logger]::Get(
            $false
        ) 
    }

    hidden static [Log] Get(
        [bool] $Force
    ) { 
        return [Logger]::GetLog(
            [Logger]::GetCallName(                
            ), 
            [Logger]::Path, 
            [Logger]::Append, 
            $Force, 
            $false
        ) 
    }
    
    hidden static [void] ClearCalls(        
    ) { 
        [Logger]::_CallStack = 
            $null
    }

    hidden static [object[]] GetCalls(        
    ) { 
        if (
            ![Logger]::_CallStack
        ) { 
            $S =
                Get-PSCallStack
            $TopOfTheStackAKAThisFile =
                $S.
                    Where(
                        $null,
                        [Where]::First
                    ).
                        ScriptName
            [Logger]::_CallStack = 
                $S.
                    Where{ 
                            $_.
                                ScriptName -ne 
                                    $TopOfTheStackAKAThisFile
                        } 
        }
        return [Logger]::_CallStack
    }    

    hidden static [object] GetCall(        
    ) { 
        return [Logger]::GetCalls(            
        )[0] 
    }

    hidden static [string] GetCallName(        
    ) { 
        return (
            [Logger]::GetCall(                
            ).
                Location -split 
                    "\.ps(m|d)?1"
        )[0] 
    }

    hidden static [LogFamilyAttribute[]] GetFamilies(        
    ) {
        $Families = 
            [LogFamilyAttribute[]](
                [System.Collections.Stack]::new(
                    [Logger]::GetCalls(                        
                    ).
                        InvocationInfo.
                            MyCommand.
                                ScriptBlock.
                                    Attributes.
                                        Where{
                                            $_ -is 
                                                [LogFamilyAttribute]
                                        }
                )
            )
        $SelectObject =
            @{
                InputObject =
                    $Families.
                        Family
                Unique =
                    $true
            }
        return [LogFamilyAttribute[]](
            $(
                foreach (
                    $Family in 
                        (
                            Select-Object @SelectObject
                        )
                ) { 
                    $Families.
                        Where(
                            {
                                $_.
                                    Family -eq 
                                        $Family
                            },
                            [Where]::Last
                        ) 
                }
            )
        )
    }

    hidden static CreatePath(
        [string] $Path
    ) { 
        $NewItem =
            @{ 
                Path =
                    $Path 
                ItemType =
                    'Directory'
                Force =
                $true
            }
        New-Item @NewItem
    }
    
    hidden static [Log] GetLog(
        [string] $Name, 
        [string] $Path, 
        [bool] $Append, 
        [bool] $Force, 
        [bool] $IsFamily 
    ) {
        if (
            ![Logger]::_Logger[$Name] -or 
                $Force
        ) { 
            [Logger]::_Logger[$Name] = 
                [Log]::new(
                    $Name, 
                    $Path, 
                    $IsFamily,
                    [Logger]::MaxMessageSize, 
                    $Append
                ) 
        }
        [Logger]::CreatePath(
            $Path
        )
        return [Logger]::_Logger[$Name]
    }
    
    hidden static [string] GetFamilyPath(
        [string] $FamilyPath
    ) { 
        if (
            ![System.IO.Path]::IsPathRooted(
                $FamilyPath
            )
        ) { 
            $JoinPath =
                @{ 
                    Path =
                        [Logger]::Path
                    ChildPath =
                        $FamilyPath 
                }
            $FamilyPath =
                Join-Path @JoinPath
        } 
        return $FamilyPath
    }

    hidden static [Log[]] Family(        
    ) {
        $LogFamilies = 
            ,[LogFamilyAttribute]::new(                
            ) + 
                [Logger]::GetFamilies(                    
                ).
                    Where{
                        $null -ne 
                            $_
                    }
        $DefinedFamilyLogs =
            foreach (
                $_ in 
                    $LogFamilies
            ) { 
                [Logger]::GetLog(
                    $_.
                        Family, 
                    [Logger]::GetFamilyPath(
                        $_.
                            Path
                    ), 
                    $true, 
                    $false, 
                    $true 
                ) 
            }
        return $DefinedFamilyLogs.
            Where{
                $_.
                    IsFamily
            }
    }    
        #endregion
}
    #endregion
#endregion