MeasureTrace.Database.psm1

Set-StrictMode -Version Latest

<#
.Synopsis
   Adds a given Measured Trace to a database
.DESCRIPTION
   Typically used indirectly via call to Measure-MtDbNewTracesFromFolder
.EXAMPLE
   dir c:\MyTracesFolder | Measure-Trace | Add-MtdbTrace -ConnectionString 'server=localhost;database=MyTraces;integrated security=SSPI'
#>

function Add-MtDbTrace {
    param(
        [Parameter(Mandatory=$true)]
        [string] $ConnectionString
        ,
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [MeasureTrace.TraceModel.Trace[]]$MeasuredTrace
    )
    begin{
        $repository = Connect-MtDbRepository -ConnectionString $ConnectionString
        $repository.Database.EnsureCreated()
    }
    process{
        foreach($trace in $MeasuredTrace){
            $repository.SaveTraceAndMeasurements($trace)
        }
    }
    end{
        $repository.Dispose()
    }
}

<#
.Synopsis
   Creates a connection to the MeasureTrace database, attempting to provision as needed
.DESCRIPTION
   Typically used indirectly via call to Measure-MtDbNewTracesFromFolder
.EXAMPLE
   Connect-MtDbRepository -ConnectionString 'server=localhost;database=MyTraces;integrated security=SSPI'
#>

function Connect-MtDbRepository {
    param(
        [Parameter(Mandatory=$true)]
        [string]$ConnectionString
    )
    $r = New-Object MeasureTrace.Database.Repository -ArgumentList $ConnectionString
    try{
        $r.TryToProvisionDatabase()
    }
    catch{}
    $r

}

<#
.Synopsis
   Determine whether a specific trace file is already represented in a DB
.DESCRIPTION
   Typically used indirectly via call to Measure-MtDbNewTracesFromFolder
.EXAMPLE
   Compare-MtDbTraceFileStatus -Path d:\MyEtlTraces -ConnectionString 'server=localhost;database=MyTraces;integrated security=SSPI'
#>

function Compare-MtDbTraceFileStatus {
    param(
        ## Path to a trace file
        [Parameter(ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
        [Alias('Path')]
        [string[]]$FullName,
        ## For connection to database. See Example section
        [string]$ConnectionString
    )
    begin{
        $repo = Connect-MtDbRepository -ConnectionString $ConnectionString
    }
    process{
        foreach($pathX in $FullName){
            $packageFileName = Split-Path -Path $pathX -Leaf
            [pscustomobject]@{
                PackageFileNameFull = $pathX
                PackageFIleName = $packageFileName
                IsInDatabase = $repo.IsTraceAlreadyInDb( $packageFileName )
            }
        }
    }
}

<#
.Synopsis
   Determine which traces in a folder are already represented in a DB
.DESCRIPTION
   Compares a given folder of traces against a database of parsed traces.
   Returns an object per trace which describes the status. Typically used
   indirectly via call to Measure-MtDbNewTracesFromFolder
.EXAMPLE
   Compare-MtDbFolderVsRepository -Path d:\MyEtlTraces -ConnectionString 'server=localhost;database=MyTraces;integrated security=SSPI'
#>

function Compare-MtDbFolderVsRepository{
    param(
        ## Path to a folder containing ETL traces
        [Parameter(ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
        [Alias('Path')]
        [string[]]$FullName,
        ## For connection to database. See Example section
        [string]$ConnectionString,
        [string[]]$Filter = @("*.etl","*.zip")
    )
    
    foreach($folder in $FullName){
        foreach($filterX in $Filter){
            Get-ChildItem -Path $folder -Filter $filterX -Recurse | Compare-MtDbTraceFileStatus -ConnectionString $ConnectionString
        }
    }
}

<#
.Synopsis
   Process traces from a folder into MeasuredTrace representation in a database
.DESCRIPTION
   Compares a given folder of traces against a database of parsed traces. For traces which are not yet
   reflected in the db it attempts to Measure them and export Measurements to the database
.EXAMPLE
   Measure-MtDbNewTracesFromFolder -Path d:\MyEtlTraces -ConnectionString 'server=localhost;database=MyTraces;integrated security=SSPI'
.EXAMPLE
   Measure-MtDbNewTracesFromFolder -Path d:\MyEtlTraces -ConnectionString 'server=localhost;database=MyTraces;integrated security=SSPI' -MaxCount 100 -Parallel
.EXAMPLE
    ## Continuous ingestion as traces arrive
    while( $true ){
        Measure-MtDbNewTracesFromFolder -Path d:\MyEtlTraces -ConnectionString 'server=localhost;database=MyTraces;integrated security=SSPI' -MaxCount 100 -Parallel
        Start-Sleep -Seconds 60
    }
#>

function Measure-MtDbNewTracesFromFolder{
    param(
        ## Path to a folder containing ETL traces
        [Parameter(ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
        [Alias('Path')]
        [string[]]$FullName,
        ## For connection to database. See Example section
        [string]$ConnectionString = 'server=(localdb)\MsSqlLocalDb;database=MeasureTrace;integrated security=true',
        ## Max number of new traces to process on this pass
        [int]$MaxCount = 50,
        ## Enables parallel processing via RSJobs
        [switch]$Parallel
    )
    begin{
        $batchLabel = [Guid]::NewGuid().ToString()
    }
    process{
        Compare-MtDbFolderVsRepository -Path $FullName -ConnectionString $ConnectionString |
          Where-Object {-not $_.IsInDatabase} | Select-Object -First $MaxCount |
          Foreach-Object{
            if(-not $Parallel){
                Measure-Trace -Path $_.PackageFileNameFull | Add-MtDbTrace -ConnectionString $ConnectionString
            }
            else{
                Start-RSJob -ModulesToImport $psscriptroot -Throttle $ParallelThrottle `
                -Batch $batchLabel -InputObject $_ -ScriptBlock {
                    Measure-Trace -Path $_.PackageFileNameFull | Add-MtDbTrace -ConnectionString $Using:ConnectionString
                }
            }
          }
    }
    end{
        Get-RSJob -Batch $batchLabel | Wait-RSJob -ShowProgress
        Get-RSJob -Batch $batchLabel | Receive-RSJob
    }
}

<#
.Synopsis
   Create task to process traces from a folder into MeasuredTrace representation in a database
.DESCRIPTION
   Via scheduled task-- compares a given folder of traces against a database of parsed traces. For traces which are not yet
   reflected in the db it attempts to Measure them and export Measurements to the database
.EXAMPLE
   Register-MtDbMeasurementTask -Path d:\MyEtlTraces -ConnectionString 'server=localhost;database=MyTraces;integrated security=SSPI'
.EXAMPLE
   Register-MtDbMeasurementTask -Path d:\MyEtlTraces -ConnectionString 'server=localhost;database=MyTraces;integrated security=SSPI' -MaxCount 100 -Parallel
.EXAMPLE
    ## Continuous ingestion as traces arrive
    while( $true ){
        Measure-MtDbNewTracesFromFolder -Path d:\MyEtlTraces -ConnectionString 'server=localhost;database=MyTraces;integrated security=SSPI' -MaxCount 100 -Parallel
        Start-Sleep -Seconds 60
    }
#>

function Register-MtDbMeasurementTask{
    param(
        ## Path to a folder containing ETL traces
        [Parameter(ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
        [Alias('Path')]
        [string[]]$FullName,
        ## For connection to database. See Example section
        [Parameter(Mandatory=$true)]
        [string]$ConnectionString = 'server=(localdb)\MsSqlLocalDb;database=MeasureTrace;integrated security=true',
        ## Max number of new traces to process on each pass
        [int]$MaxCount = 50,
        ## Enables parallel processing via RSJobs
        [switch]$Parallel
    )
    $taskActionArgs = @(
        '-ExecutionPolicy'
        'Bypass'
        '-Command'
        '"&{Measure-MtDbNewTracesFromFolder -Path PATHTOKEN -ConnectionString CONNECTIONSTRINGTOKEN }"'
        )
    $taskPath = '\MeasureTrace.Database\'
    $taskName = 'Measure-MtDbNewTracesFromFolder'
    Get-ScheduledTask -TaskPath $taskPath -TaskName $taskName -ErrorAction SilentlyContinue | Unregister-ScheduledTask
    $pathsToWatchMergedToken = $FullName -replace '^',"'" -replace '$',"'" -join ","
    $taskActionArgs[3] = $taskActionArgs[3] -replace 'PATHTOKEN',$pathsToWatchMergedToken -replace 'CONNECTIONSTRINGTOKEN',("'" + $ConnectionString + "'")
    $ta = New-ScheduledTaskAction -Execute '%windir%\system32\WindowsPowerShell\v1.0\powershell.exe' -Argument ($taskActionArgs -join " ")
    $tt = New-ScheduledTaskTrigger -Daily -At (Get-Date -Hour 0 -Minute 1) -DaysInterval 1
    $ts = New-ScheduledTaskSettingsSet -Compatibility Win8 -MultipleInstances IgnoreNew
    $st = Register-ScheduledTask -TaskName $taskName -TaskPath $taskPath -User 'NT AUTHORITY\NetworkService' -Action $ta -Trigger $tt -Settings $ts -RunLevel Highest
    $st.Triggers[0].Repetition.StopAtDurationEnd = $false
    $st.Triggers[0].Repetition.Interval = 'PT2M'
    $st.Triggers[0].Repetition.Duration = 'P1D'
    Set-ScheduledTask -InputObject $st
}