Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Invoking "Use-PodeRoutes" as schedule #964

Closed
RobinBeismann opened this issue Apr 28, 2022 · 12 comments · Fixed by #1042
Closed

Invoking "Use-PodeRoutes" as schedule #964

RobinBeismann opened this issue Apr 28, 2022 · 12 comments · Fixed by #1042
Assignees
Milestone

Comments

@RobinBeismann
Copy link
Sponsor Contributor

Hey @Badgerati,

we're using Pode on many servers as agent to publish metrics and monitoring data for PRTG (a monitoring solution) so we don't have to put highly privileged credentials into our monitoring system, this works out great for us.

With every Pode Agent we're deploying, we publish all routes with it and have some logic at the start of the route that checks if it shall actually publish this route or not (e.g. check if node is part of a cluster, if so, publish cluster related checks). We then have another script that loops through all hosts, checks if Pode is running and creates the checks in PRTG based on the OpenAPI Definition.

We sometimes face the issue that the Pode Agent starts up too early while other services are not yet initialized (like on Windows the Failover Cluster Manager) and therefor the detection methods fails, resulting in the Route not being published. I tried setting the Pode Agent (ran by NSSM) to delayed start, however that doesn't really solve it in all cases..

So my idea was to just run Use-PodeRoutes as Pode schedule, but it doesn't seem to be working during the runtime of Pode.
Is it possible to publish routes after the actual startup?

For reference, here is the code:

try {
    #Try to load the local module
    Import-Module -Name "$PSScriptRoot\ps_modules\pode\Pode.psm1" -ErrorAction Stop
    Write-Host("Loaded local Pode module.")
}
catch {
    #Fail back to the system module
    Write-Host("Could not load local Pode module: $_")
    if (!(Get-Module -Name 'Pode' -ListAvailable)) {
        Install-Module -Scope CurrentUser -Name 'Pode' -Confirm:$false -Force
    }else{
        Get-Module -Name 'Pode' | Remove-Module
    }
    Import-Module -Name "Pode"
    Write-Host("Loaded system Pode module.")
}

Start-PodeServer -Threads 2 -ScriptBlock {
    $podePort = 15123
    Add-PodeEndpoint -Address * -Protocol Http -Port $podePort
    
    #Logging
    
    New-PodeLoggingMethod -File -Path ./logs -Name "$($env:COMPUTERNAME)_errors" -MaxDays 30 | Enable-PodeErrorLogging
    New-PodeLoggingMethod -File -Path ./logs -Name "$($env:COMPUTERNAME)_request" -MaxDays 30 | Enable-PodeRequestLogging
    New-PodeLoggingMethod -File -Path ./logs -Name "$($env:COMPUTERNAME)_feed" -MaxDays 30 | Add-PodeLogger -Name 'Feed' -ScriptBlock {
        param($arg)        
        $string = ($arg.GetEnumerator() | ForEach-Object { $_.Name + ": " + $_.Value }) -join "; "
        return $string
    } -ArgumentList $arg
    

    # Save current directory as Pode State
    Set-PodeState -Name "PSScriptRoot" -Value $PSScriptRoot
    Set-PodeState -Name "siteTitle" -Value "PodeMon - Monitoring Agent"
    Set-PodeState -Name "sitePort" -Value $podePort

    # Import Modules
    Import-PodeModule -Path './ps_modules/PodeMon/PodeMon.psm1'
    Import-PodeModule -Path './ps_modules/Invoke-SqlCmd2/Invoke-SqlCmd2.psm1'

    # Load all routes from /routes once a minute
    Add-PodeSchedule -Name 'loadPodeMonRoutes' -Cron '@minutely' -OnStart -ScriptBlock { 
        #Update the role with the group set under general settings
        Use-PodeRoutes
    }

    # Enable OpenAPI
    Enable-PodeOpenApi -Path '/docs/openapi' -Title 'PodeMon Monitoring Endpoints' -Version 1.0.0.0 -RouteFilter "/api/*"

    # Enable Swagger
    Enable-PodeOpenApiViewer -Type 'Swagger' -Path '/' -DarkMode -OpenApiUrl "/docs/openapi"
}

Example Route under /routes/:

#-------------------------------------------------------------------------------------------------------------------------------------------------------------
#                                    PodeMon Monitoring Route
#
#                   Please don't modify any areas except the following ones:
#                    - Route Meta: 
#                                 - RoutePath: Please assign a unique URL to this route, you can check the used ones under
#                                              http://localhost:7799/docs/swagger
#                                 - Route Description: The description shown in Swagger and the OpenAPI Definition
#                                 - Route Tags: A comma seperated array of tags shown in Swagger and the OpenAPI Definition
#                                 - Route Prereq: A Scriptblock which needs to be $true for the route to be added.
#                                                 You can use this to check if a prerequisites for a check is given or not.
#                                                 For example to check if Hyper-V is installed before advertising a Hyper-V based Check
#
#                    - Route Logic:
#                                 This is the section where the actual route logic goes.
#                                 Please make sure to add proper error catching, if there is no error thrown, the result will show 0 (successful)
#                                 If you want to add any messages to the output, add them to $res.Messages using $res.Messages.Add($message)
#
#-------------------------------------------------------------------------------------------------------------------------------------------------------------

#region Define Route Meta
$RoutePath = 'api/v1/filesystem/perfcounter-diskqueue'
$RouteDescription = 'Reports Disk Performance Counters for Disk Queues'
$RouteTags = 'Filesystem'
$RoutePrereq = [scriptblock]{ [bool](Get-Service -ErrorAction SilentlyContinue | Where-Object { $_.Name.StartsWith("MSSQL") } ) }
#endregion

# Logic
#region Route Initialization
if(
    (   
        # Check if we have no route prereq
        !$RoutePrereq -or
        # Or if our route prereq equals an empty one
        ($RoutePrereq.ToString().Trim() -eq ([scriptblock]{}).ToString())
    ) -or
    (
        # Or if our route prereq is defined and actually fulfilled
        ( (. $RoutePrereq) -eq $true)
    )
){
    # Yes it is -> Initialize route
    Add-PodeRoute -Method Get -Path $RoutePath -ScriptBlock {       
        $global:baseJSON = $null
        # Try our actual route logic
    #endregion
    #region Route content
#---------------------------------------------------------------------------------------------------------------------------
#----------------------------------- Route Logic here ----------------------------------------------------------------------
#---------------------------------------------------------------------------------------------------------------------------
            
        $queues = @{
			"Disk Queue Write Length" = @{
				_Countername = "\PhysicalDisk(*)\Avg. Disk Write Queue Length"
				_ValueExtractionSB = [scriptblock]{
					# Round value
					[math]::Round($v,2)
				}
				Unit = "Count"
				LimitMaxWarning = 1
				LimitMaxError = 1.5
				LimitMode = $true	
			}
			"Disk Queue Read Length" = @{
				_Countername = "\PhysicalDisk(*)\Avg. Disk Read Queue Length"
				_ValueExtractionSB = [scriptblock]{
					# Round value
					[math]::Round($v,2)
				}
				Unit = "Count"
				LimitMaxWarning = 1
				LimitMaxError = 1.5
				LimitMode = $true
			}

		}

		$regex = "\\\\(?'hostname'.+)\\.+\(\d (?'diskletter'\w):\)\\(?'checktype'.*)$"

		$queues.GetEnumerator() | ForEach-Object {
			$checkName = $_.Name
			$checkMeta = $_.Value
			$counterName = $_.Value._Countername

			# Get Counters
			Get-Counter -Counter $counterName | Select-Object -ExpandProperty 'CounterSamples' | ForEach-Object {
				if(
					# Extract their data
					($match = [regex]::Match($_.path,$regex)) -and
					($match.Success)
				){
					# Format them to a usable format
					$val = $_.CookedValue
					
					if($checkMeta._ValueExtractionSB){
						# Cast "v" as var for the value to be used in the scriptblock
						$v = $val
						
						# Invoke the scriptblock
						$val = ($checkMeta._ValueExtractionSB.Invoke())[0]
					}
					
					[PSCustomObject]@{
						checkName = $checkName
						checkMeta = $checkMeta
						Diskletter = $match.Groups.Item('diskletter').Value
						Checktype = $match.Groups.Item('checktype').Value
						Value = $val
					}
				}    
			}
			# Convert them to the PRTG Format
		} | ForEach-Object {
			$channelName = "$($_.checkName) ($($_.Diskletter.ToUpper()):)"
			$res = $_

			$splat = @{
				Channel = $channelName
				Value = $res.Value
			}
			$res.checkMeta.Keys | Where-Object { !$_.StartsWith("_") } | ForEach-Object {
				$splat.$_ = $res.checkMeta.$_
			}    

			Write-MonitoringResult @splat
		}

#---------------------------------------------------------------------------------------------------------------------------
#----------------------------------- Route Logic end -----------------------------------------------------------------------
#---------------------------------------------------------------------------------------------------------------------------

        Write-PodeJsonResponse -Value $global:baseJSON
        #endregion
    #endregion
    } -PassThru | Set-PodeOARouteInfo -Summary $RouteDescription -Description (Get-FileHash -Path $MyInvocation.MyCommand.Definition).Hash -Tags $RouteTags -Deprecated:$RouteDeprecated 
}

And the Write-MonitoringResult Function used above:

function Write-MonitoringResult(){
    [CmdletBinding()]
    [OutputType([psobject])]
    param (
        # Name
        [Parameter(Mandatory = $true,
                    ValueFromPipelineByPropertyName = $false,
                    Position = 0)]
        [string]
        $Channel,

        # Value
        [Parameter(Mandatory = $true,
                    ValueFromPipelineByPropertyName = $false,
                    Position = 1)]
        $Value,

        # Unit
        [Parameter(Mandatory = $false,
                    ValueFromPipelineByPropertyName = $false,
                    Position = 2)]
        [ValidateSet("BytesBandwidth","BytesDisk","Temperature","Percent","TimeResponse","TimeSeconds","Custom","Count","CPU","BytesFile","SpeedDisk","SpeedNet","TimeHours")]
        [string]
        $Unit,

        # CustomUnit
        [Parameter(Mandatory = $false,
                    ValueFromPipelineByPropertyName = $false,
                    Position = 3)]
        [string]
        $CustomUnit,

        # SpeedSize
        [Parameter(Mandatory = $false,
                    ValueFromPipelineByPropertyName = $false,
                    Position = 4)]
        [ValidateSet("One","Kilo","Mega","Giga","Tera","Byte","KiloByte","MegaByte","GigaByte","TeraByte","Bit","KiloBit","MegaBit","GigaBit","TeraBit")]
        [string]
        $SpeedSize,

        # VolumeSize
        [Parameter(Mandatory = $false,
                    ValueFromPipelineByPropertyName = $false,
                    Position = 5)]
        [ValidateSet("One","Kilo","Mega","Giga","Tera","Byte","KiloByte","MegaByte","GigaByte","TeraByte","Bit","KiloBit","MegaBit","GigaBit","TeraBit")]
        [string]
        $VolumeSize,

        # SpeedTime
        [Parameter(Mandatory = $false,
                    ValueFromPipelineByPropertyName = $false,
                    Position = 6)]
        [string]
        [ValidateSet("Second","Minute","Hour","Day")]
        $SpeedTime,

        # Mode
        [Parameter(Mandatory = $false,
                    ValueFromPipelineByPropertyName = $false,
                    Position = 7)]
        [ValidateSet("Absolute","Difference")]
        [string]
        $Mode,

        # DecimalMode
        [Parameter(Mandatory = $false,
                    ValueFromPipelineByPropertyName = $false,
                    Position = 9)]
        #[ValidateSet("Auto","All",)]
        [string]
        $DecimalMode = "Auto",

        # Warning
        [Parameter(Mandatory = $false,
                    ValueFromPipelineByPropertyName = $false,
                    Position = 10)]
        [bool]
        $Warning = $false,

        # ShowChart
        [Parameter(Mandatory = $false,
                    ValueFromPipelineByPropertyName = $false,
                    Position = 11)]
        [bool]
        $ShowChart = $true,

        # ShowTable
        [Parameter(Mandatory = $false,
                    ValueFromPipelineByPropertyName = $false,
                    Position = 12)]
        [bool]
        $ShowTable = $true,

        # LimitMaxError
        [Parameter(Mandatory = $false,
                    ValueFromPipelineByPropertyName = $false,
                    Position = 13)]
        [string]
        $LimitMaxError,

        # LimitMaxWarning
        [Parameter(Mandatory = $false,
                    ValueFromPipelineByPropertyName = $false,
                    Position = 14)]
        [string]
        $LimitMaxWarning,

        # LimitMinWarning
        [Parameter(Mandatory = $false,
                    ValueFromPipelineByPropertyName = $false,
                    Position = 15)]
        [string]
        $LimitMinWarning,

        # LimitMinError
        [Parameter(Mandatory = $false,
                    ValueFromPipelineByPropertyName = $false,
                    Position = 16)]
        [string]
        $LimitMinError,

        # LimitErrorMsg
        [Parameter(Mandatory = $false,
                    ValueFromPipelineByPropertyName = $false,
                    Position = 17)]
        [string]
        $LimitErrorMsg,

        # LimitWarningMsg
        [Parameter(Mandatory = $false,
                    ValueFromPipelineByPropertyName = $false,
                    Position = 18)]
        [string]
        $LimitWarningMsg,

        # LimitMode
        [Parameter(Mandatory = $false,
                    ValueFromPipelineByPropertyName = $false,
                    Position = 19)]
        [bool]
        $LimitMode = $false,

        # ValueLookup
        [Parameter(Mandatory = $false,
                    ValueFromPipelineByPropertyName = $false,
                    Position = 20)]
        [string]
        $ValueLookup,

        # NotifyChanged
        [Parameter(Mandatory = $false,
                    ValueFromPipelineByPropertyName = $false,
                    Position = 21)]
        [string]
        $NotifyChanged

    )
    process {
        # Set our result to the bound parameters
        $res = $PSBoundParameters

        # These vars have defaults, process them
        $defVars = "Warning", "Float", "ShowChart", "ShowTable", "LimitMode"
        $defVars | ForEach-Object {
            # Save our var name for the loop
            $varName = $_
            
            # Check if we got a value (we should!)
            if(
                ($lVar = Get-Variable -Name $varName -ErrorAction SilentlyContinue)
            ){
                # Translate bools into 1 or 0 for PRTG
                switch($lVar.Value){
                    $true {
                        $res.$varName = 1
                    }
                    $false {
                        $res.$varName = 0
                    }
                    default {                        
                        # Not a bool, return the actual value
                        $res.$varName = $lVar.Value
                    }
                }
            }
        }
		if(
			($Value -is [single]) -or
			($Value -is [double])
		){
			$res.Float = 1
			$res.DecimalMode = "All"
		}

        #Generate JSON
        if(!($global:baseJSON.prtg.Result)){
            $global:baseJSON = @{}
        }
        if(!($global:baseJSON.prtg)){
            $global:baseJSON.prtg = @{}
        }
        if(!($global:baseJSON.prtg.Result)){
            $global:baseJSON.prtg.Result = @()
        }

        # Add the result to the JSON File
        $global:baseJSON.prtg.Result += $res
    }

}     

Thanks already. :)

@Badgerati
Copy link
Owner

Hey @RobinBeismann,

I just tried a similar, albeit simpler 😂, setup with the following server.ps1:

Start-PodeServer -Threads 2 {
    Add-PodeEndpoint -Address localhost -Port 8090 -Protocol Http
    New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging

    Add-PodeSchedule -Name 'test' -Cron '@minutely' -OnStart -ScriptBlock {
        Use-PodeRoutes
    }

    Enable-PodeOpenApi -Path '/docs/openapi' -Title 'Test' -Version 1.0.0.0 -RouteFilter "/route-*"
    Enable-PodeOpenApiViewer -Type 'Swagger' -Path '/' -DarkMode -OpenApiUrl "/docs/openapi"
}

and /routes/ file as the following to just add a random route every minute:

$rand = Get-Random -Minimum 1 -Maximum 1000
"adding: /route-file$($rand)" | out-default

Add-PodeRoute -Method Get -Path "/route-file$($rand)" -ScriptBlock {
    Write-PodeJsonResponse -Value @{ Message = "I'm from a route file!" }
}

For me, Use-PodeRoutes added the routes; I could Invoke-RestMethod against them, and see them in Swagger as well 🤔:

image

For being "published", is it that the routes for you aren't being created at all? Or that they are but not appearing in Swagger? Everything in your above code does look like it's good.

@RobinBeismann
Copy link
Sponsor Contributor Author

Hey @Badgerati,

I just tried your example also (which in the end does the same as mine) and figured out that it works on PowerShell 7.x but not on PowerShell 5.
The OpenAPI Definition doesn't update on either versions (probably by design?), but on PowerShell 5 the Route is not even working after being added by the schedule.

@Badgerati
Copy link
Owner

Hey @RobinBeismann,

Hmm, that's weird. For me, on both 5 and 7, the Route is added and works, and OpenAPI is updated 🤔 I also tried via NSSM as well, and that worked.

Which version of Pode are you using? I've tested the version I'm working on right now, the latest (2.6.2), and 2.6.0. I'm guessing no errors are being thrown and written to the log file?

@RobinBeismann
Copy link
Sponsor Contributor Author

Hey @Badgerati,

sorry, didn't get around earlier. I just figured out that Pode actually errors out when re-importing a route it already has (which effectively happens for me):
Provider Health: Attempting to perform the GetChildItems operation on the 'FileSystem' provider failed for path '<path>\routes'. [Get] /api/v1/filesystem/perfcounter-diskavg: Already defined.
This doesn't pop up in your example as you're randomly generating the names.
As far as I can see, there is no switch at the moment to tell it to just import new routes, is it?

@Badgerati
Copy link
Owner

Hey @RobinBeismann,

No worries! :)

Aah, yes that would make sense! You're right that there isn't anything in place at the moment to "skip" or "only import new routes". I don't think there's anything we could put onto Use-PodeRoutes, as it simply dot sources the files.

What we could maybe do is add a -Force to Add-PodeRoute to overwrite existing ones, and/or a -SkipIfExists to only add if it doesn't exist. Or possibly even a Test-PodeRoute you could wrap Add-PodeRoute calls in 🤔

@RobinBeismann
Copy link
Sponsor Contributor Author

I actually worked around it with -ErrorAction SilentlyContinue for now, but I could raise a PR with a larger Parameter Set when I find time.

@RobinBeismann
Copy link
Sponsor Contributor Author

Hey @Badgerati,

I'll close it for now as I probably won't find time to inplement it any time soon. I'll reopen it when I get to it.

@Badgerati
Copy link
Owner

Hey @RobinBeismann,

I'm looking at what to put into v2.8.0, and I've had somebody ask about a similar feature on Pode.Web. If you're OK, I can implement the work for the new parameters and function? :)

@RobinBeismann
Copy link
Sponsor Contributor Author

Hey @RobinBeismann,

I'm looking at what to put into v2.8.0, and I've had somebody ask about a similar feature on Pode.Web. If you're OK, I can implement the work for the new parameters and function? :)

Hey @Badgerati,

sure, go ahead, I was planning on implementing it already but I'm too exhausted by work at the moment plus currently on a business trip right now.

@Badgerati Badgerati reopened this Nov 15, 2022
@Badgerati Badgerati added this to the 2.8.0 milestone Nov 15, 2022
@Badgerati Badgerati self-assigned this Dec 4, 2022
@Badgerati
Copy link
Owner

Linking with Badgerati/Pode.Web#367

@Badgerati
Copy link
Owner

I've added a new -IfExists parameter to Add-PodeRoute, Add-PodeStaticRoute, and Add-PodeSignalRoute. This parameter takes the values Default, Error, Overwrite, and Skip. (the default behaviour is Error, as it currently is)

To Skip over creating a Route that already exists:

Start-PodeServer -Thread 2 -Verbose {
    Add-PodeEndpoint -Address * -Port 8090 -Protocol Http

    Add-PodeRoute -Method Get -Path '/' -ScriptBlock {
        Write-PodeJsonResponse -Value @{ Result = 1 }
    }

    Add-PodeRoute -Method Get -Path '/' -IfExists Skip -ScriptBlock {
        Write-PodeJsonResponse -Value @{ Result = 2 }
    }
}

Running the above, and calling http://localhost:8090/ will return the result of 1 not 2 😄. If you swap Skip to Overwrite and re-run, you'll get the result of 2 instead!

The same parameter has also been added onto Use-PodeRoutes, meaning you can apply Skip to all Routes on mass:

Use-PodeRoutes -IfExists Skip

Furthermore, there's also a new Set-PodeRouteIfExistsPreference function which let's you overwrite the global default behaviour of Error:

Set-PodeRouteIfExistsPreference -Value Overwrite

In the case of the options:

  • Default: will use the IfExists value from higher up the hierarchy (see below) - if none defined, Error is the final default
  • Error: if the Route already exists, throw an error
  • Overwrite: if the Route already exists, delete the existing Route and recreate with the new definition
  • Skip: if the Route already exists, skip over the Route and do nothing

The default value that is used is defined by the following hierarchy:

  • IfExists value of the current Route (like Add-PodeRoute)
  • IfExists value from Use-PodeRoute
  • IfExists value from Set-PodeRouteIfExistsPreference
  • Error

@RobinBeismann
Copy link
Sponsor Contributor Author

I've added a new -IfExists parameter to Add-PodeRoute, Add-PodeStaticRoute, and Add-PodeSignalRoute. This parameter takes the values Default, Error, Overwrite, and Skip. (the default behaviour is Error, as it currently is)

To Skip over creating a Route that already exists:

Start-PodeServer -Thread 2 -Verbose {
    Add-PodeEndpoint -Address * -Port 8090 -Protocol Http

    Add-PodeRoute -Method Get -Path '/' -ScriptBlock {
        Write-PodeJsonResponse -Value @{ Result = 1 }
    }

    Add-PodeRoute -Method Get -Path '/' -IfExists Skip -ScriptBlock {
        Write-PodeJsonResponse -Value @{ Result = 2 }
    }
}

Running the above, and calling http://localhost:8090/ will return the result of 1 not 2 😄. If you swap Skip to Overwrite and re-run, you'll get the result of 2 instead!

The same parameter has also been added onto Use-PodeRoutes, meaning you can apply Skip to all Routes on mass:

Use-PodeRoutes -IfExists Skip

Furthermore, there's also a new Set-PodeRouteIfExistsPreference function which let's you overwrite the global default behaviour of Error:

Set-PodeRouteIfExistsPreference -Value Overwrite

In the case of the options:

  • Default: will use the IfExists value from higher up the hierarchy (see below) - if none defined, Error is the final default
  • Error: if the Route already exists, throw an error
  • Overwrite: if the Route already exists, delete the existing Route and recreate with the new definition
  • Skip: if the Route already exists, skip over the Route and do nothing

The default value that is used is defined by the following hierarchy:

  • IfExists value of the current Route (like Add-PodeRoute)
  • IfExists value from Use-PodeRoute
  • IfExists value from Set-PodeRouteIfExistsPreference
  • Error

Great. Especially the Set-PodeRouteIfExistsPreference. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Done
2 participants