Ref #556: adjust alert history range handling
This commit is contained in:
parent
32421b36b8
commit
5927535110
3 changed files with 218 additions and 25 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { createSignal, Show, For, createMemo, createEffect, onMount } from 'solid-js';
|
import { createSignal, Show, For, createMemo, createEffect, onMount, onCleanup } from 'solid-js';
|
||||||
import type { JSX } from 'solid-js';
|
import type { JSX } from 'solid-js';
|
||||||
import { EmailProviderSelect } from '@/components/Alerts/EmailProviderSelect';
|
import { EmailProviderSelect } from '@/components/Alerts/EmailProviderSelect';
|
||||||
import { WebhookConfig } from '@/components/Alerts/WebhookConfig';
|
import { WebhookConfig } from '@/components/Alerts/WebhookConfig';
|
||||||
|
|
@ -2936,6 +2936,54 @@ function HistoryTab() {
|
||||||
(typeof navigator !== 'undefined' ? navigator.language : undefined) ||
|
(typeof navigator !== 'undefined' ? navigator.language : undefined) ||
|
||||||
'en-US';
|
'en-US';
|
||||||
|
|
||||||
|
const buildHistoryParams = (range: string) => {
|
||||||
|
const params: { limit?: number; startTime?: string } = {};
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
switch (range) {
|
||||||
|
case '24h':
|
||||||
|
params.limit = 2000;
|
||||||
|
params.startTime = new Date(now - 24 * MS_PER_HOUR).toISOString();
|
||||||
|
break;
|
||||||
|
case '7d':
|
||||||
|
params.limit = 10000;
|
||||||
|
params.startTime = new Date(now - 7 * 24 * MS_PER_HOUR).toISOString();
|
||||||
|
break;
|
||||||
|
case '30d':
|
||||||
|
params.limit = 10000;
|
||||||
|
params.startTime = new Date(now - 30 * 24 * MS_PER_HOUR).toISOString();
|
||||||
|
break;
|
||||||
|
case 'all':
|
||||||
|
params.limit = 0;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
params.limit = 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
};
|
||||||
|
|
||||||
|
let fetchRequestId = 0;
|
||||||
|
const fetchAlertHistory = async (range: string) => {
|
||||||
|
const requestId = ++fetchRequestId;
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const history = await AlertsAPI.getHistory(buildHistoryParams(range));
|
||||||
|
if (requestId === fetchRequestId) {
|
||||||
|
setAlertHistory(history);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (requestId === fetchRequestId) {
|
||||||
|
console.error('Failed to load alert history:', err);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (requestId === fetchRequestId) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Ref for search input
|
// Ref for search input
|
||||||
let searchInputRef: HTMLInputElement | undefined;
|
let searchInputRef: HTMLInputElement | undefined;
|
||||||
|
|
||||||
|
|
@ -2968,15 +3016,8 @@ function HistoryTab() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load alert history on mount
|
// Load alert history on mount
|
||||||
onMount(async () => {
|
onMount(() => {
|
||||||
try {
|
fetchAlertHistory(timeFilter());
|
||||||
const history = await AlertsAPI.getHistory({ limit: 1000 });
|
|
||||||
setAlertHistory(history);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load alert history:', err);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add keyboard event listeners
|
// Add keyboard event listeners
|
||||||
const handleKeydown = (e: KeyboardEvent) => {
|
const handleKeydown = (e: KeyboardEvent) => {
|
||||||
|
|
@ -3004,10 +3045,21 @@ function HistoryTab() {
|
||||||
|
|
||||||
document.addEventListener('keydown', handleKeydown);
|
document.addEventListener('keydown', handleKeydown);
|
||||||
|
|
||||||
// Cleanup on unmount
|
onCleanup(() => {
|
||||||
return () => {
|
|
||||||
document.removeEventListener('keydown', handleKeydown);
|
document.removeEventListener('keydown', handleKeydown);
|
||||||
};
|
// Prevent pending requests from updating state after unmount
|
||||||
|
fetchRequestId++;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let skipInitialFetchEffect = true;
|
||||||
|
createEffect(() => {
|
||||||
|
const range = timeFilter();
|
||||||
|
if (skipInitialFetchEffect) {
|
||||||
|
skipInitialFetchEffect = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetchAlertHistory(range);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Format duration for display
|
// Format duration for display
|
||||||
|
|
|
||||||
|
|
@ -3540,6 +3540,15 @@ func (m *Manager) GetAlertHistory(limit int) []Alert {
|
||||||
return m.historyManager.GetAllHistory(limit)
|
return m.historyManager.GetAllHistory(limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAlertHistorySince returns alert history entries created after the provided time.
|
||||||
|
func (m *Manager) GetAlertHistorySince(since time.Time, limit int) []Alert {
|
||||||
|
if since.IsZero() {
|
||||||
|
return m.GetAlertHistory(limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.historyManager.GetHistory(since, limit)
|
||||||
|
}
|
||||||
|
|
||||||
// ClearAlertHistory clears all alert history
|
// ClearAlertHistory clears all alert history
|
||||||
func (m *Manager) ClearAlertHistory() error {
|
func (m *Manager) ClearAlertHistory() error {
|
||||||
return m.historyManager.ClearAllHistory()
|
return m.historyManager.ClearAllHistory()
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,11 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
|
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
|
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/utils"
|
"github.com/rcourtman/pulse-go-rewrite/internal/utils"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
|
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
|
||||||
|
|
@ -112,35 +114,165 @@ func (h *AlertHandlers) GetActiveAlerts(w http.ResponseWriter, r *http.Request)
|
||||||
|
|
||||||
// GetAlertHistory returns alert history
|
// GetAlertHistory returns alert history
|
||||||
func (h *AlertHandlers) GetAlertHistory(w http.ResponseWriter, r *http.Request) {
|
func (h *AlertHandlers) GetAlertHistory(w http.ResponseWriter, r *http.Request) {
|
||||||
|
query := r.URL.Query()
|
||||||
|
|
||||||
limit := 100
|
limit := 100
|
||||||
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
|
if limitStr := query.Get("limit"); limitStr != "" {
|
||||||
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 10000 {
|
l, err := strconv.Atoi(limitStr)
|
||||||
limit = l
|
switch {
|
||||||
} else if err != nil {
|
case err != nil:
|
||||||
log.Warn().Str("limit", limitStr).Msg("Invalid limit parameter, using default")
|
log.Warn().Str("limit", limitStr).Msg("Invalid limit parameter, using default")
|
||||||
} else {
|
case l < 0:
|
||||||
|
http.Error(w, "limit must be non-negative", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
case l == 0:
|
||||||
|
limit = 0
|
||||||
|
case l > 10000:
|
||||||
log.Warn().Int("limit", l).Msg("Limit exceeds maximum, capping at 10000")
|
log.Warn().Int("limit", l).Msg("Limit exceeds maximum, capping at 10000")
|
||||||
limit = 10000
|
limit = 10000
|
||||||
|
default:
|
||||||
|
limit = l
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
offset := 0
|
||||||
|
if offsetStr := query.Get("offset"); offsetStr != "" {
|
||||||
|
if o, err := strconv.Atoi(offsetStr); err == nil {
|
||||||
|
if o < 0 {
|
||||||
|
http.Error(w, "offset must be non-negative", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
offset = o
|
||||||
|
} else {
|
||||||
|
log.Warn().Str("offset", offsetStr).Msg("Invalid offset parameter, ignoring")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var startTime *time.Time
|
||||||
|
if startStr := query.Get("startTime"); startStr != "" {
|
||||||
|
parsed, err := time.Parse(time.RFC3339, startStr)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid startTime parameter", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startTime = &parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
var endTime *time.Time
|
||||||
|
if endStr := query.Get("endTime"); endStr != "" {
|
||||||
|
parsed, err := time.Parse(time.RFC3339, endStr)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid endTime parameter", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
endTime = &parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
if startTime != nil && endTime != nil && endTime.Before(*startTime) {
|
||||||
|
http.Error(w, "endTime must be after startTime", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
severity := strings.ToLower(strings.TrimSpace(query.Get("severity")))
|
||||||
|
switch severity {
|
||||||
|
case "", "all":
|
||||||
|
severity = ""
|
||||||
|
case "warning", "critical":
|
||||||
|
default:
|
||||||
|
log.Warn().Str("severity", severity).Msg("Invalid severity filter, ignoring")
|
||||||
|
severity = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceID := strings.TrimSpace(query.Get("resourceId"))
|
||||||
|
|
||||||
|
matchesFilters := func(alertTime time.Time, alertLevel string, alertResourceID string) bool {
|
||||||
|
if startTime != nil && alertTime.Before(*startTime) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if endTime != nil && alertTime.After(*endTime) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if severity != "" && !strings.EqualFold(alertLevel, severity) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if resourceID != "" && alertResourceID != resourceID {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
trimAlerts := func(alerts []alerts.Alert) []alerts.Alert {
|
||||||
|
if offset > 0 {
|
||||||
|
if offset >= len(alerts) {
|
||||||
|
return alerts[:0]
|
||||||
|
}
|
||||||
|
alerts = alerts[offset:]
|
||||||
|
}
|
||||||
|
if limit > 0 && len(alerts) > limit {
|
||||||
|
alerts = alerts[:limit]
|
||||||
|
}
|
||||||
|
return alerts
|
||||||
|
}
|
||||||
|
|
||||||
|
trimMockAlerts := func(alerts []models.Alert) []models.Alert {
|
||||||
|
if offset > 0 {
|
||||||
|
if offset >= len(alerts) {
|
||||||
|
return alerts[:0]
|
||||||
|
}
|
||||||
|
alerts = alerts[offset:]
|
||||||
|
}
|
||||||
|
if limit > 0 && len(alerts) > limit {
|
||||||
|
alerts = alerts[:limit]
|
||||||
|
}
|
||||||
|
return alerts
|
||||||
|
}
|
||||||
|
|
||||||
// Check if mock mode is enabled
|
// Check if mock mode is enabled
|
||||||
mockEnabled := mock.IsMockEnabled()
|
mockEnabled := mock.IsMockEnabled()
|
||||||
log.Info().Bool("mockEnabled", mockEnabled).Msg("GetAlertHistory: checking mock mode")
|
log.Debug().Bool("mockEnabled", mockEnabled).Msg("GetAlertHistory: mock mode status")
|
||||||
|
|
||||||
|
fetchLimit := limit
|
||||||
|
if fetchLimit > 0 && offset > 0 {
|
||||||
|
fetchLimit += offset
|
||||||
|
}
|
||||||
|
|
||||||
if mockEnabled {
|
if mockEnabled {
|
||||||
history := mock.GetMockAlertHistory(limit)
|
mockHistory := mock.GetMockAlertHistory(fetchLimit)
|
||||||
log.Info().Int("mockHistoryCount", len(history)).Msg("Returning mock alert history")
|
filtered := make([]models.Alert, 0, len(mockHistory))
|
||||||
if err := utils.WriteJSONResponse(w, history); err != nil {
|
for _, alert := range mockHistory {
|
||||||
|
if matchesFilters(alert.StartTime, alert.Level, alert.ResourceID) {
|
||||||
|
filtered = append(filtered, alert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filtered = trimMockAlerts(filtered)
|
||||||
|
if err := utils.WriteJSONResponse(w, filtered); err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to write mock alert history response")
|
log.Error().Err(err).Msg("Failed to write mock alert history response")
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get real alert history
|
if h.monitor == nil {
|
||||||
alertHistory := h.monitor.GetAlertManager().GetAlertHistory(limit)
|
http.Error(w, "monitor is not initialized", http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := utils.WriteJSONResponse(w, alertHistory); err != nil {
|
manager := h.monitor.GetAlertManager()
|
||||||
|
var history []alerts.Alert
|
||||||
|
if startTime != nil {
|
||||||
|
history = manager.GetAlertHistorySince(*startTime, fetchLimit)
|
||||||
|
} else {
|
||||||
|
history = manager.GetAlertHistory(fetchLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := make([]alerts.Alert, 0, len(history))
|
||||||
|
for _, alert := range history {
|
||||||
|
if matchesFilters(alert.StartTime, string(alert.Level), alert.ResourceID) {
|
||||||
|
filtered = append(filtered, alert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filtered = trimAlerts(filtered)
|
||||||
|
|
||||||
|
if err := utils.WriteJSONResponse(w, filtered); err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to write alert history response")
|
log.Error().Err(err).Msg("Failed to write alert history response")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue