diff options
| author | Fuwn <[email protected]> | 2026-01-20 17:16:22 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-01-20 17:16:22 -0800 |
| commit | 2371b28128213fbcc8d1c062dccc3074e6b0fa98 (patch) | |
| tree | 84452dbf5f2b1821d1fc5cf8ecdb0a5ad2b74f56 /internal/config/config.go | |
| parent | fix: Use wildcard path for badge endpoint to support .svg extension (diff) | |
| download | kaze-2371b28128213fbcc8d1c062dccc3074e6b0fa98.tar.xz kaze-2371b28128213fbcc8d1c062dccc3074e6b0fa98.zip | |
feat: Use composite group/name key for monitor identification
Previously monitors were identified by just their name, causing monitors
with the same name in different groups to share data in the database.
Changes:
- Add ID() method to MonitorConfig returning 'group/name' format
- Add Group field to MonitorConfig (set at runtime)
- Update Monitor interface with ID() and Group() methods
- Update all monitor implementations (http, tcp, dns, icmp, gemini,
graphql, database) to use composite ID
- Update Scheduler to use monitor ID instead of name
- Update server handlers to use composite ID for stats lookups
- Change API routes to use {group}/{name} pattern:
- /api/monitor/{group}/{name}
- /api/history/{group}/{name}
- /api/uptime/{group}/{name}
- /api/badge/{group}/{name}.svg
- URL-encode group and name components to handle special characters
(e.g., slashes in names become %2F)
- Update config.UpdateResetFlag to accept group and name separately
BREAKING: API endpoints now require group in the path. Existing database
data using just monitor names won't be associated with the new composite
keys.
Diffstat (limited to 'internal/config/config.go')
| -rw-r--r-- | internal/config/config.go | 71 |
1 files changed, 67 insertions, 4 deletions
diff --git a/internal/config/config.go b/internal/config/config.go index 0c8b430..7542d3e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "net/url" "os" "strings" "time" @@ -117,6 +118,7 @@ type MonitorDefaults struct { // MonitorConfig represents a single monitor type MonitorConfig struct { Name string `yaml:"name"` + Group string `yaml:"-"` // Set at runtime, not from YAML - the group this monitor belongs to Type string `yaml:"type"` // http, https, tcp, gemini Target string `yaml:"target"` Link string `yaml:"link,omitempty"` // Custom URL for clicking the monitor name (e.g., docs page) @@ -151,6 +153,45 @@ type MonitorConfig struct { DBType string `yaml:"db_type,omitempty"` // Database type: postgres, mysql, redis, memcached, mongodb } +// ID returns the unique identifier for this monitor (group/name format) +// Both group and name are URL-encoded to handle special characters like '/' +func (m *MonitorConfig) ID() string { + if m.Group == "" { + return url.PathEscape(m.Name) + } + return url.PathEscape(m.Group) + "/" + url.PathEscape(m.Name) +} + +// ParseMonitorID splits a monitor ID back into group and name components +// Returns (group, name, ok) where ok is false if the ID format is invalid +func ParseMonitorID(id string) (group, name string, ok bool) { + // Find the separator (first unescaped '/') + idx := strings.Index(id, "/") + if idx == -1 { + // No group, just a name + decoded, err := url.PathUnescape(id) + if err != nil { + return "", "", false + } + return "", decoded, true + } + + groupPart := id[:idx] + namePart := id[idx+1:] + + decodedGroup, err := url.PathUnescape(groupPart) + if err != nil { + return "", "", false + } + + decodedName, err := url.PathUnescape(namePart) + if err != nil { + return "", "", false + } + + return decodedGroup, decodedName, true +} + // IncidentConfig represents an incident or maintenance type IncidentConfig struct { Title string `yaml:"title"` @@ -285,6 +326,9 @@ func (c *Config) applyDefaults() { for j := range c.Groups[i].Monitors { m := &c.Groups[i].Monitors[j] + // Set the group name on the monitor + m.Group = grp.Name + // Apply group-level defaults first, then monitor-level overrides if m.Interval.Duration == 0 { if grp.Defaults != nil && grp.Defaults.Interval != nil { @@ -534,7 +578,8 @@ type MonitorWithGroup struct { // UpdateResetFlag updates the reset_on_next_check flag for a specific monitor in the config file // Uses a line-based approach to preserve original formatting and omitted fields -func UpdateResetFlag(configPath string, monitorName string, value bool) error { +// groupName and monitorName are used to find the correct monitor in the YAML structure +func UpdateResetFlag(configPath string, groupName string, monitorName string, value bool) error { // Read the config file data, err := os.ReadFile(configPath) if err != nil { @@ -542,6 +587,7 @@ func UpdateResetFlag(configPath string, monitorName string, value bool) error { } lines := strings.Split(string(data), "\n") + inTargetGroup := false inMonitor := false monitorIndent := "" foundMonitor := false @@ -550,8 +596,25 @@ func UpdateResetFlag(configPath string, monitorName string, value bool) error { line := lines[i] trimmed := strings.TrimSpace(line) - // Check if this is the start of our target monitor - if strings.HasPrefix(trimmed, "- name:") || strings.HasPrefix(trimmed, "name:") { + // Check if this is a group name line + if strings.HasPrefix(trimmed, "- name:") && !inMonitor { + // Could be a group name - check if it matches our target group + namePart := strings.TrimPrefix(trimmed, "- name:") + namePart = strings.TrimSpace(namePart) + namePart = strings.Trim(namePart, "\"'") + + // Check if this is our target group + if namePart == groupName { + inTargetGroup = true + } else if inTargetGroup { + // We've moved to a different group, stop looking + break + } + continue + } + + // Check if this is the start of our target monitor (within the target group) + if inTargetGroup && (strings.HasPrefix(trimmed, "- name:") || strings.HasPrefix(trimmed, "name:")) { // Extract monitor name from line namePart := strings.TrimPrefix(trimmed, "- name:") namePart = strings.TrimPrefix(namePart, "name:") @@ -597,7 +660,7 @@ func UpdateResetFlag(configPath string, monitorName string, value bool) error { } if !foundMonitor { - return fmt.Errorf("monitor %q not found in config", monitorName) + return fmt.Errorf("monitor %q in group %q not found in config", monitorName, groupName) } // Write back to file |