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. #>

}