package main import ( "crypto/md5" "fmt" "log" "os" "os/exec" "strings" "sync" "time" ) type ContainerInfo struct { ID string Name string Interval time.Duration Ignores []string } var notifiedChanges = struct { sync.RWMutex changes map[string]string }{changes: make(map[string]string)} func main() { // Check if GLITCHTIP_DSN environment variable is set glitchtipDSN := os.Getenv("GLITCHTIP_DSN") if glitchtipDSN == "" { log.Fatal("GLITCHTIP_DSN environment variable is not set") } log.Println("Starting Oculus...") for { containers, err := getContainers() if err != nil { log.Printf("Error listing containers: %v", err) time.Sleep(1 * time.Minute) continue } for _, container := range containers { log.Printf("Monitoring container: %s (%s) every %s", container.Name, container.ID, container.Interval) go monitorContainer(container) } // Sleep for 1 minute before checking for new containers again time.Sleep(1 * time.Minute) } } func getContainers() ([]ContainerInfo, error) { log.Println("Fetching container list...") output, err := exec.Command("docker", "ps", "--format", "{{.ID}} {{.Label \"oculus.enable\"}} {{.Label \"oculus.interval\"}} {{.Label \"oculus.cname\"}} {{.Label \"oculus.ignores\"}}").Output() if err != nil { return nil, err } lines := strings.Split(string(output), "\n") var containers []ContainerInfo for _, line := range lines { if line == "" { continue } parts := strings.Fields(line) if len(parts) < 2 || parts[1] == "" { continue } id := parts[0] intervalStr := "300s" if len(parts) > 2 && parts[2] != "" { intervalStr = parts[2] } interval, err := time.ParseDuration(intervalStr) if err != nil { log.Printf("Invalid interval format for container %s: %v", id, err) continue } cname := id if len(parts) > 3 && parts[3] != "" { cname = parts[3] } ignores := []string{} if len(parts) > 4 && parts[4] != "" { ignores = strings.Split(parts[4], ",") } containers = append(containers, ContainerInfo{ ID: id, Name: cname, Interval: interval, Ignores: ignores, }) } log.Printf("Found %d containers to monitor.", len(containers)) return containers, nil } func monitorContainer(container ContainerInfo) { for { log.Printf("Checking diffs for container: %s (%s)", container.Name, container.ID) checkDiff(container) time.Sleep(container.Interval) } } func checkDiff(container ContainerInfo) { cmd := exec.Command("docker", "diff", container.ID) output, err := cmd.Output() if err != nil { log.Printf("Error running docker diff for container %s: %v", container.ID, err) return } diffOutput := string(output) for _, ignore := range container.Ignores { diffOutput = removeIgnoredPaths(diffOutput, ignore) } if diffOutput != "" { diffHash := fmt.Sprintf("%x", md5.Sum([]byte(diffOutput))) notifiedChanges.RLock() lastNotifiedHash, notified := notifiedChanges.changes[container.ID] notifiedChanges.RUnlock() if !notified || lastNotifiedHash != diffHash { log.Printf("Writing diff output for container: %s (%s)", container.Name, container.ID) // Write diff output to a file filename := fmt.Sprintf("%s.diff", container.Name) err = writeToFile(filename, diffOutput) if err != nil { log.Printf("Error writing diff to file for container %s: %v", container.ID, err) } // Send notification using go-glitch log.Printf("Sending notification for container: %s (%s)", container.Name, container.ID) cmd = exec.Command("go-glitch") cmd.Stdin = strings.NewReader(diffOutput) output, err = cmd.CombinedOutput() if err != nil { log.Printf("Error sending notification for container %s: %v", container.ID, err) log.Printf("go-glitch output: %s", output) } else { notifiedChanges.Lock() notifiedChanges.changes[container.ID] = diffHash notifiedChanges.Unlock() } } else { log.Printf("No new changes detected for container: %s (%s)", container.Name, container.ID) } } else { log.Printf("No significant changes detected for container: %s (%s)", container.Name, container.ID) } } func removeIgnoredPaths(diffOutput string, ignore string) string { lines := strings.Split(diffOutput, "\n") filteredLines := []string{} for _, line := range lines { if !strings.Contains(line, ignore) { filteredLines = append(filteredLines, line) } } return strings.Join(filteredLines, "\n") } func writeToFile(filename, content string) error { file, err := os.Create(filename) if err != nil { return err } defer file.Close() _, err = file.WriteString(content) return err }