aboutsummaryrefslogtreecommitdiff
path: root/internal/config/config.go
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-01-20 17:16:22 -0800
committerFuwn <[email protected]>2026-01-20 17:16:22 -0800
commit2371b28128213fbcc8d1c062dccc3074e6b0fa98 (patch)
tree84452dbf5f2b1821d1fc5cf8ecdb0a5ad2b74f56 /internal/config/config.go
parentfix: Use wildcard path for badge endpoint to support .svg extension (diff)
downloadkaze-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.go71
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