Pode
Pode copied to clipboard
Invoking "Use-PodeRoutes" as schedule
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. :)
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 🤔:
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.
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.
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?
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?
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 🤔
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.
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.
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 @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.
Linking with https://github.com/Badgerati/Pode.Web/issues/367
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
I've added a new
-IfExists
parameter toAdd-PodeRoute
,Add-PodeStaticRoute
, andAdd-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 of1
not2
😄. If you swapSkip
toOverwrite
and re-run, you'll get the result of2
instead!The same parameter has also been added onto
Use-PodeRoutes
, meaning you can applySkip
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
. :)