Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions alert_channels.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package linodego

import (
"context"
)

// AlertChannelEnvelope represents a single alert channel entry returned inside alert definition
type AlertChannelEnvelope struct {
ID int `json:"id"`
Label string `json:"label"`
Type string `json:"type"`
URL string `json:"url"`
}

// AlertChannel represents a Monitor Channel object.
type AlertChannel struct {
ID int `json:"id"`
Label string `json:"label"`
ChannelType string `json:"channel_type"`
Content ChannelContent `json:"content"`
Created string `json:"created"`
CreatedBy string `json:"created_by"`
Updated string `json:"updated"`
UpdatedBy string `json:"updated_by"`
URL string `json:"url"`
}

// AlertChannelDetail represents the details of a Monitor Channel.
type AlertChannelDetail struct {
To string `json:"to,omitempty"`
From string `json:"from,omitempty"`
User string `json:"user,omitempty"`
Token string `json:"token,omitempty"`
URL string `json:"url,omitempty"`
}

// AlertChannelCreateOptions are the options used to create a new Monitor Channel.
type AlertChannelCreateOptions struct {
Label string `json:"label"`
Type string `json:"type"`
Details AlertChannelDetailOptions `json:"details"`
}

// AlertChannelDetailOptions are the options used to create the details of a new Monitor Channel.
type AlertChannelDetailOptions struct {
To string `json:"to,omitempty"`
}

// AlertingChannelCreateOptions are the options used to create a new Monitor Channel.
//
// Deprecated: AlertChannelCreateOptions should be used in all new implementations.
type AlertingChannelCreateOptions = AlertChannelCreateOptions

type EmailChannelContent struct {
EmailAddresses []string `json:"email_addresses"`
}

// ChannelContent represents the content block for an AlertChannel, which varies by channel type.
type ChannelContent struct {
Email *EmailChannelContent `json:"email,omitempty"`
// Other channel types like 'webhook', 'slack' could be added here as optional fields.
}

// ListAlertChannels gets a paginated list of Alert Channels.
func (c *Client) ListAlertChannels(ctx context.Context, opts *ListOptions) ([]AlertChannel, error) {
endpoint := formatAPIV4BetaPath("monitor/alert-channels")
return getPaginatedResults[AlertChannel](ctx, c, endpoint, opts)
}

// GetAlertChannel gets an Alert Channel by ID.
func (c *Client) GetAlertChannel(ctx context.Context, channelID int) (*AlertChannel, error) {
e := formatAPIV4BetaPath("monitor/alert-channels/%d", channelID)
return doGETRequest[AlertChannel](ctx, c, e)
}
2 changes: 0 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,4 @@ require (

go 1.24.0

toolchain go1.25.1

retract v1.0.0 // Accidental branch push
218 changes: 218 additions & 0 deletions monitor_alert_definitions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
package linodego

import (
"context"
"encoding/json"
"time"

"github.com/linode/linodego/internal/parseabletime"
)

// AlertDefinition represents an ACLP Alert Definition object
type AlertDefinition struct {
ID int `json:"id"`
Label string `json:"label"`
Severity int `json:"severity"`
Type string `json:"type"`
ServiceType string `json:"service_type"`
Status string `json:"status"`
HasMoreResources bool `json:"has_more_resources"`
Rule *Rule `json:"rule"`
RuleCriteria *RuleCriteria `json:"rule_criteria"`
TriggerConditions *TriggerConditions `json:"trigger_conditions"`
AlertChannels []AlertChannelEnvelope `json:"alert_channels"`
Created *time.Time `json:"-"`
Updated *time.Time `json:"-"`
UpdatedBy string `json:"updated_by"`
CreatedBy string `json:"created_by"`
EntityIDs []string `json:"entity_ids"`
Description string `json:"description"`
Class string `json:"class"`
}

// Backwards-compatible alias

// MonitorAlertDefinition represents an ACLP Alert Definition object
//
// Deprecated: AlertDefinition should be used in all new implementations.
type MonitorAlertDefinition = AlertDefinition

// TriggerConditions represents the trigger conditions for an alert.
type TriggerConditions struct {
CriteriaCondition string `json:"criteria_condition,omitempty"`
EvaluationPeriodSeconds int `json:"evaluation_period_seconds,omitempty"`
PollingIntervalSeconds int `json:"polling_interval_seconds,omitempty"`
TriggerOccurrences int `json:"trigger_occurrences,omitempty"`
}

// RuleCriteria represents the rule criteria for an alert.
type RuleCriteria struct {
Rules []Rule `json:"rules,omitempty"`
}

// Rule represents a single rule for an alert.
type Rule struct {
AggregateFunction string `json:"aggregate_function,omitempty"`
DimensionFilters []DimensionFilter `json:"dimension_filters,omitempty"`
Label string `json:"label,omitempty"`
Metric string `json:"metric,omitempty"`
Operator string `json:"operator,omitempty"`
Threshold *float64 `json:"threshold,omitempty"`
Unit *string `json:"unit,omitempty"`
}

// DimensionFilter represents a single dimension filter used inside a Rule.
type DimensionFilter struct {
DimensionLabel string `json:"dimension_label"`
Label string `json:"label"`
Operator string `json:"operator"`
Value any `json:"value"`
}

// AlertType represents the type of alert: "user" or "system"
type AlertType string

const (
AlertTypeUser AlertType = "user"
AlertTypeSystem AlertType = "system"
)

// Severity represents the severity level of an alert.
// 0 = Severe, 1 = Medium, 2 = Low, 3 = Info
type Severity int

const (
SeveritySevere Severity = 0
SeverityMedium Severity = 1
SeverityLow Severity = 2
SeverityInfo Severity = 3
)

// CriteriaCondition represents supported criteria conditions
type CriteriaCondition string

const (
CriteriaConditionAll CriteriaCondition = "ALL"
)

// AlertDefinitionCreateOptions are the options used to create a new alert definition.
type AlertDefinitionCreateOptions struct {
ServiceType string `json:"service_type"` // mandatory
Label string `json:"label"` // mandatory
Severity int `json:"severity"` // mandatory
ChannelIDs []int `json:"channel_ids"` // mandatory
RuleCriteria *RuleCriteria `json:"rule_criteria,omitempty"` // optional
TriggerConditions *TriggerConditions `json:"trigger_conditions,omitempty"` // optional
EntityIDs []string `json:"entity_ids,omitempty"` // optional
Description string `json:"description,omitempty"` // optional
}

// AlertDefinitionUpdateOptions are the options used to update an alert definition.
type AlertDefinitionUpdateOptions struct {
ServiceType string `json:"service_type"` // mandatory, must not be empty
AlertID int `json:"alert_id"` // mandatory, must not be zero
Label string `json:"label,omitempty"` // optional
Severity int `json:"severity,omitempty"` // optional, should be int to match AlertDefinition
Description string `json:"description,omitempty"` // optional
RuleCriteria *RuleCriteria `json:"rule_criteria,omitempty"` // optional
TriggerConditions *TriggerConditions `json:"trigger_conditions,omitempty"` // optional
EntityIDs []string `json:"entity_ids,omitempty"` // optional
ChannelIDs []int `json:"channel_ids,omitempty"` // optional
}

// UnmarshalJSON implements the json.Unmarshaler interface
func (i *AlertDefinition) UnmarshalJSON(b []byte) error {
type Mask AlertDefinition

p := struct {
*Mask

Created *parseabletime.ParseableTime `json:"created"`
Updated *parseabletime.ParseableTime `json:"updated"`
}{
Mask: (*Mask)(i),
}

if err := json.Unmarshal(b, &p); err != nil {
return err
}

i.Created = (*time.Time)(p.Created)
i.Updated = (*time.Time)(p.Updated)

return nil
}

// ListMonitorAlertDefinitions gets a paginated list of ACLP Monitor Alert Definitions.
func (c *Client) ListMonitorAlertDefinitions(ctx context.Context, serviceType string, opts *ListOptions) ([]AlertDefinition, error) {
var endpoint string
if serviceType != "" {
endpoint = formatAPIV4BetaPath("monitor/services/%s/alert-definitions", serviceType)
} else {
endpoint = formatAPIV4BetaPath("monitor/alert-definitions")
}

return getPaginatedResults[AlertDefinition](ctx, c, endpoint, opts)
}

// GetMonitorAlertDefinition gets an ACLP Monitor Alert Definition.
func (c *Client) GetMonitorAlertDefinition(ctx context.Context, serviceType string, alertID int) (*AlertDefinition, error) {
e := formatAPIV4BetaPath("monitor/services/%s/alert-definitions/%d", serviceType, alertID)
return doGETRequest[AlertDefinition](ctx, c, e)
}

// CreateMonitorAlertDefinition creates an ACLP Monitor Alert Definition.
func (c *Client) CreateMonitorAlertDefinition(ctx context.Context, serviceType string, opts AlertDefinitionCreateOptions) (*AlertDefinition, error) {
e := formatAPIV4BetaPath("monitor/services/%s/alert-definitions", serviceType)
return doPOSTRequest[AlertDefinition](ctx, c, e, opts)
}

// CreateMonitorAlertDefinitionWithIdempotency creates an ACLP Monitor Alert Definition
// and optionally sends an Idempotency-Key header to make the request idempotent.
func (c *Client) CreateMonitorAlertDefinitionWithIdempotency(
ctx context.Context,
serviceType string,
opts AlertDefinitionCreateOptions,
idempotencyKey string,
) (*AlertDefinition, error) {
e := formatAPIV4BetaPath("monitor/services/%s/alert-definitions", serviceType)

var result AlertDefinition

req := c.R(ctx).SetResult(&result)

if idempotencyKey != "" {
req.SetHeader("Idempotency-Key", idempotencyKey)
}

body, err := json.Marshal(opts)
if err != nil {
return nil, err
}

req.SetBody(string(body))

r, err := coupleAPIErrors(req.Post(e))
if err != nil {
return nil, err
}

return r.Result().(*AlertDefinition), nil
}

// UpdateMonitorAlertDefinition updates an ACLP Monitor Alert Definition.
func (c *Client) UpdateMonitorAlertDefinition(
ctx context.Context,
serviceType string,
alertID int,
opts AlertDefinitionUpdateOptions,
) (*AlertDefinition, error) {
e := formatAPIV4BetaPath("monitor/services/%s/alert-definitions/%d", serviceType, alertID)
return doPUTRequest[AlertDefinition](ctx, c, e, opts)
}

// DeleteMonitorAlertDefinition deletes an ACLP Monitor Alert Definition.
func (c *Client) DeleteMonitorAlertDefinition(ctx context.Context, serviceType string, alertID int) error {
e := formatAPIV4BetaPath("monitor/services/%s/alert-definitions/%d", serviceType, alertID)
return doDELETERequest(ctx, c, e)
}
2 changes: 1 addition & 1 deletion monitor_dashboards.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const (
ServiceTypeDBaaS ServiceType = "dbaas"
ServiceTypeACLB ServiceType = "aclb"
ServiceTypeNodeBalancer ServiceType = "nodebalancer"
ServiceTypeObjectStorage ServiceType = "objectstorage"
ServiceTypeObjectStorage ServiceType = "object_storage"
ServiceTypeVPC ServiceType = "vpc"
ServiceTypeFirewallService ServiceType = "firewall"
ServiceTypeNetLoadBalancer ServiceType = "netloadbalancer"
Expand Down
14 changes: 14 additions & 0 deletions request_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"net/url"
"reflect"
"strings"
)

// paginatedResponse represents a single response from a paginated
Expand Down Expand Up @@ -316,6 +317,19 @@ func formatAPIPath(format string, args ...any) string {
return fmt.Sprintf(format, escapedArgs...)
}

// formatAPIV4BetaPath builds a fully-qualified URL for v4beta endpoints.
// We return a full URL (including scheme and host) so requests made with the
// standard client (which is pointed at /v4) will hit the /v4beta host/path
// directly.
func formatAPIV4BetaPath(format string, args ...any) string {
p := formatAPIPath(format, args...)

// Ensure we don't produce a double slash when joining
p = strings.TrimPrefix(p, "/")

return fmt.Sprintf("%s://%s/%s/%s", APIProto, APIHost, "v4beta", p)
}
Comment on lines +324 to +331
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally we put the responsibility on users to configure v4beta using either LINODE_API_VERSION or the Client{}.SetVersion(...) / Client{}.UseURL(...) if they want to access beta endpoints. If a user wants to use just a couple of v4beta endpoints and run everything else under v4, we generally recommend that they create two separate clients.

The idea is that users will be required to acknowledge that they're using beta endpoints rather than it happening without them knowing. I'll take a note to document this a bit more explicitly πŸ‘


func isNil(i any) bool {
if i == nil {
return true
Expand Down
Loading
Loading