signalfx.psm1
class SFxClient { [string]$Realm = 'us1' [string]$ApiVersion = 'v2' [string]$Uri [string]$Method [string] hidden $Endpoint [string] hidden $Path [hashtable] hidden $Headers = @{ } [hashtable] hidden $Body = @{ } [string] hidden $EnvName_Realm = 'SFX_REALM' [string] hidden $EnvName_AccessToken = 'SFX_ACCESS_TOKEN' [string] hidden $EnvName_UserToken = 'SFX_USER_TOKEN' SFxClient($endpoint, $path, $method) { if ([Environment]::GetEnvironmentVariables().Contains($this.EnvName_Realm)) { $this.SetRealm([Environment]::GetEnvironmentVariable($this.EnvName_Realm)) } $this.Endpoint = $endpoint $this.Path = $path $this.Method = $method $this.ConstructUri() } [void] ConstructUri() { $this.Uri = 'https://{0}.{1}.signalfx.com/{2}/{3}' -f $this.Endpoint, $this.Realm, $this.ApiVersion, $this.Path } [SFxClient] SetRealm([string]$realm) { $this.Realm = $realm $this.ConstructUri() return $this } [SFxClient] SetToken([string]$token) { if ($this.Headers.ContainsKey('X-SF-TOKEN')) { $this.Headers['X-SF-TOKEN'] = $token } else { $this.Headers.Add('X-SF-TOKEN', $token) } return $this } [object] Invoke() { $parameters = @{ Uri = $this.Uri Headers = $this.Headers ContentType = 'application/json' Method = $this.Method } if ($this.Body.Count -gt 0) { $parameters["Body"] = '[{0}]' -f ($this.Body | ConvertTo-Json) } return Invoke-RestMethod @parameters } [int64] GetTimeStamp() { return [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() } [int64] GetTimeStamp([DateTime]$timestamp) { return [DateTimeOffset]::new($timestamp).ToUnixTimeMilliseconds() } } class SFxClientApi : SFxClient { $delimiter = '?' SFxClientApi ($path, $method) : base ('api', $path, $method) { if ([Environment]::GetEnvironmentVariables().Contains('SFX_USER_TOKEN')) { $this.SetToken([Environment]::GetEnvironmentVariable('SFX_USER_TOKEN')) } } [char] GetDelimiter() { if ($this.delimiter -eq '?') { $this.delimiter = '&' return '?' } return $this.delimiter } } class SFxClientIngest : SFxClient { SFxClientIngest ($path, $method) : base ('ingest', $path, $method) { if ([Environment]::GetEnvironmentVariables().Contains('SFX_ACCESS_TOKEN')) { $this.SetToken([Environment]::GetEnvironmentVariable('SFX_ACCESS_TOKEN')) } } } class SFxClientBackfill : SFxClient { [Text.StringBuilder] $Body SFxClientBackfill () : base ('backfill', 'backfill', 'POST') { $this.ApiVersion = 'v1' # Apply the custom API version for this endpoint $this.ConstructUri() if ([Environment]::GetEnvironmentVariables().Contains('SFX_ACCESS_TOKEN')) { $this.SetToken([Environment]::GetEnvironmentVariable('SFX_ACCESS_TOKEN')) } # At least 360 datapoints an hour in the JSON format SFx is expecting is at least 13,320 chars # So, we might as well initialize the StringBuilder to hold at least that $this.Body = [Text.StringBuilder]::new(13400) } [object] Invoke() { $parameters = @{ Uri = $this.Uri Headers = $this.Headers ContentType = 'application/json' Method = $this.Method Body = $this.Body.ToString() } return Invoke-RestMethod @parameters } } # https://developers.signalfx.com/incidents_reference.html#tag/Retrieve-Alert-Muting-Rules-Query class SFxQueryAlertMuting : SFxClientApi { SFxQueryAlertMuting([string]$query) : base('alertmuting', 'GET') { $this.Uri = $this.Uri + '?query={0}' -f $query } [SFxQueryAlertMuting] Include([string]$include) { $this.Uri = $this.Uri + '&include={0}' -f $include return $this } [SFxQueryAlertMuting] OrderBy([string]$orderBy) { $this.Uri = $this.Uri + '&orderBy={0}' -f $orderBy return $this } [SFxQueryAlertMuting] Offset([int]$offset) { $this.Uri = $this.Uri + '&offset={0}' -f $offset return $this } [SFxQueryAlertMuting] Limit([int]$limit) { $this.Uri = $this.Uri + '&limit={0}' -f $limit return $this } } class SFxMuteFilter { [bool] $NOT = $false [string] $property [string] $propertyValue SFxMuteFilter([string]$p, [string]$v) { $this.property = $p $this.propertyValue = $v } SFxMuteFilter([bool]$n, [string]$p, [string]$v) { $this.NOT = $n $this.property = $p $this.propertyValue = $v } } # https://developers.signalfx.com/incidents_reference.html#tag/Create-Single-Alert-Muting-Rule class SFxNewAlertMuting : SFxClientApi { [datetime] hidden $StartTime [datetime] hidden $StopTime SFxNewAlertMuting([string]$description) : base('alertmuting', 'POST') { $this.Body.Add('description', $description) $this.SetStartTime() $this.SetStopTime('1h') } [SFxNewAlertMuting] AddFilter ([bool]$not, [string]$key, [string]$value) { $filter = [SFxMuteFilter]::new($not, $key, $value) if ($this.Body.ContainsKey('filters')) { $this.Body.filters += $filter } else { $this.Body.Add('filters', @( $filter )) } return $this } [SFxNewAlertMuting] AddFilter ([string]$key, [string]$value) { return $this.AddFilter($false, $key, $value) } [SFxNewAlertMuting] SetStartTime([DateTime]$timestamp) { $this.StartTime = $timestamp if ($this.Body.ContainsKey('startTime')) { $this.Body['startTime'] = $this.GetTimeStamp($this.StartTime) } else { $this.Body.Add('startTime', $this.GetTimeStamp($this.StartTime)) } return $this } [SFxNewAlertMuting] SetStartTime() { return $this.SetStartTime([datetime]::Now) } [SFxNewAlertMuting] SetStopTime([DateTime]$timestamp) { $this.StopTime = $timestamp if ($this.Body.ContainsKey('stopTime')) { $this.Body['stopTime'] = $this.GetTimeStamp($this.StopTime) } else { $this.Body.Add('stopTime', $this.GetTimeStamp($this.StopTime)) } return $this } [SFxNewAlertMuting] SetStopTime([string]$timespan) { $pattern = [regex]"(\d+)([mhd]{1})" $match = $pattern.Match($timespan) if ($match.Length -ne 2) { throw "Not a valid Timespan format" } else { $value = $match.Groups[1].Value $scale = $match.Groups[2].Value [DateTime]$datetime = switch -casesensitive ($scale) { 'm' { $this.StartTime.AddMinutes($value) break } 'h' { $this.StartTime.AddHours($value) break } 'd' { $this.StartTime.AddDays($value) break } } return $this.SetStopTime($datetime) } } [object] Invoke() { $parameters = @{ Uri = $this.Uri Headers = $this.Headers ContentType = 'application/json' Method = $this.Method } if ($this.Body.Count -gt 0) { $parameters["Body"] = ConvertTo-Json $this.Body } return Invoke-RestMethod @parameters } } # https://developers.signalfx.com/backfill_reference.html#tag/Backfill-MTS class SFxBackfill : SFxClientBackfill { SFxBackfill($orgId, $metricName) { $this.Uri = $this.Uri + '?orgid={0}&metric={1}' -f $orgId, $metricName } [SFxBackfill] SetMetricType([string]$type) { $this.Uri = $this.Uri + '&metric_type={0}' -f $type return $this } [SFxBackfill] AddDimension ([string]$key, [string]$value) { $this.Uri = $this.Uri + '&sfxdim_{0}={1}' -f $key, $value return $this } [void] AddValue ([string]$timestamp, [int64]$value) { $this.Body.AppendFormat('{{"timestamp":{0},"value":{1}}} ', $timestamp, $value) } } #https://developers.signalfx.com/detectors_reference.html#operation/Retrieve Detectors Query class SFxQueryDetector : SFxClientApi { SFxQueryDetector([string]$name) : base('detector', 'GET') { $this.Uri = $this.Uri + '?name={0}' -f $name } [SFxQueryDetector] Id([string]$id) { $this.Uri = $this.Uri + '&id={0}' -f $id return $this } [SFxQueryDetector] Tags([string]$tag) { $this.Uri = $this.Uri + '&tags={0}' -f $tag return $this } [SFxQueryDetector] Offset([int]$offset) { $this.Uri = $this.Uri + '&offset={0}' -f $offset return $this } [SFxQueryDetector] Limit([int]$limit) { $this.Uri = $this.Uri + '&limit={0}' -f $limit return $this } } # https://developers.signalfx.com/ingest_data_reference.html#operation/Send%20Custom%20Events class SFxPostEvent : SFxClientIngest { SFxPostEvent([string]$eventType) :base('event', 'POST') { $this.Body.Add('eventType', $eventType) $this.Body.Add('timestamp', $this.GetTimeStamp()) $this.Body.Add('category', 'USER_DEFINED') } [SFxPostEvent] SetCategory ([string]$category) { $valid = @("USER_DEFINED", "ALERT", "AUDIT", "JOB", "COLLECTED", "SERVICE_DISCOVERY", "EXCEPTION") if ($valid -notcontains $category) { throw "Invalid Category. Valid optiosn are [$($valid -join ', ')]" } $this.Body["category"] = $category return $this } [SFxPostEvent] AddDimension ([string]$key, [string]$value) { if ($this.Body.ContainsKey('dimensions')) { $this.Body.dimensions.Add($key, $value) } else { $this.Body.Add('dimensions', @{$key = $value }) } return $this } [SFxPostEvent] AddProperty ([string]$key, [string]$value) { if ($this.Body.ContainsKey('properties')) { $this.Body.properties.Add($key, $value) } else { $this.Body.Add('properties', @{$key = $value }) } return $this } } class SFxGetIncident : SFxClientAPI { SFxGetIncident() : base('incident', 'GET') { } [SFxGetIncident] IncludeResolved(){ $this.Uri = $this.Uri + '{0}includeResolved=true' -f $this.GetDelimiter() return $this } [SFxGetIncident] Offset([int]$offset) { $this.Uri = $this.Uri + '{0}offset={1}' -f $this.GetDelimiter(), $offset return $this } [SFxGetIncident] Limit([int]$limit) { $this.Uri = $this.Uri + '{0}limit={1}' -f $this.GetDelimiter(), $limit return $this } } class SFxClearIncident : SFxClientAPI { SFxClearIncident([string]$Id) : base('incident', 'PUT') { $this.Uri = $this.Uri + '/{0}/clear' -f $Id } } # https://developers.signalfx.com/metrics_metadata_reference.html#tag/Retrieve-Dimension-Metadata-Name-Value class SFxGetDimension : SFxClientApi { SFxGetDimension([string]$key, [string]$value) : base('dimension', 'GET') { $this.Uri = $this.Uri + '/{0}/{1}' -f $key, $value } } # https://developers.signalfx.com/metrics_metadata_reference.html#operation/Retrieve%20Dimensions%20Query class SFxQueryDimension : SFxClientApi { SFxQueryDimension([string]$query) : base('dimension', 'GET') { $this.Uri = $this.Uri + '?query={0}' -f $query } [SFxQueryDimension] OrderBy([string]$orderBy) { $this.Uri = $this.Uri + '&orderBy={0}' -f $orderBy return $this } [SFxQueryDimension] Offset([int]$offset) { $this.Uri = $this.Uri + '&offset={0}' -f $offset return $this } [SFxQueryDimension] Limit([int]$limit) { $this.Uri = $this.Uri + '&limit={0}' -f $limit return $this } } class SFxGetMetric : SFxClientApi { SFxGetMetric([string]$name) : base('metric', 'GET') { $this.Uri = $this.Uri + '/{0}' -f $name } } # https://developers.signalfx.com/metrics_metadata_reference.html#operation/Retrieve%20Dimensions%20Query class SFxQueryMetric : SFxClientApi { SFxQueryMetric([string]$query) : base('metric', 'GET') { $this.Uri = $this.Uri + '?query={0}' -f $query } [SFxQueryDimension] OrderBy([string]$orderBy) { $this.Uri = $this.Uri + '&orderBy={0}' -f $orderBy return $this } [SFxQueryDimension] Offset([int]$offset) { $this.Uri = $this.Uri + '&offset={0}' -f $offset return $this } [SFxQueryDimension] Limit([int]$limit) { $this.Uri = $this.Uri + '&limit={0}' -f $limit return $this } } # https://developers.signalfx.com/organizations_reference.html#tag/Retrieve-Organization-Members class SFxGetMember : SFxClientApi { SFxGetMember() : base('organization/member', 'GET') { } [SFxGetMember] Query([string]$query) { $this.Uri = $this.Uri + '{0}query={1}' -f $this.GetDelimiter(), $query return $this } [SFxGetMember] OrderBy([string]$orderBy) { $this.Uri = $this.Uri + '{0}orderBy={1}' -f $this.GetDelimiter(), $orderBy return $this } [SFxGetMember] Offset([int]$offset) { $this.Uri = $this.Uri + '{0}offset={1}' -f $this.GetDelimiter(), $offset return $this } [SFxGetMember] Limit([int]$limit) { $this.Uri = $this.Uri + '{0}limit={1}' -f $this.GetDelimiter(), $limit return $this } } # https://developers.signalfx.com/organizations_reference.html#tag/Invite-Member class SFxInviteMember : SFxClientApi { SFxInviteMember([string]$email) : base('organization/member', 'POST') { $this.Body.Add('email', $email) $this.Body.Add('admin', 'false') $this.Body.Add('fullName', [string]::Empty) $this.Body.Add('phone', [string]::Empty) $this.Body.Add('title', [string]::Empty) } [SFxInviteMember] SetAdmin() { $this.Body['admin'] = 'true' return $this } [SFxInviteMember] SetFullName([string]$name) { $this.Body['fullName'] = $name return $this } [SFxInviteMember] SetPhone([string]$phone) { $this.Body['phone'] = $phone return $this } [SFxInviteMember] SetTitle([string]$title) { $this.Body['title'] = $title return $this } [object] Invoke() { $parameters = @{ Uri = $this.Uri Headers = $this.Headers ContentType = 'application/json' Method = $this.Method } if ($this.Body.Count -gt 0) { $parameters["Body"] = $this.Body | ConvertTo-Json } try { return Invoke-RestMethod @parameters } catch { Write-Error ("StatusCode: {0}{1}StatusDescription: {2}" -f $_.Exception.Response.StatusCode.value__, [Environment]::NewLine ,$_.Exception.Response.StatusDescription) return $null } } } # https://developers.signalfx.com/organizations_reference.html#tag/Delete-Member-Using-ID class SFxRemoveMember : SFxClientApi { SFxRemoveMember([string]$id) : base('organization/member', 'DELETE') { $this.Uri = $this.Uri + '/{0}' -f $id } } # https://developers.signalfx.com/sessiontokens_reference.html#operation/Create%20Session%20Token class GetSFxSessionToken : SFxClientIngest { GetSFxSessionToken([pscredential]$credential) : base('session', 'POST') { $this.Body.Add('email', $credential.UserName) $this.Body.Add('password', $credential.GetNetworkCredential().Password) $this.Endpoint = 'api' $this.ConstructUri() } [object] Invoke() { $parameters = @{ Uri = $this.Uri Headers = $this.Headers ContentType = 'application/json' Method = $this.Method } if ($this.Body.Count -gt 0) { $parameters["Body"] = $this.Body | ConvertTo-Json } try { return Invoke-RestMethod @parameters } catch { Write-Error ("StatusCode: {0}{1}StatusDescription: {2}" -f $_.Exception.Response.StatusCode.value__, [Environment]::NewLine ,$_.Exception.Response.StatusDescription) return $null } } } function Clear-SFxIncident { [CmdletBinding()] param ( [Parameter(Position=0, Mandatory, ValueFromPipelineByPropertyName)] [Alias('incidentId')] [string] $Id, [Parameter(Position = 1)] [string] $ApiToken ) process { $request = [SFxClearIncident]::new($Id) if ($PSBoundParameters.ContainsKey('ApiToken')) { $request.SetToken($ApiToken) | Out-Null } Write-Information $request.Uri $request.Invoke() } } <# .SYNOPSIS Retrieves alerting muting rules based on search criteria .DESCRIPTION Retrieves alerting muting rules based on the query you specify in the query query parameter. This endpoint retrieves alert muting rules regardless of the version of the detector associated with the rule. .PARAMETER Id SignalFx-assigned ID of an alerting muting rule .PARAMETER Include Specifies the type of muting rules you want to retrieve. The allowed values are: Past Future Ongoing Open All .PARAMETER Query Query that specifies the muting rules you want to retrieve. .PARAMETER OrderBy The metadata property on which the API should sort the results. You don't have to include this property in the query, but the name must be a property of alert muting rules. .PARAMETER Offset The result object in the result set at which the API should start returning results to you. If omitted, the API starts at the first result in the set. .PARAMETER Limit The number of results to return from the result set. .PARAMETER ApiToken Authentication token .EXAMPLE PS C:\> <example usage> Explanation of what the example does .INPUTS String .OUTPUTS Object .NOTES Retrieves metadata objects for which the metrics name matches the search criteria. The API first collects all of the matching results. This is known as the result set. Depending on the values you specify for offset and limit, the number of metadata objects in the response body can be smaller than than the result set. For example, if you specify offset=0 (the default) and limit=50, and the API finds 100 matches, you only receive the first 50 results. #> function Get-SFxAlertMuting { [CmdletBinding(DefaultParameterSetName = "Query")] param ( [Parameter(Position = 0, Mandatory, ParameterSetName = "ID")] [string] $Id, [Parameter(Position = 0, Mandatory, ParameterSetName = "Query")] [string] $Query, [Parameter(Position = 1, ParameterSetName = "Query")] [ValidateSet('Past','Future','Ongoing','Open','All')] [string] $Include = 'All', [Parameter(Position = 2, ParameterSetName = "Query")] [string] $OrderBy, [Parameter(Position = 3, ParameterSetName = "Query")] [int] $Offset, [Parameter(Position = 4, ParameterSetName = "Query")] [int] $Limit, [Parameter(Position = 2, ParameterSetName = "ID")] [Parameter(Position = 5, ParameterSetName = "Query")] [string] $ApiToken ) process { switch ($PSCmdlet.ParameterSetName) { "ID" { Write-Warning "Not yet implemented" } "Query" { $request = [SFxQueryAlertMuting]::new($Query) if ($PSBoundParameters.ContainsKey('Include')) { $request.Include($Include) | Out-Null } if ($PSBoundParameters.ContainsKey('OrderBy')) { $request.OrderBy($OrderBy) | Out-Null } if ($PSBoundParameters.ContainsKey('Offset')) { $request.Offset($Offset) | Out-Null } if ($PSBoundParameters.ContainsKey('Limit')) { $request.Limit($Limit) | Out-Null } } } if ($PSBoundParameters.ContainsKey('ApiToken')) { $request.SetToken($ApiToken) | Out-Null } $request.Invoke() } } <# .SYNOPSIS Retrieves metadata for a dimension and value .DESCRIPTION Retrieves the metadata for the dimension and value specified in the key and value path parameters .PARAMETER Key Dimension name .PARAMETER Value Dimension value .PARAMETER Query Metric name search string. The string always starts with name:. You have the following search options: To search by metric name, use name:<metric_name>. This returns all of the metadata for that metric. To search for names using wildcards, use * as the wildcard character. For example, to search for all the metrics that start with cpu., use name:cpu.*. This returns metadata for cpu.utilization, cpu.num_cores, and so forth. .PARAMETER OrderBy Result object property on which the API should sort the results. This must be a property of the metrics metadata object. .PARAMETER Offset Object in the result set at which the API should start returning results to you. If omitted, the API starts at the first result in the set. API Default: 0 .PARAMETER Limit Number of results to return from the set of all metrics that match the query. .PARAMETER ApiToken Authentication token .EXAMPLE PS C:\> <example usage> Explanation of what the example does .INPUTS String .OUTPUTS Object .NOTES Retrieves metadata objects for which the metrics name matches the search criteria. The API first collects all of the matching results. This is known as the result set. Depending on the values you specify for offset and limit, the number of metadata objects in the response body can be smaller than than the result set. For example, if you specify offset=0 (the default) and limit=50, and the API finds 100 matches, you only receive the first 50 results. #> function Get-SFxDimensionMetadata { [CmdletBinding(DefaultParameterSetName = "KV")] param ( [Parameter(Position = 0, Mandatory, ParameterSetName = "KV")] [string] $Key, [Parameter(Position = 1, Mandatory, ParameterSetName = "KV")] [string] $Value, [Parameter(Position = 0, Mandatory, ParameterSetName = "Query")] [string] $Query, [Parameter(Position = 1, ParameterSetName = "Query")] [string] $OrderBy, [Parameter(Position = 2, ParameterSetName = "Query")] [int] $Offset, [Parameter(Position = 3, ParameterSetName = "Query")] [int] $Limit, [Parameter(Position = 2, ParameterSetName = "KV")] [Parameter(Position = 4, ParameterSetName = "Query")] [string] $ApiToken ) process { switch ($PSCmdlet.ParameterSetName) { "KV" { $request = [SFxGetDimension]::new($Key, $Value) } "Query" { $request = [SFxQueryDimension]::new($Query) if ($PSBoundParameters.ContainsKey('OrderBy')) { $request.OrderBy($OrderBy) | Out-Null } if ($PSBoundParameters.ContainsKey('Offset')) { $request.Offset($Offset) | Out-Null } if ($PSBoundParameters.ContainsKey('Limit')) { $request.Limit($Limit) | Out-Null } } } if ($PSBoundParameters.ContainsKey('ApiToken')) { $request.SetToken($ApiToken) | Out-Null } $request.Invoke() } } function Get-SFxIncident { [CmdletBinding()] param ( [Parameter(Position = 0)] [int] $Offset, [Parameter(Position = 1)] [int] $Limit, [Parameter(Position = 2)] [string] $ApiToken, [Parameter()] [Switch] $IncludeResolved ) $request = [SFxGetIncident]::new() if ($PSBoundParameters.ContainsKey('Offset')) { $request.Offset($Offset) | Out-Null } if ($PSBoundParameters.ContainsKey('Limit')) { $request.Limit($Limit) | Out-Null } if ($PSBoundParameters.ContainsKey('ApiToken')) { $request.SetToken($ApiToken) | Out-Null } if ($IncludeResolved) { $request.IncludeResolved() | Out-Null } Write-Information $request.Uri $request.Invoke() } <# .SYNOPSIS Generates a couple simple, precanned reports about recent Incidents. .DESCRIPTION The data returned by Get-SfxIncidents is pretty verbose. This cmdlet summarizes that information into some simple reports. .EXAMPLE PS C:\> Get-SfxIncidentReport -ByDetector Returns a count of the incidents from the last 7 days grouped by Detector Name. .EXAMPLE PS C:\> Get-SfxIncidentReport 30 -ByDetector Returns a count of the incidents from the last 30 days grouped by Detector Name. .EXAMPLE PS C:\> Get-SfxIncidentReport -SignalLossByHost Returns a count of the Hosts that have triggered a Loss of Signal alert in the last 7 days. This assumes you have a detector named 'host.loss_of_signal' .INPUTS Int .OUTPUTS GroupInfoNoElement .NOTES Experimental. These reports may not work for you. #> function Get-SfxIncidentReport { [CmdletBinding(DefaultParameterSetName = 'byDetector')] param ( [Parameter(Position = 0)] [int] $Days = 7, [Parameter(ParameterSetName = 'byDetector')] [switch] $ByDetector, [Parameter(ParameterSetName = 'SignalLossByHost')] [switch] $SignalLossByHost ) $incidents = GetRecentIncidents -days $Days switch ($PsCmdlet.ParameterSetName) { "byDetector" { $group = $incidents | Group-Object DetectorName # -NoElement | Sort-Object -Property Count -Descending foreach ($item in ($group | Sort-Object -Property Count -Descending) ) { $avgDur = $item.Group.Duration.totalseconds | Measure-Object -Minimum -Maximum -Average [pscustomobject]@{ Detector = $item.Name Count = $item.Count AvgDur = [timespan]::new(0, 0, $avgDur.Average) MinDur = [timespan]::new(0, 0, $avgDur.Minimum) MaxDur = [timespan]::new(0, 0, $avgDur.Maximum) } } } "SignalLossByHost" { $group = $incidents | Where-Object DetectorName -like "*loss*of*signal*" | ForEach-Object { $inputs = $_.Inputs | ConvertFrom-Json $hostvalue = $inputs._S7.key.host $_ | Add-Member -MemberType NoteProperty -Name 'Host' -Value $hostvalue -PassThru } | Group-Object -Property Host foreach ($item in ($group | Sort-Object -Property Count -Descending) ) { $avgDur = $item.Group.Duration.totalseconds | Measure-Object -Minimum -Maximum -Average [pscustomobject]@{ Host = $item.Name Count = $item.Count AvgDur = [timespan]::new(0, 0, $avgDur.Average) MinDur = [timespan]::new(0, 0, $avgDur.Minimum) MaxDur = [timespan]::new(0, 0, $avgDur.Maximum) } } } } } function GetRecentIncidents { param ( [int]$days ) $batchSize = 100 $count = 0 $cutoff = (Get-Date).AddDays($days * -1).ToUniversalTime() $oldest = (Get-Date).ToUniversalTime() $incidents = @() while ($oldest -gt $cutoff) { Write-Verbose "Querying SFx Limit [$batchSize] Offset [$count]" $batch = Get-SFxIncident -Limit $batchSize -Offset $count -IncludeResolved $data = foreach ($incident in $batch) { $firstEvent = $incident.events | Sort-Object timestamp -desc | Select-Object -first 1 $duration = if ($incident.duration) { [timespan]::FromMilliseconds($incident.duration) } else { $null } [PSCustomObject]@{ Active = [bool]$incident.active AnomalyState = $incident.anomalyState DetectLabel = $incident.detectLabel DetectorId = $incident.detectorId DetectorName = $incident.detectorName TimestampUtc = [DateTimeOffset]::FromUnixTimeMilliseconds($firstEvent.timestamp).UtcDateTime Duration = $duration IncidentId = $incident.incidentId IsMuted = [bool]$incident.isMuted Severity = $incident.severity TriggeredNotificationSent = [bool]$incident.triggeredNotificationSent TriggeredWhileMuted = [bool]$incident.triggeredWhileMuted Inputs = $firstEvent.inputs | ConvertTo-Json -Compress } } $incidents += $data $oldest = $incidents | Sort-Object -Property TimestampUtc | Select-Object -First 1 -ExpandProperty TimestampUtc $count += $batch.count if ($count -eq 0) { break } } $incidents | Where-Object { $_.TimestampUtc -gt $cutoff } | Sort-Object -Property TimestampUtc } function Get-SFxMember { [CmdletBinding()] param ( [Parameter(Position = 0)] [string] $Query, [Parameter(Position = 1)] [string] $OrderBy, [Parameter(Position = 2)] [int] $Limit, [Parameter(Position = 3)] [string] $ApiToken ) $request = [SFxGetMember]::new() if ($PSBoundParameters.ContainsKey('Query')) { $request.Query($Query) | Out-Null } if ($PSBoundParameters.ContainsKey('OrderBy')) { $request.OrderBy($OrderBy) | Out-Null } if ($PSBoundParameters.ContainsKey('Limit')) { $request.Limit($Limit) | Out-Null } if ($PSBoundParameters.ContainsKey('ApiToken')) { $request.SetToken($ApiToken) | Out-Null } Write-Information $request.Uri $request.Invoke() } function Get-SFxMetricMetadata { [CmdletBinding(DefaultParameterSetName = "Metric")] param ( [Parameter(Position = 0, Mandatory, ParameterSetName = "Metric")] [string] $Metric, [Parameter(Position = 0, Mandatory, ParameterSetName = "Query")] [string] $Query, [Parameter(Position = 1, ParameterSetName = "Query")] [string] $OrderBy, [Parameter(Position = 2, ParameterSetName = "Query")] [int] $Offset, [Parameter(Position = 3, ParameterSetName = "Query")] [int] $Limit, [Parameter(Position = 2, ParameterSetName = "Metric")] [Parameter(Position = 4, ParameterSetName = "Query")] [string] $ApiToken ) process { switch ($PSCmdlet.ParameterSetName) { "Metric" { $request = [SFxGetMetric]::new($Metric) } "Query" { $request = [SFxQueryMetric]::new($Query) if ($PSBoundParameters.ContainsKey('OrderBy')) { $request.OrderBy($OrderBy) | Out-Null } if ($PSBoundParameters.ContainsKey('Offset')) { $request.Offset($Offset) | Out-Null } if ($PSBoundParameters.ContainsKey('Limit')) { $request.Limit($Limit) | Out-Null } } } if ($PSBoundParameters.ContainsKey('ApiToken')) { $request.SetToken($ApiToken) | Out-Null } $request.Invoke() } } <# .SYNOPSIS Creates a new alert muting rule .DESCRIPTION Creates a new alert muting rule, based on the specifications in the request body. Unlike the detector APIs, you can use the alert muting APIs with detectors you create in the web UI as well as detectors you create with the API. .PARAMETER Description Description of the rule. .PARAMETER Filter List of alert muting filters for this rule, in the form of a JSON array of alert muting filter objects. Each object is a set of conditions for an alert muting rule. Each object property (name-value pair) specifies a dimension or custom property to match to alert events. .PARAMETER Duration The duration of the event in the form of [number][scale]. Scale options: m = minute h = hour d = day The default is 1h. .PARAMETER StartTime Starting time of an alert muting rule, in Unix time format UTC. If not specified, defaults to the current time. .PARAMETER ApiToken Authentication token .EXAMPLE PS C:\> <example usage> Explanation of what the example does .INPUTS String, Hashtable, Int64 .OUTPUTS Object .NOTES The SignalFx API will return the string "OK" if the POST is successful. #> function New-SFxAlertMuting { [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory)] [string] $Description, [Parameter(Position = 1)] [hashtable] $Filter, [Parameter(Position=2)] [ValidatePattern("(\d+)([mhd]{1})")] [string] $Duration, [Parameter(Position=3)] [ValidateScript({if ($_ -ge (Get-Date)) {$true} else {Throw "StarTime must not be in the past."}})] [datetime] $StartTime, [Parameter(Position = 4)] [string] $ApiToken ) process { $request = [SFxNewAlertMuting]::new($Description) if ($PSBoundParameters.ContainsKey('Filter')) { Foreach ($key in $Filter.Keys) { $request.AddFilter($key, $Filter[$key]) | Out-Null } } if ($PSBoundParameters.ContainsKey('StartTime')) { $request.SetStartTime($StartTime) | Out-Null } if ($PSBoundParameters.ContainsKey('Duration')) { $request.SetStopTime($Duration) | Out-Null } if ($PSBoundParameters.ContainsKey('ApiToken')) { $request.SetToken($ApiToken) | Out-Null } $request.Invoke() } } function New-SFxMember { [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory, ValueFromPipelineByPropertyName)] [string] $Email, [Parameter(Position=1, ValueFromPipelineByPropertyName)] [string] $Fullname, [Parameter(Position=2, ValueFromPipelineByPropertyName)] [string] $Title, [Parameter(Position=3, ValueFromPipelineByPropertyName)] [string] $Phone, [Parameter(Position=4, ValueFromPipelineByPropertyName)] [switch] $Admin, [Parameter(Position = 5)] [string] $ApiToken ) process { $request = [SFxInviteMember]::new($Email) if ($PSBoundParameters.ContainsKey('Fullname')) { $request.SetFullname($Fullname) | Out-Null } if ($PSBoundParameters.ContainsKey('Title')) { $request.SetTitle($Title) | Out-Null } if ($PSBoundParameters.ContainsKey('Phone')) { $request.SetPhone($Phone) | Out-Null } if ($Admin) { $request.SetAdmin() | Out-Null } if ($PSBoundParameters.ContainsKey('ApiToken')) { $request.SetToken($ApiToken) | Out-Null } Write-Information $request.Uri Write-Information $request.Body $request.Invoke() } } <# .SYNOPSIS Creates a session token .DESCRIPTION Creates a session token (referred to as an User API Access Token in the web UI) that provides authentication for other SignalFx API calls. .PARAMETER Credential The email address you used to join the organization for which you want a session token. The password you provided to SignalFx when you accepted an invitation to join an organization. If you're using an external protocol such as SAML or LDAP to connect to SignalFx, you can't use that protocol's credentials. .EXAMPLE PS C:\> Get-SFxSessionToken -Credential (Get-Credential) abc123-zyx098 .INPUTS pscredential .OUTPUTS string .NOTES You can't use a session token for authenticating a /datapoint, /backfill, or /event API call. These APIs require an org token (referred to as an access token in the web UI.) #> function New-SFxSessionToken { [CmdletBinding()] param ( [Parameter(Mandatory, Position=0)] [pscredential] $Credential, [Parameter(Position = 1)] [string] $ApiToken ) $request = [GetSFxSessionToken]::new($Credential) if ($PSBoundParameters.ContainsKey('ApiToken')) { $request.SetToken($ApiToken) | Out-Null } Write-Information $request.Uri $result = $request.Invoke() Write-Information ($result | Format-List | Out-String) $result | Select-Object -ExpandProperty accessToken } <# .SYNOPSIS Sends custom events to SignalFx .DESCRIPTION Sends custom events to SignalFx. Use this API to send events that SignalFx itself doesn't detect, such as software deployments or hardware changes. You can then correlate these events with changes detected in your metrics. .PARAMETER EventType EventType must be a non-empty ASCII string with a length less than or equal to 256 characters. .PARAMETER Dimension A list of key-value pairs that specify dimension names and values to associate with the event. SignalFx assumes that each value of eventType you send is associated with a specific set of dimension names and values. .PARAMETER Property A list of key-value pairs that specify properties of the specified event. .PARAMETER Category A category that describes the custom event, in the form of one of the allowed enumerated types: USER_DEFINED: The default for custom events ALERT: Used by SignalFx to mark an event generated by a detector. AUDIT: Used by third-party integrations JOB: Event generated by a SignalFx or third-party background job COLLECTD: Generated by the SignalFx CollectD integration SERVICE_DISCOVERY: Generated by third-party integrations EXCEPTION: A software exception occurred .PARAMETER ApiToken Authentication token .EXAMPLE PS C:\> <example usage> Explanation of what the example does .INPUTS String, Hashtable, Int64 .OUTPUTS Object .NOTES The SignalFx API will return the string "OK" if the POST is successful. #> function Publish-SFxEvent { [CmdletBinding()] param ( [ValidateLength(1, 256)] [ValidateNotNullOrEmpty()] [Parameter(Position = 0, Mandatory)] [string] $EventType, [Parameter(Position = 1)] [hashtable] $Dimension, [Parameter(Position = 2)] [hashtable] $Property, [ValidateSet("USER_DEFINED", "ALERT", "AUDIT", "JOB", "COLLECTED", "SERVICE_DISCOVERY", "EXCEPTION")] [Parameter(Position = 5)] [string] $Category, [Parameter(Position = 6)] [string] $ApiToken ) process { $request = [SFxPostEvent]::new($EventType) if ($PSBoundParameters.ContainsKey('Dimension')) { Foreach ($key in $Dimension.Keys) { $request.AddDimension($key, $Dimension[$key]) | Out-Null } } if ($PSBoundParameters.ContainsKey('Property')) { Foreach ($key in $Property.Keys) { $request.AddProperty($key, $Property[$key]) | Out-Null } } if ($PSBoundParameters.ContainsKey('Category')) { $request.SetCategory($Category) | Out-Null } if ($PSBoundParameters.ContainsKey('ApiToken')) { $request.SetToken($ApiToken) | Out-Null } Write-Information $request.Uri Write-Information ($request.Body | ConvertTo-Json) $request.Invoke() } } <# .SYNOPSIS Sends historical MTS to SignalFx .DESCRIPTION Sends historical MTS to SignalFx, overwriting any existing datapoints for the same time period. .PARAMETER OrgId The SignalFx ID for the organization that should receive the incoming data .PARAMETER Name The name of the metric in the MTS that you're backfilling .PARAMETER Type The metric type for the metric you're backfilling. .PARAMETER Dimension Designates one or mroe of the dimension names that identify the MTS you're backfilling, up to the limit of 36 dimensions. Note: You must specify all the dimensions associated with the MTS. If you don't, the backfill creates a new MTS based on the dimensions you specify. .PARAMETER ApiToken Authentication token .EXAMPLE PS C:\> <example usage> Explanation of what the example does .INPUTS String, DateTime .OUTPUTS .NOTES A single call to /backfill can only refer to a single MTS specified by its metric type, metric name, and dimensions. Use the API in "bulk" mode. It's designed to accept thousands of datapoints in each call, such as 1 hour of points with a resolution of one second or one day of points with a resolution of one minute. Timestamps for each datapoint must be monotonically ascending. A single call to /backfill must contain one or more hour-long groups of datapoints, with each hour starting one millisecond after the top of the hour and ending exactly at the top of the following hour. Avoid large gaps in the data, because the provided data replaces all of the data in the equivalent time period of existing data. For example, if you have one hundred datapoints for an MTS over one hour, and you backfill with 20 datapoints for the same MTS over the same hour, you're left with 20 datapoints. NOTE: /backfill doesn't support the built-in sf_hires dimension that marks datapoints as high resolution. #> function Publish-SFxMetricBackfill { [CmdletBinding()] param ( [Parameter(Position=0, Mandatory)] [string] $OrgId, [Parameter(Position=1, Mandatory)] [string] $Name, [Parameter(Position=2, Mandatory)] [ValidateSet('gauge','counter','cumulative_counter')] [string] $Type, [Parameter(Position=3, Mandatory)] [hashtable] $Dimension, [Parameter(Position = 4)] [string] $ApiToken, [Parameter(ValueFromPipelineByPropertyName, Mandatory)] [object] $Timestamp, [Parameter(ValueFromPipelineByPropertyName, Mandatory)] [int64] $Value ) begin { $request = [SFxBackfill]::new($OrgId, $Name).SetMetricType($Type) Foreach ($key in $Dimension.Keys) { $request.AddDimension($key, $Dimension[$key]) | Out-Null } if ($PSBoundParameters.ContainsKey('ApiToken')) { $request.SetToken($ApiToken) | Out-Null } } process { # TODO: Somehow handle the stict batching requirements # A single call to /backfill must contain one or more hour-long groups of datapoints, with # each hour starting one millisecond after the top of the hour and ending exactly at the top # of the following hour. if ($Timestamp.GetType().Name -eq 'DateTime' ) { $Timestamp = $request.GetTimeStamp($Timestamp) } $request.AddValue($Timestamp, $Value) } end { Write-Information ("Request URI: {0}" -f $request.Uri) $request.Invoke() } } function Remove-SFxMember { [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory, ValueFromPipelineByPropertyName)] [string] $Id, [Parameter(Position = 1)] [string] $ApiToken ) process { $request = [SFxRemoveMember]::new($Id) if ($PSBoundParameters.ContainsKey('ApiToken')) { $request.SetToken($ApiToken) | Out-Null } Write-Information $request.Uri $request.Invoke() } } <# .SYNOPSIS Configured the SignalFx Realm .DESCRIPTION A realm is a self-contained deployment of SignalFx that hosts your organization. The API endpoint for a realm contains a realm-specific path segment. For example: For the us1 realm, the endpoint for sending metrics is https://ingest.us1.signalfx.com/v2. For the eu0 realm, the endpoint for sending metrics is https://ingest.eu0.signalfx.com/v2 .EXAMPLE PS C:\> Set-SFxRealm eu0 This sets the API endpoign to https://{api|ingest}.eu0.signalfx.com/v2. .INPUTS string .NOTES To find the name of your realm, go to your profile page in the web UI. #> function Set-SFxRealm { [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory)] [ValidateSet('eu0', 'us0', 'us1')] [string] $Realm ) $env:SFX_REALM = $Realm } <# .SYNOPSIS Configure SignalFx API Tokens .DESCRIPTION SignalFx REST requests use token-based authentication. SignalFx uses two types of tokens: Org tokens Known as Access Tokens in the SignalFx web UI, org tokens are long-lived organization-level tokens. Session tokens Known as User API Access Tokens in the SignalFx web UI, session tokens are short-lived user-level tokens. .EXAMPLE PS C:\> Set-SFx-Token abc123 Set $env:SFX_USER_TOKEN = 'abc123' .EXAMPLE PS C:\> Set-SFx-Token -UserToken abc123 -OrgToken xyz890 Set $env:SFX_USER_TOKEN = 'abc123' and $env:SFX_ACCESS_TOKEN = 'xyz890' .INPUTS string .NOTES To get the org token for your organization, go to the Organization Overview in the SignalFx web UI and click the Access Tokens option. SignalFx administrators can also get a new token or manage organization tokens in this location. To get a session token, go to your profile page to generate a User API Access Token. #> function Set-SFxToken { [CmdletBinding()] param ( [Parameter(Position = 0)] [Alias('SessionToken')] [string] $UserToken, [Parameter(Position = 1)] [Alias('AccessToken')] [string] $OrgToken ) if ($PSBoundParameters.ContainsKey('UserToken')) { [Environment]::SetEnvironmentVariable('SFX_USER_TOKEN', $UserToken) } if ($PSBoundParameters.ContainsKey('OrgToken')) { [Environment]::SetEnvironmentVariable('SFX_ACCESS_TOKEN', $OrgToken) } <# TODO: The endpoint https://api.{REALM}.signalfx.com/v2/session manages session tokens. You don’t need a token to create a session token, but you do need to specify the email and password of an organization member. #> } |