Last active
February 11, 2025 18:58
-
-
Save PanosGreg/c621a041d561a07f04946a1fffba2bd3 to your computer and use it in GitHub Desktop.
New Relic functions for Synthetic Monitors (enable/disable, get) and API Keys (get)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# change this Enum with your actual accounts and their IDs | |
enum NewRelicAccountName { | |
NRAcc1 = 300000 | |
NRAcc2 = 400000 | |
NRAcc3 = 500000 | |
NRAcc4 = 600000 | |
} | |
function Get-NRMonitor { | |
<# | |
.SYNOPSIS | |
Get Synthetic Monitors from New Relic, using GraphQL | |
.DESCRIPTION | |
To get all the monitors using GraphQL, we need to use paging | |
which is done by using the the NextCursor property | |
Unfortunately, unlike the REST API, there is no option to have an offset and limit properties. | |
Which means, the web requests must be done serially, they cannot be done in parallel. | |
So each time we send the web request, we receive a new cursor and we use that to get the next batch of items. | |
.EXAMPLE | |
$key = 'NRAK-123...' | |
$all = Get-NRMonitor -ApiKey $key | |
get all the monitors from New Relic | |
.EXAMPLE | |
$key = 'NRAK-123...' | |
$mon = Get-NRMonitor -ApiKey $key -Account NRAcc1 -SearchFor MyApp -Verbose | |
get only the monitors for a specific account that have a specific string in their name | |
.EXAMPLE | |
$key = 'NRAK-123...' | |
$mon = Get-NRMonitor -ApiKey $key -Account NRAcc1,NRAcc3 | |
get all the monitors from two accounts | |
#> | |
[cmdletbinding(DefaultParameterSetName = 'FilterLike')] | |
param ( | |
[Parameter(Mandatory)] | |
[ValidatePattern('^NRAK-[A-Z|0-9]{27}$')] | |
[string]$ApiKey, | |
[NewRelicAccountName[]]$Account, # <-- optional, default is all accounts | |
[Parameter(ParameterSetName = 'FilterLike')] | |
[ValidateNotNullOrWhiteSpace()] | |
[string]$SearchFor, | |
[Parameter(ParameterSetName = 'FilterEqual')] | |
[ValidateNotNullOrWhiteSpace()] | |
[string]$SearchExactlyFor | |
) | |
# helper function | |
function Convert-NREntityObject($InputObject) { | |
$InputObject.data.actor.entitySearch.results.entities | foreach { | |
$Tags = @{} | |
$_.tags | foreach {$Tags[$_.key] = $_.values | foreach {$_}} | |
[pscustomobject] @{ | |
PSTypeName = 'NewRelic.SyntheticMonitor' | |
EntityID = $_.guid | |
ID = $_.monitorId | |
Name = $_.name | |
URL = $_.monitoredUrl | |
Status = $_.monitorSummary.status | |
Account = @{Name=$_.account.name;ID=$_.account.id} | |
Tags = $Tags | |
} | |
} | |
} | |
# set some default values for Invoke-RestMethod | |
$PSDefaultParameterValues = @{ | |
'Invoke-RestMethod:Uri' = 'https://api.newrelic.com/graphql' | |
'Invoke-RestMethod:Method' = 'POST' | |
'Invoke-RestMethod:Headers' = @{'API-Key' = $ApiKey} | |
'Invoke-RestMethod:ContentType' = 'application/json' | |
'Invoke-RestMethod:Verbose' = $false | |
'Invoke-RestMethod:TimeoutSec' = 5 # <-- this is the ConnectionTimeoutSeconds on PS v7+ | |
'Invoke-RestMethod:UseBasicParsing' = $true # <-- has no effect on PS v6+ | |
'Invoke-RestMethod:ErrorAction' = 'Stop' | |
} | |
$WithoutCursor = @' | |
query GetMonitors($Monitors: String!) { | |
actor { | |
entitySearch(query: $Monitors) { | |
query | |
count | |
results { | |
nextCursor | |
entities { | |
... on SyntheticMonitorEntityOutline { | |
name | |
guid | |
monitorSummary {status} | |
account {id, name} | |
monitoredUrl | |
monitorId | |
tags {key, values} | |
} | |
} | |
} | |
} | |
} | |
} | |
'@ | |
$WithCursor = @' | |
query GetMonitors($Monitors: String!) { | |
actor { | |
entitySearch(query: $Monitors) { | |
results(cursor: "@CURSOR@") { | |
nextCursor | |
entities { | |
... on SyntheticMonitorEntityOutline { | |
name | |
guid | |
monitorSummary {status} | |
account {id, name} | |
monitoredUrl | |
monitorId | |
tags {key, values} | |
} | |
} | |
} | |
} | |
} | |
} | |
'@ | |
$BaseQuery = [System.Text.StringBuilder]::new("domain = 'SYNTH' AND type = 'MONITOR'") | |
if ($Account.Count -eq 1) {[void]$BaseQuery.Append(" AND accountId = $($Account.value__)")} | |
elseif ($Account.Count -ge 2) {[void]$BaseQuery.Append(" AND accountId in ($($Account.value__ -join ','))")} | |
if ($PSBoundParameters.ContainsKey('SearchFor')) {[void]$BaseQuery.Append(" AND name like '$SearchFor'")} | |
elseif ($PSBoundParameters.ContainsKey('SearchExactlyFor')) {[void]$BaseQuery.Append(" AND name = '$SearchExactlyFor'")} | |
$Body = @{query = '' ; variables = @{Monitors = $BaseQuery.ToString()}} | |
# inital query, where we'll get the 1st cursor | |
$Body['query'] = $WithoutCursor | |
$response = Invoke-RestMethod -Body ($Body | ConvertTo-Json) -EA Stop | |
$Cursor = $response.data.actor.entitySearch.results.nextCursor | |
$Count = $response.data.actor.entitySearch.count | |
$First = Convert-NREntityObject -InputObject $response | |
$ReqCount = [math]::Ceiling($Count/200) | |
Write-Verbose "Query: $($response.data.actor.entitySearch.query)" | |
Write-Verbose "Found: $Count monitors (will need to send $ReqCount web calls)" | |
# now iterate through all the remaining queries until we get them all | |
$i = 1 # <-- I start from 1 and not 0, cause we already sent 1 web request above | |
$Results = while ($Cursor -ne $null) { | |
$i++ | |
$pc = [math]::Round(($i/$ReqCount)*100,0) | |
if ($ReqCount -ge 4) { # <-- show the progress bar only if we'll be doing 3 or more extra web requests (4 = 1 initial + 3 extra) | |
Write-Progress -Activity 'Collect New Relic Monitors' -Status "$pc% Complete:" -PercentComplete $pc | |
} | |
$Body['query'] = $WithCursor.Replace('@CURSOR@',$Cursor) | |
$response = Invoke-RestMethod -Body ($Body | ConvertTo-Json) | |
# refresh the cursor with the next one | |
$Cursor = $response.data.actor.entitySearch.results.nextCursor | |
# output the results | |
Convert-NREntityObject -InputObject $response | |
} #while | |
$CollectedCount = $First.Count + $Results.Count | |
if ($CollectedCount -ne $Count) {Write-Warning "Got $CollectedCount monitors instead of $Count"} | |
if ([bool]$Results) {Write-Output ($First + $Results)} | |
else {Write-Output $First} | |
} | |
function Switch-NRMonitor { | |
<# | |
.SYNOPSIS | |
It enables or disables one or more monitors (New Relic Synthetic Monitors) | |
This function uses a REST API endpoint, not a GraphQL mutation (via NerdGraph) | |
.DESCRIPTION | |
This function will terminate if there is any error. | |
So for example if you have 5 IDs and the 3rd one was not found, | |
then the function will exit and the 4th and 5th won't be executed. | |
This function is idempotent, so if a monitor is already Enabled and you try to | |
enable it, the function will identify that and will not do anything. | |
.EXAMPLE | |
$Key = 'NRAK-123...' | |
$IDs = 'e30fe1b8-46cc-4b9c-b7d0-df5a29ffb06a','c66e1635-c303-40c3-9fc6-7b5ad19958a1' | |
Enable-NRMonitor -MonitorID $IDs -ApiKey $Key -Verbose -PassThru | |
# enable two monitors | |
.NOTES | |
Documentation sources: | |
- REST API Endpoint URL | |
https://docs.newrelic.com/docs/apis/synthetics-rest-api/monitor-examples/manage-synthetics-monitors-rest-api/#use-api | |
- Get a specific monitor | |
https://docs.newrelic.com/docs/apis/synthetics-rest-api/monitor-examples/manage-synthetics-monitors-rest-api/#get-specific-monitor | |
- Update individual attribute(s) of synthetic monitors | |
https://docs.newrelic.com/docs/apis/synthetics-rest-api/monitor-examples/manage-synthetics-monitors-rest-api/#patch-monitor | |
#> | |
[Alias('Enable-NRMonitor')] | |
[Alias('Disable-NRMonitor')] | |
[OutputType([void],[psobject])] # <-- default is [void], and [psobject] with PassThru | |
[CmdletBinding()] | |
param ( | |
[Parameter(Mandatory)] | |
[guid[]]$MonitorID, | |
[Parameter(Mandatory)] | |
[ValidatePattern('^NRAK-[A-Z|0-9]{27}$')] | |
[string]$ApiKey, | |
[ValidateSet('US','EU')] | |
[string]$EndpointRegion = 'US', | |
[switch]$PassThru | |
) | |
trap { | |
Write-Warning 'There was an error with the web request to New Relic!' | |
Write-Warning @' | |
Here are some possible scenarios on what might be happening: | |
- [404] Not Found = No monitor found for that ID. Make sure your API Key is for the same account as the monitor. | |
Make sure the monitor ID is correct. Make sure the REST API Endpoint (US or EU) is correct. | |
- [403] Forbidden = Same as access denied. Make sure you have permissions to change the monitor. | |
- [400] Bad Request = There might be a policy that does not allow specific values on the attributes of the monitor. | |
Alternatively, invalid BASE64 encoded string, maybe try again. | |
- [429] Too many requests = You may have hit the limit of 3 requests per second on the synthetic monitor API. | |
You may have hit the limit of requests per seconds on the account level. | |
- [405] Method not allowed = Invalid HTTP Method (ex.GET,POST,PATCH). | |
There might be a policy that allows only specific methods. | |
- [504] Timeout = The server side from New Relic did not respond within a timely manner. | |
- [500] Error = Generic server error from New Relic side. | |
'@ | |
break # <-- will execute the trap and then show the error from the statement, and then exit the function | |
} | |
# find which operation we'll do | |
$Verb = ($MyInvocation.InvocationName).Split('-')[0] | |
switch ($Verb) { | |
'Enable' {$NewStatus = 'ENABLED'} | |
'Disable' {$NewStatus = 'DISABLED'} | |
default {Write-Warning 'Please use the alias of this command, for either Enable or Disable';return} | |
} | |
# set some default values for Invoke-RestMethod | |
$PSDefaultParameterValues['Invoke-RestMethod:Headers'] = @{'API-Key' = $ApiKey} | |
$PSDefaultParameterValues['Invoke-RestMethod:ContentType'] = 'application/json' | |
$PSDefaultParameterValues['Invoke-RestMethod:Verbose'] = $false | |
$PSDefaultParameterValues['Invoke-RestMethod:TimeoutSec'] = 5 # <-- this is the ConnectionTimeoutSeconds on PS v7+ | |
$PSDefaultParameterValues['Invoke-RestMethod:UseBasicParsing'] = $true # <-- has no effect on PS v6+ | |
$PSDefaultParameterValues['Invoke-RestMethod:ErrorAction'] = 'Stop' | |
# get the base URL depending on the region | |
if ($EndpointRegion -eq 'US') {$BaseUrl = 'https://synthetics.newrelic.com/synthetics/api'} | |
elseif ($EndpointRegion -eq 'EU') {$BaseUrl = 'https://synthetics.eu.newrelic.com/synthetics/api'} | |
# loop through each monitor | |
$out = foreach ($ID in $MonitorID) { | |
$Url = "$BaseUrl/v3/monitors/$ID" | |
# first make sure you can get the monitor | |
$Monitor = Invoke-RestMethod -Uri $Url -Method Get | |
# get the name of the monitor (sometimes the URI might be empty, not sure why) | |
if ($Monitor.uri) {$Name = $Monitor.uri} | |
else {$Name = $Monitor.name} | |
# now flip the status | |
if ($Monitor.status -ne $NewStatus) { | |
Write-Verbose "Change the status for monitor $Name to $NewStatus" | |
Invoke-RestMethod -Uri $Url -Method Patch -Body "{`"status`": `"$NewStatus`"}" | Out-Null | |
# and then refresh the monitor variable | |
$Monitor = Invoke-RestMethod -Uri $Url -Method Get | |
} | |
else { | |
Write-Verbose "The monitor for $Name is already $NewStatus" | |
} | |
# make sure the monitor was flipped | |
if ($Monitor.status -ne $NewStatus) { | |
throw "The status monitor for $Name was NOT changed to $NewStatus" | |
} | |
Write-Output $Monitor | |
} #foreach monitor | |
# return the object(s) if PassThru was used | |
if ($PassThru) {Write-Output $out} | |
} | |
function Get-NRApiKey { | |
<# | |
.SYNOPSIS | |
Get all the API Keys and their associated accounts for the user of the provided API Key | |
.EXAMPLE | |
$key = 'NRAK-123...' | |
$all = Get-NRApiKey -ApiKey $key -Verbose | |
get all the keys from New Relic for the user of the provided API Key | |
#> | |
[outputtype([psobject])] | |
[cmdletbinding()] | |
param ( | |
[Parameter(Mandatory)] | |
[ValidatePattern('^NRAK-[A-Z|0-9]{27}$')] | |
[string]$ApiKey | |
) | |
$PSDefaultParameterValues = @{ | |
'Invoke-RestMethod:Uri' = 'https://api.newrelic.com/graphql' | |
'Invoke-RestMethod:Method' = 'POST' | |
'Invoke-RestMethod:Headers' = @{'API-Key' = $ApiKey} | |
'Invoke-RestMethod:ContentType' = 'application/json' | |
'Invoke-RestMethod:Verbose' = $false | |
'Invoke-RestMethod:TimeoutSec' = 5 # <-- this is the ConnectionTimeoutSeconds on PS v7+ | |
'Invoke-RestMethod:UseBasicParsing' = $true # <-- has no effect on PS v6+ | |
'Invoke-RestMethod:ErrorAction' = 'Stop' | |
} | |
# helper function | |
function ConvertFrom-UnixTime { | |
[OutputType([DateTime])] | |
Param( | |
[Parameter(Mandatory,ValueFromPipeline=$true)] | |
[int64]$Ticks # <-- UNIX time long number | |
) | |
$Utc = [System.DateTimeKind]::Utc | |
[DateTime]::new(1970, 1, 1, 0, 0, 0, 0, $Utc).AddSeconds($Ticks).ToLocalTime() | |
} | |
# first get your User ID | |
Write-Verbose 'Get the User ID of the current user' | |
$UserInfoQuery = @' | |
{ | |
actor { | |
user { | |
name | |
id | |
} | |
} | |
} | |
'@ | |
$Body = @{query = $UserInfoQuery} | ConvertTo-Json | |
$User = (Invoke-RestMethod -Body $Body).data.actor.user | |
# then you need to find the API keys for your user (using your User ID) | |
# I assume that your key is a USER key and not INGEST | |
Write-Verbose 'Get the API Key IDs for all the user''s keys' | |
$KeySearchQuery = @' | |
{ | |
actor { | |
apiAccess { | |
keySearch(query: {scope: {userIds: @USERID@}, types: USER}) { | |
keys { | |
id | |
name | |
} | |
} | |
} | |
} | |
} | |
'@ | |
$Body = @{query = $KeySearchQuery.Replace('@USERID@',$User.id)} | ConvertTo-Json | |
$KeyIDs = (Invoke-RestMethod -Body $Body).data.actor.apiAccess.keySearch.keys | |
# finally you can now find your API keys and their associated accounts | |
Write-Verbose "Get the details for $($KeyIDs.Count) keys" | |
$ApiKeyQuery = @' | |
{ | |
actor { | |
apiAccess { | |
key( | |
id: "@KEYID@" | |
keyType: USER | |
) { | |
... on ApiAccessUserKey { | |
id | |
name | |
type | |
key | |
createdAt | |
account { | |
name | |
id | |
} | |
user { | |
id | |
name | |
} | |
} | |
} | |
} | |
} | |
} | |
'@ | |
$i = 0 | |
$KeyInfo = foreach ($KeyObj in $KeyIDs) { | |
$i++ | |
$pc = [math]::Round(($i/$KeyIDs.Count)*100,0) # <-- pc means percent | |
if ($KeyIDs.Count -ge 2) { # <-- show the progress bar only if we'll be doing 2 or more web requests | |
Write-Progress -Activity 'Collect New Relic API Keys' -Status "$pc% Complete:" -PercentComplete $pc | |
} | |
$Body = @{query = $ApiKeyQuery.Replace('@KEYID@',$KeyObj.id)} | ConvertTo-Json | |
(Invoke-RestMethod -Body $Body).data.actor.apiAccess.key | |
} | |
$List = foreach ($k in $KeyInfo) { | |
[pscustomobject] @{ | |
PSTypeName = 'NewRelic.ApiKey' | |
Name = $k.name | |
ID = $k.id | |
Account = $k.account.name | |
AccountID = $k.account.id -as [int64] | |
KeyType = $k.type | |
CreatedAt = ConvertFrom-UnixTime -Ticks $k.createdAt | |
Key = $k.Key | ConvertTo-SecureString -AsPlainText -Force | |
UserID = $k.user.id | |
UserName = $k.user.name | |
UserEmail = $k.user.email | |
} | |
} | |
# add a default view for the output object | |
$PSet = [Management.Automation.PSPropertySet] | |
$Prop = 'Name','Account','UserName','CreatedAt' | |
$DDPS = $PSet::new('DefaultDisplayPropertySet',[string[]]$Prop) | |
$Std = [Management.Automation.PSMemberInfo[]]@($DDPS) | |
$List | Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $Std -Force | |
Write-Output $List | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment