internal/loggingProviders/sql.provider.ps1

$FunctionDefinitions = {
    function Get-DatabaseConnection {
        [CmdletBinding()]
        param ()

        if (-not $script:connections) {
            $script:connections = @{ }
        }

        # Connection already established
        if ($script:cfgServer.ConnectionContext) {
            $server = $script:cfgServer
        }
        elseif ($script:connections[$script:cfgServer]) {
            $server = $script:connections[$script:cfgServer]
        }
        else {
            $param = @{
                SqlInstance = $script:cfgServer
            }
            $credential = Get-ConfigValue -Name 'Credential'
            if ($credential) { $param.SqlCredential = $Credential }
            try { $server = Connect-DbaInstance @param -ErrorAction Stop }
            catch { throw }
            $script:connections[$script:cfgServer] = $server
        }
        if (-not $server.ConnectionContext.IsOpen) {
            try { $server.ConnectionContext.Connect() }
            catch { throw }
        }
        $server
    }

    function Export-DataToSql {
        <#
        .SYNOPSIS
            Function to send logging data to a Sql database
 
        .DESCRIPTION
            This function is the main function that takes a PSFMessage object to log in a Sql database.
 
        .PARAMETER ObjectToProcess
            This is a PSFMessage object that will be converted and serialized then injected to a Sql database.
 
        .EXAMPLE
            Export-DataToAzure $objectToProcess
 
        .NOTES
            How to register this provider
            -----------------------------
            Set-PSFLoggingProvider -Name sql -InstanceName sqlloginstance -Enabled $true
        #>

        
        [cmdletbinding()]
        param (
            [parameter(Mandatory = $True)]
            $ObjectToProcess
        )
        
        process {
            $queryParameters = @($script:converter.Process($ObjectToProcess))[0]
            $insertQuery = Get-Query -Parameters $queryParameters
            
            try {
                $sqlInstance = Get-DatabaseConnection
                
                Invoke-DbaQuery -SqlInstance $sqlInstance -Database $script:cfgDatabase -Query $insertQuery -SqlParameters $queryParameters -EnableException
            }
            catch { throw }
        }
    }
    
    function Get-Query {
        [CmdletBinding()]
        param (
            [hashtable]
            $Parameters
        )
        
        if ($script:insertQuery) { return $script:insertQuery }
        
        $properties = $Parameters.Keys
        $propSquared = foreach ($property in $properties) {
            "[$property]"
        }
        $propAdd = foreach ($property in $properties) {
            "@$property"
        }

        if ($script:cfgTable -match '\[|\]|\(|\)| ') {
            throw "Invalid Table name: $script:cfgTable"
        }
        if ($script:cfgSchema -match '\[|\]|\(|\)| ') {
            throw "Invalid Schema name: $script:cfgSchema"
        }
        
        $script:insertQuery = @"
INSERT INTO [$script:cfgDatabase].[$script:cfgSchema].[$script:cfgTable]($($propSquared -join ','))
VALUES ($($propAdd -join ','))
"@

        $script:insertQuery
    }
    
    function New-DefaultSqlDatabaseAndTable {
        <#
        .SYNOPSIS
                This function will create a default sql database object
 
        .DESCRIPTION
                This function will create the default sql default logging database
 
        .EXAMPLE
            None
        #>

        
        [cmdletbinding()]
        param (
        )
        
        # need to use dba tools to create the database and credentials for connecting.
        
        
        begin {
            # set instance and database name variables
            $SqlTable = Get-ConfigValue -Name 'Table'
            $SqlDatabaseName = Get-ConfigValue -Name 'Database'
            $SqlSchema = Get-ConfigValue -Name 'Schema'
            if (-not $SqlSchema) { $SqlSchema = 'dbo' }
            
            if ($SqlTable -match '\[|\]|\(|\)| ') {
                throw "Invalid Table name: $SqlTable"
            }
            if ($SqlSchema -match '\[|\]|\(|\)| ') {
                throw "Invalid Schema name: $SqlSchema"
            }
        }
        process {
            try {
                $dbaconnection = Get-DatabaseConnection
                $database = Get-DbaDatabase -SqlInstance $dbaconnection | Where-Object Name -eq $SqlDatabaseName
                if (-NOT $database) {
                    $database = New-DbaDatabase -SqlInstance $dbaconnection -Name $SqlDatabaseName
                }
                if (-NOT ($database.Tables | Where-Object Name -eq $SqlTable)) {
                    $createtable = "CREATE TABLE $SqlSchema.$SqlTable (Message VARCHAR(max), Level VARCHAR(max), TimeStamp [DATETIME], FunctionName VARCHAR(max), ModuleName VARCHAR(max), Tags VARCHAR(max), Runspace VARCHAR(36), ComputerName VARCHAR(max), Username VARCHAR(max), TargetObject VARCHAR(max), [File] VARCHAR(max), Line BIGINT, ErrorRecord VARCHAR(max), CallStack VARCHAR(max), [Data] VARCHAR(max))"
                    Invoke-dbaquery -SQLInstance $dbaconnection -Database $SqlDatabaseName -Query $createtable
                }
            }
            catch {
                throw
            }
        }
    }
}

#region Installation
$installationParameters = {
    $results = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
    $attributesCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
    $parameterAttribute = New-Object System.Management.Automation.ParameterAttribute
    $parameterAttribute.ParameterSetName = '__AllParameterSets'
    $attributesCollection.Add($parameterAttribute)
    
    $validateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute('CurrentUser', 'AllUsers')
    $attributesCollection.Add($validateSetAttribute)
    
    $runtimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter("Scope", [string], $attributesCollection)
    $results.Add("Scope", $runtimeParam)

    $attributesCollection2 = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
    $runtimeParam2 = New-Object System.Management.Automation.RuntimeDefinedParameter("Repository", [string], $attributesCollection2)
    $results.Add("Repository", $runtimeParam2)
    $results
}

$installation_script = {
    param (
        $BoundParameters
    )
    
    $paramInstallModule = @{
        Name = 'dbatools'
    }
    if ($BoundParameters.Scope) { $paramInstallModule['Scope'] = $BoundParameters.Scope }
    elseif (-not (Test-PSFPowerShell -Elevated)) { $paramInstallModule['Scope'] = 'CurrentUser' }
    if ($BoundParameters.Repository) { $paramInstallModule['Repository'] = $BoundParameters.Repository }
    else { $paramInstallModule['Repository'] = Get-PSFConfigValue -FullName 'PSFramework.System.DefaultRepository' -Fallback 'PSGallery' }
    
    Install-Module @paramInstallModule
}

$isInstalled_script = {
    (Get-Module dbatools -ListAvailable) -as [bool]
}
#endregion Installation
#region Events
$begin_event = {
    $script:cfgServer = Get-ConfigValue -Name 'SqlServer'

    New-DefaultSqlDatabaseAndTable
}
$start_event = {
    $changePending = $false
    if ($script:cfgHeaders -ne (Get-ConfigValue -Name 'Headers')) {
        $script:cfgHeaders = Get-ConfigValue -Name 'Headers'
        $changePending = $true
    }
    if ($script:cfgServer -ne (Get-ConfigValue -Name 'SqlServer')) {
        $script:cfgServer = Get-ConfigValue -Name 'SqlServer'
        $changePending = $true
    }
    if ($script:cfgDatabase -ne (Get-ConfigValue -Name 'Database')) {
        $script:cfgDatabase = Get-ConfigValue -Name 'Database'
        $changePending = $true
    }
    if ($script:cfgSchema -ne (Get-ConfigValue -Name 'Schema')) {
        $script:cfgSchema = Get-ConfigValue -Name 'Schema'
        if (-not $script:cfgSchema) { $script:cfgSchema = 'dbo' }
        $changePending = $true
    }
    if ($script:cfgTable -ne (Get-ConfigValue -Name 'Table')) {
        $script:cfgTable = Get-ConfigValue -Name 'Table'
        $changePending = $true
    }
    if (-not $changePending) { return }
    
    $script:sql_headers = switch ($script:cfgHeaders) {
        'Tags'
        {
            @{
                Name       = 'Tags'
                Expression = { ($_.Tags -join ",") -as [string] }
            }
        }
        'Message' { @{ Name = 'Message'; Expression = { $_.LogMessage } } }
        'Level' { @{ Name = 'Level'; Expression = { $_.Level -as [string] } } }
        'Runspace' { @{ Name = 'Runspace'; Expression = { $_.Runspace -as [string] } } }
        'TargetObject' { @{ Name = 'TargetObject'; Expression = { $_.TargetObject -as [string] } } }
        'ErrorRecord' { @{ Name = 'ErrorRecord'; Expression = { $_.ErrorRecord -as [string] } } }
        'CallStack' { @{ Name = 'CallStack'; Expression = { $_.CallStack -as [string] } } }
        'Timestamp'
        {
            @{
                Name                           = 'Timestamp'
                Expression                       = {
                    $_.Timestamp.ToUniversalTime()
                }
            }
        }
        'Data'
        {
            @{
                Name = 'Data'
                Expression = {
                    if (-not $_.Data) { return 'null' }
                    $_.Data | ConvertTo-Json -Compress
                }
            }
        }
        default { $_ }
    }
    
    if ($script:converter) {
        $null = $script:converter.End()
        $script:converter = $null
    }
    # Cache the conversion logic once as a steppable pipeline to avoid having to do it
    $script:converter = { Microsoft.PowerShell.Utility\Select-Object $script:sql_headers | PSFramework\ConvertTo-PSFHashtable }.GetSteppablePipeline()
    $script:converter.Begin($true)
    
    $script:insertQuery = ''
}

$message_event = {
    param (
        $Message
    )
    
    Export-DataToSql -ObjectToProcess $Message
}

$end_event = {
    if ($script:converter) {
        $null = $script:converter.End()
        $script:converter = $null
    }
}

# Action that is performed when stopping the logging script.
$final_event = {
    
}
#endregion Events

# Configuration values for the logging provider
$configuration_Settings = {
    Set-PSFConfig -Module PSFramework -Name 'Logging.Sql.Credential' -Initialize -Validation 'credential' -Description "Credentials used for connecting to the SQL server."
    Set-PSFConfig -Module PSFramework -Name 'Logging.Sql.Database' -Value "LoggingDatabase" -Initialize -Validation 'string' -Description "SQL server database."
    Set-PSFConfig -Module PSFramework -Name 'Logging.Sql.Table' -Value "LoggingTable" -Initialize -Validation 'string' -Description "SQL server database table."
    Set-PSFConfig -Module PSFramework -Name 'Logging.Sql.SqlServer' -Value "" -Initialize -Description "SQL server hosting logs."
    Set-PSFConfig -Module PSFramework -Name 'Logging.Sql.Schema' -Value "dbo" -Initialize -Description "SQL server schema."
}

# Registered parameters for the logging provider.
# ConfigurationDefaultValues are used for all instances of the sql log provider
$paramRegisterPSFSqlProvider = @{
    Name               = "Sql"
    Version2           = $true
    ConfigurationRoot  = 'PSFramework.Logging.Sql'
    InstanceProperties = 'Database', 'Schema', 'Table', 'SqlServer', 'Credential', 'Headers'
    MessageEvent       = $message_Event
    BeginEvent           = $begin_event
    StartEvent           = $start_event
    EndEvent           = $end_event
    FinalEvent           = $final_event
    IsInstalledScript  = $isInstalled_script
    InstallationScript = $installation_script
    ConfigurationSettings = $configuration_Settings
    InstallationParameters = $installationParameters
    FunctionDefinitions = $functionDefinitions
    ConfigurationDefaultValues = @{
        'Database' = "LoggingDatabase"
        'Table'    = "LoggingTable"
        'Schema'   = 'dbo'
        Headers    = 'Message', 'Timestamp', 'Level', 'Tags', 'Data', 'ComputerName', 'Runspace', 'UserName', 'ModuleName', 'FunctionName', 'File', 'Line', 'CallStack', 'TargetObject', 'ErrorRecord'
    }
}

# Register the Azure logging provider
Register-PSFLoggingProvider @paramRegisterPSFSqlProvider