alerts.go•13.7 kB
package alerting
import (
"context"
"encoding/json"
"fmt"
"io"
"last9-mcp/internal/models"
"net/http"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// AlertRule represents an alert configuration from Last9 API
type AlertRule struct {
ID string `json:"id"`
OrganizationID string `json:"organization_id"`
EntityID string `json:"entity_id"`
PrimaryIndicator string `json:"primary_indicator"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
DeletedAt *int64 `json:"deleted_at"`
ErrorSince *int64 `json:"error_since"`
State string `json:"state"`
ExternalRef string `json:"external_ref"`
Severity string `json:"severity"`
Algorithm string `json:"algorithm"`
RuleName string `json:"rule_name"`
MuteUntil int64 `json:"mute_until"`
Properties map[string]interface{} `json:"properties"`
GroupTimeseriesNotifications bool `json:"group_timeseries_notifications"`
}
// Alert represents an active alert instance
type Alert struct {
ID string `json:"id"`
RuleID string `json:"rule_id"`
RuleName string `json:"rule_name"`
State string `json:"state"`
Severity string `json:"severity"`
StartsAt string `json:"starts_at"`
EndsAt string `json:"ends_at"`
Labels map[string]string `json:"labels"`
Annotations map[string]string `json:"annotations"`
GeneratorURL string `json:"generator_url"`
Fingerprint string `json:"fingerprint"`
}
// AlertConfigResponse represents the response from alert rules API (direct array)
type AlertConfigResponse []AlertRule
// AlertsResponse represents the response from alerts monitoring API
type AlertsResponse struct {
Timestamp int64 `json:"timestamp"`
Window int64 `json:"window"`
AlertRules []AlertRuleData `json:"alert_rules"`
}
type AlertRuleData struct {
AlertGroupID string `json:"alert_group_id"`
AlertGroupName string `json:"alert_group_name"`
RuleID string `json:"rule_id"`
RuleName string `json:"rule_name"`
State string `json:"state"`
Severity string `json:"severity"`
RuleType string `json:"rule_type"`
LastFiredAt int64 `json:"last_fired_at"`
Since int64 `json:"since"`
Alerts []AlertInstance `json:"alerts"`
RuleProperties map[string]interface{} `json:"rule_properties"`
}
type AlertInstance struct {
State string `json:"state"`
LabelHash string `json:"label_hash"`
Annotations map[string]interface{} `json:"annotations"`
GroupLabels map[string]interface{} `json:"group_labels"`
MetricDegradation float64 `json:"metric_degradation"`
CurrentValue float64 `json:"current_value"`
LastFiredAt int64 `json:"last_fired_at"`
Since int64 `json:"since"`
}
const GetAlertConfigDescription = `
Get alert configurations (alert rules) from Last9.
Returns all configured alert rules including their conditions, labels, and annotations.
Each alert rule includes:
- id: Unique identifier for the alert rule
- name: Human-readable name of the alert
- description: Detailed description of what the alert monitors
- state: Current state of the alert rule (active, inactive, etc.)
- severity: Alert severity level (critical, warning, info)
- query: PromQL query used for the alert condition
- for: Duration threshold before alert fires
- labels: Key-value pairs for alert routing and grouping
- annotations: Additional metadata and descriptions
- group_name: Alert group this rule belongs to
- condition: Alert condition configuration (thresholds, operators)
- created_at: When the alert rule was created
- updated_at: When the alert rule was last modified
`
const GetAlertsDescription = `
Get currently active alerts from Last9 monitoring system.
Returns all alerts that are currently firing or have fired recently within the specified time window.
Parameters:
- timestamp: Unix timestamp for the query time (defaults to current time)
- window: Time window in seconds to look back for alerts (defaults to 900 seconds = 15 minutes)
Each alert includes:
- id: Unique identifier for this alert instance
- rule_id: ID of the alert rule that triggered this alert
- rule_name: Name of the alert rule
- state: Current state (firing, resolved, pending)
- severity: Alert severity level
- starts_at: When this alert instance started firing
- ends_at: When this alert instance was resolved (if resolved)
- labels: Key-value pairs for alert identification and routing
- annotations: Additional context and descriptions
- generator_url: URL to the source of the alert
- fingerprint: Unique fingerprint for this alert instance
`
type GetAlertConfigArgs struct{}
func NewGetAlertConfigHandler(client *http.Client, cfg models.Config) func(context.Context, *mcp.CallToolRequest, GetAlertConfigArgs) (*mcp.CallToolResult, any, error) {
return func(ctx context.Context, req *mcp.CallToolRequest, args GetAlertConfigArgs) (*mcp.CallToolResult, any, error) {
// Build the URL for alert rules API
url := fmt.Sprintf("%s/alert-rules", cfg.APIBaseURL)
// Create HTTP request
httpReq, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, nil, fmt.Errorf("failed to create request: %w", err)
}
// Set headers
httpReq.Header.Set("Accept", "application/json")
httpReq.Header.Set("X-LAST9-API-TOKEN", "Bearer "+cfg.AccessToken)
// Make the request
resp, err := client.Do(httpReq)
if err != nil {
return nil, nil, fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()
// Check response status
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
}
// Read response body
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, nil, fmt.Errorf("failed to read response: %w", err)
}
// Parse JSON response
var alertConfig AlertConfigResponse
if err := json.Unmarshal(body, &alertConfig); err != nil {
return nil, nil, fmt.Errorf("failed to parse response: %w", err)
}
// Format the response
formattedResponse := fmt.Sprintf("Found %d alert rules:\n\n", len(alertConfig))
for i, rule := range alertConfig {
formattedResponse += fmt.Sprintf("Alert Rule %d:\n", i+1)
formattedResponse += fmt.Sprintf(" ID: %s\n", rule.ID)
formattedResponse += fmt.Sprintf(" Rule Name: %s\n", rule.RuleName)
formattedResponse += fmt.Sprintf(" Primary Indicator: %s\n", rule.PrimaryIndicator)
formattedResponse += fmt.Sprintf(" State: %s\n", rule.State)
formattedResponse += fmt.Sprintf(" Severity: %s\n", rule.Severity)
formattedResponse += fmt.Sprintf(" Algorithm: %s\n", rule.Algorithm)
formattedResponse += fmt.Sprintf(" Entity ID: %s\n", rule.EntityID)
if rule.ErrorSince != nil {
errorTime := time.Unix(*rule.ErrorSince, 0).UTC().Format("2006-01-02 15:04:05 UTC")
formattedResponse += fmt.Sprintf(" Error Since: %s\n", errorTime)
}
if rule.Properties != nil && len(rule.Properties) > 0 {
formattedResponse += " Properties:\n"
for k, v := range rule.Properties {
formattedResponse += fmt.Sprintf(" %s: %v\n", k, v)
}
}
createdTime := time.Unix(rule.CreatedAt, 0).UTC().Format("2006-01-02 15:04:05 UTC")
updatedTime := time.Unix(rule.UpdatedAt, 0).UTC().Format("2006-01-02 15:04:05 UTC")
formattedResponse += fmt.Sprintf(" Created: %s\n", createdTime)
formattedResponse += fmt.Sprintf(" Updated: %s\n", updatedTime)
formattedResponse += fmt.Sprintf(" Group Timeseries Notifications: %v\n", rule.GroupTimeseriesNotifications)
formattedResponse += "\n"
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: formattedResponse,
},
},
}, nil, nil
}
}
type GetAlertsArgs struct {
Timestamp float64 `json:"timestamp,omitempty" jsonschema:"Unix timestamp for query time (defaults to current time)"`
Window float64 `json:"window,omitempty" jsonschema:"Time window in seconds (default: 900, range: 60-86400)"`
}
func NewGetAlertsHandler(client *http.Client, cfg models.Config) func(context.Context, *mcp.CallToolRequest, GetAlertsArgs) (*mcp.CallToolResult, any, error) {
return func(ctx context.Context, req *mcp.CallToolRequest, args GetAlertsArgs) (*mcp.CallToolResult, any, error) {
// Parse timestamp parameter (defaults to current time)
timestamp := time.Now().Unix()
if args.Timestamp != 0 {
timestamp = int64(args.Timestamp)
}
// Parse window parameter (defaults to 900 seconds = 15 minutes)
window := int64(900)
if args.Window != 0 {
window = int64(args.Window)
}
// Build the URL for alerts monitoring API
url := fmt.Sprintf("%s/alerts/monitor?timestamp=%d&window=%d", cfg.APIBaseURL, timestamp, window)
// Create HTTP request
httpReq, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, nil, fmt.Errorf("failed to create request: %w", err)
}
// Set headers
httpReq.Header.Set("Accept", "application/json")
httpReq.Header.Set("X-LAST9-API-TOKEN", "Bearer "+cfg.AccessToken)
// Make the request
resp, err := client.Do(httpReq)
if err != nil {
return nil, nil, fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()
// Check response status
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
}
// Read response body
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, nil, fmt.Errorf("failed to read response: %w", err)
}
// Parse JSON response
var alertsResp AlertsResponse
if err := json.Unmarshal(body, &alertsResp); err != nil {
return nil, nil, fmt.Errorf("failed to parse response: %w", err)
}
// Format the response
timeStr := time.Unix(alertsResp.Timestamp, 0).UTC().Format("2006-01-02 15:04:05 UTC")
formattedResponse := fmt.Sprintf("Alerts for timestamp %s (window: %d seconds):\n", timeStr, alertsResp.Window)
totalAlertInstances := 0
for _, rule := range alertsResp.AlertRules {
totalAlertInstances += len(rule.Alerts)
}
formattedResponse += fmt.Sprintf("Found %d alert rule(s) with %d alert instance(s):\n\n", len(alertsResp.AlertRules), totalAlertInstances)
if len(alertsResp.AlertRules) == 0 {
formattedResponse += "No alerts found in the specified time window.\n"
} else {
for i, rule := range alertsResp.AlertRules {
formattedResponse += fmt.Sprintf("Alert Rule %d:\n", i+1)
formattedResponse += fmt.Sprintf(" Rule ID: %s\n", rule.RuleID)
formattedResponse += fmt.Sprintf(" Rule Name: %s\n", rule.RuleName)
formattedResponse += fmt.Sprintf(" Alert Group: %s\n", rule.AlertGroupName)
formattedResponse += fmt.Sprintf(" State: %s\n", rule.State)
formattedResponse += fmt.Sprintf(" Severity: %s\n", rule.Severity)
formattedResponse += fmt.Sprintf(" Rule Type: %s\n", rule.RuleType)
if rule.LastFiredAt > 0 {
lastFired := time.Unix(rule.LastFiredAt, 0).UTC().Format("2006-01-02 15:04:05 UTC")
formattedResponse += fmt.Sprintf(" Last Fired At: %s\n", lastFired)
}
if rule.Since > 0 {
since := time.Unix(rule.Since, 0).UTC().Format("2006-01-02 15:04:05 UTC")
formattedResponse += fmt.Sprintf(" Since: %s\n", since)
}
if len(rule.RuleProperties) > 0 {
formattedResponse += " Rule Properties:\n"
for k, v := range rule.RuleProperties {
formattedResponse += fmt.Sprintf(" %s: %v\n", k, v)
}
}
if len(rule.Alerts) > 0 {
formattedResponse += fmt.Sprintf(" Alert Instances (%d):\n", len(rule.Alerts))
for j, alert := range rule.Alerts {
formattedResponse += fmt.Sprintf(" Instance %d:\n", j+1)
formattedResponse += fmt.Sprintf(" State: %s\n", alert.State)
formattedResponse += fmt.Sprintf(" Current Value: %.4f\n", alert.CurrentValue)
formattedResponse += fmt.Sprintf(" Metric Degradation: %.4f\n", alert.MetricDegradation)
if len(alert.GroupLabels) > 0 {
formattedResponse += " Group Labels:\n"
for k, v := range alert.GroupLabels {
formattedResponse += fmt.Sprintf(" %s: %v\n", k, v)
}
}
if len(alert.Annotations) > 0 {
formattedResponse += " Annotations:\n"
for k, v := range alert.Annotations {
formattedResponse += fmt.Sprintf(" %s: %v\n", k, v)
}
}
}
}
formattedResponse += "\n"
}
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: formattedResponse,
},
},
}, nil, nil
}
}