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
133 changes: 133 additions & 0 deletions config/endpoint/event.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,40 @@
package endpoint

import (
"strings"
"time"
)

// FailureReason represents a failure with its timestamp
type FailureReason struct {
// Description is the error message or failed condition
Description string `json:"description"`

// Timestamp is when this failure was first observed
Timestamp time.Time `json:"timestamp"`
}

// Event is something that happens at a specific time
type Event struct {
// Type is the kind of event
Type EventType `json:"type"`

// Timestamp is the moment at which the event happened
Timestamp time.Time `json:"timestamp"`

// Errors is a list of errors that occurred during the health check (for UNHEALTHY events)
// Deprecated: Use ErrorReasons for new implementations
Errors []string `json:"errors,omitempty"`

// FailedConditions is a list of condition expressions that failed (for UNHEALTHY events)
// Deprecated: Use FailedConditionReasons for new implementations
FailedConditions []string `json:"failedConditions,omitempty"`

// ErrorReasons is a list of errors with their timestamps (for UNHEALTHY events)
ErrorReasons []FailureReason `json:"errorReasons,omitempty"`

// FailedConditionReasons is a list of failed conditions with their timestamps (for UNHEALTHY events)
FailedConditionReasons []FailureReason `json:"failedConditionReasons,omitempty"`
}

// EventType is, uh, the types of events?
Expand All @@ -34,6 +58,115 @@ func NewEventFromResult(result *Result) *Event {
event.Type = EventHealthy
} else {
event.Type = EventUnhealthy
// Capture error messages with timestamps
if len(result.Errors) > 0 {
event.ErrorReasons = make([]FailureReason, len(result.Errors))
for i, err := range result.Errors {
event.ErrorReasons[i] = FailureReason{
Description: err,
Timestamp: result.Timestamp,
}
}
// Also populate deprecated field for backwards compatibility
event.Errors = make([]string, len(result.Errors))
copy(event.Errors, result.Errors)
}
// Capture failed conditions with timestamps
if len(result.ConditionResults) > 0 {
event.FailedConditionReasons = make([]FailureReason, 0)
event.FailedConditions = make([]string, 0)
for _, conditionResult := range result.ConditionResults {
if !conditionResult.Success {
event.FailedConditionReasons = append(event.FailedConditionReasons, FailureReason{
Description: conditionResult.Condition,
Timestamp: result.Timestamp,
})
// Also populate deprecated field for backwards compatibility
event.FailedConditions = append(event.FailedConditions, conditionResult.Condition)
}
}
}
}
return event
}

// extractConditionPattern extracts the base condition pattern without the actual value
// For example: "[RESPONSE_TIME] (2280) < 500" becomes "[RESPONSE_TIME] < 500"
func extractConditionPattern(condition string) string {
// Look for pattern like "[SOMETHING] (actual_value) operator expected_value"
// We want to remove the (actual_value) part
var result []rune
inParens := false
for _, char := range condition {
if char == '(' {
inParens = true
continue
}
if char == ')' {
inParens = false
continue
}
if !inParens {
result = append(result, char)
}
}
// Clean up extra spaces
pattern := string(result)
for strings.Contains(pattern, " ") {
pattern = strings.ReplaceAll(pattern, " ", " ")
}
return strings.TrimSpace(pattern)
}

// AddUniqueFailuresFromResult adds new unique errors and failed conditions from a result to an existing UNHEALTHY event
func (e *Event) AddUniqueFailuresFromResult(result *Result) {
if e.Type != EventUnhealthy {
return
}

// Add unique errors with timestamps
if len(result.Errors) > 0 {
for _, newError := range result.Errors {
found := false
for _, existingErrorReason := range e.ErrorReasons {
if existingErrorReason.Description == newError {
found = true
break
}
}
if !found {
e.ErrorReasons = append(e.ErrorReasons, FailureReason{
Description: newError,
Timestamp: result.Timestamp,
})
// Also update deprecated field for backwards compatibility
e.Errors = append(e.Errors, newError)
}
}
}

// Add unique failed conditions with timestamps (deduplicate by pattern, not exact match)
if len(result.ConditionResults) > 0 {
for _, conditionResult := range result.ConditionResults {
if !conditionResult.Success {
newPattern := extractConditionPattern(conditionResult.Condition)
found := false
for _, existingConditionReason := range e.FailedConditionReasons {
existingPattern := extractConditionPattern(existingConditionReason.Description)
if existingPattern == newPattern {
found = true
break
}
}
if !found {
e.FailedConditionReasons = append(e.FailedConditionReasons, FailureReason{
Description: conditionResult.Condition,
Timestamp: result.Timestamp,
})
// Also update deprecated field for backwards compatibility
e.FailedConditions = append(e.FailedConditions, conditionResult.Condition)
}
}
}
}
}
6 changes: 6 additions & 0 deletions storage/store/memory/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ func AddResult(ss *endpoint.Status, result *endpoint.Result, maximumNumberOfResu
// length of MaximumNumberOfEvents by using ss.Events[len(ss.Events)-MaximumNumberOfEvents:] instead
ss.Events = ss.Events[len(ss.Events)-maximumNumberOfEvents:]
}
} else if !result.Success && len(ss.Events) > 0 {
// If the endpoint is still unhealthy, update the latest UNHEALTHY event with any new unique failures
latestEvent := ss.Events[len(ss.Events)-1]
if latestEvent.Type == endpoint.EventUnhealthy {
latestEvent.AddUniqueFailuresFromResult(result)
}
}
} else {
// This is the first result, so we need to add the first healthy/unhealthy event
Expand Down
33 changes: 32 additions & 1 deletion web/app/src/views/EndpointDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,38 @@
</div>
<div class="flex-1">
<p class="font-medium">{{ event.fancyText }}</p>
<p class="text-sm text-muted-foreground">{{ prettifyTimestamp(event.timestamp) }} • {{ event.fancyTimeAgo }}</p>
<div v-if="event.type === 'UNHEALTHY' && (event.errorReasons?.length > 0 || event.failedConditionReasons?.length > 0 || event.errors?.length > 0 || event.failedConditions?.length > 0)" class="mt-1 ml-1 text-sm text-muted-foreground space-y-0.5">
<!-- New format with timestamps -->
<div v-for="errorReason in event.errorReasons" :key="errorReason.description + errorReason.timestamp" class="flex items-start gap-1.5">
<span class="text-red-500 mt-0.5">•</span>
<div class="flex-1">
<span class="text-red-500">{{ errorReason.description }}</span>
<span class="text-xs text-muted-foreground ml-2">{{ generatePrettyTimeAgo(errorReason.timestamp) }}</span>
</div>
</div>
<div v-for="conditionReason in event.failedConditionReasons" :key="conditionReason.description + conditionReason.timestamp" class="flex items-start gap-1.5">
<span class="text-red-500 mt-0.5">•</span>
<div class="flex-1">
<span class="text-red-500 font-mono text-xs">{{ conditionReason.description }}</span>
<span class="text-xs text-muted-foreground ml-2">{{ generatePrettyTimeAgo(conditionReason.timestamp) }}</span>
</div>
</div>

<!-- Fallback to old format for backwards compatibility -->
<template v-if="!event.errorReasons && event.errors">
<div v-for="error in event.errors" :key="error" class="flex items-center gap-1.5">
<span class="text-red-500">•</span>
<span class="text-red-500">{{ error }}</span>
</div>
</template>
<template v-if="!event.failedConditionReasons && event.failedConditions">
<div v-for="condition in event.failedConditions" :key="condition" class="flex items-center gap-1.5">
<span class="text-red-500">•</span>
<span class="text-red-500 font-mono text-xs">{{ condition }}</span>
</div>
</template>
</div>
<p class="text-sm text-muted-foreground" :class="{ 'mt-1': event.type === 'UNHEALTHY' && (event.errors?.length > 0 || event.failedConditions?.length > 0) }">{{ prettifyTimestamp(event.timestamp) }} • {{ event.fancyTimeAgo }}</p>
</div>
</div>
</div>
Expand Down