package main import ( "bytes" "flag" "fmt" "log" "net/http" "os" "sort" "strings" "sync" "time" "html/template" "github.com/localhots/koff" ) type clusterState struct { consumerOffsets map[string]koff.OffsetMessage consumerGroups map[string]koff.GroupMessage } var state = &clusterState{ consumerOffsets: make(map[string]koff.OffsetMessage), consumerGroups: make(map[string]koff.GroupMessage), } var lock sync.Mutex func main() { brokers := flag.String("brokers", "", "Comma separated list of brokers") flag.Parse() if *brokers == "" { fmt.Println("Brokers list required") flag.Usage() os.Exit(1) } c, err := koff.NewConsumer(strings.Split(*brokers, ","), false) if err != nil { log.Fatalf("Failed to create consumer: %v", err) } defer c.Close() go func() { for msg := range c.Messages() { state.add(msg) } }() http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write(state.render()) }) http.ListenAndServe(":8080", nil) } func (s *clusterState) add(msg koff.Message) { lock.Lock() defer lock.Unlock() if msg.OffsetMessage != nil { cur, ok := s.consumerOffsets[msg.Consumer] if !ok || cur.CommittedAt.Before(msg.OffsetMessage.CommittedAt) { s.consumerOffsets[msg.Consumer] = *msg.OffsetMessage } } else { cur, ok := s.consumerGroups[msg.Consumer] if !ok || (msg.GroupMessage.GenerationID > cur.GenerationID && msg.GroupMessage.Complete()) { s.consumerGroups[msg.Consumer] = *msg.GroupMessage } } } // // Render // var htmlTpl = template.Must(template.New("main").Parse(` Kafka Consumers

Consumer Offsets

{{range .ConsumerOffsets}} {{end}}
Consumer Topic Partition Offset Timestamp
{{.Consumer}} {{.Topic}} {{.Partition}} {{.Offset}} {{.Timestamp}}

Consumer Groups

{{range .ConsumerGroups}} {{range .Members}} {{$id := .ID}} {{range .Assignment}} {{end}} {{end}}
{{.Consumer}}
Consumer Topic Partition Leader
{{$id}} {{.Topic}} {{.Partition}} Yes

{{end}} `)) type offsetMessage struct { Consumer string Timestamp string koff.OffsetMessage } type groupMessage struct { Consumer string koff.GroupMessage } func (s *clusterState) render() []byte { var tpl struct { ConsumerOffsets []offsetMessage ConsumerGroups []groupMessage } lock.Lock() defer lock.Unlock() for k, m := range s.consumerOffsets { if strings.HasPrefix(k, "console-consumer") { continue } tpl.ConsumerOffsets = append(tpl.ConsumerOffsets, offsetMessage{ Consumer: k, OffsetMessage: m, Timestamp: m.CommittedAt.Format(time.Stamp), }) } sort.Slice(tpl.ConsumerOffsets, func(i, j int) bool { return tpl.ConsumerOffsets[i].Consumer < tpl.ConsumerOffsets[j].Consumer }) for k, m := range s.consumerGroups { // if strings.HasPrefix(k, "console-consumer") { // continue // } tpl.ConsumerGroups = append(tpl.ConsumerGroups, groupMessage{ Consumer: k, GroupMessage: m, }) } sort.Slice(tpl.ConsumerGroups, func(i, j int) bool { return tpl.ConsumerGroups[i].Consumer < tpl.ConsumerGroups[j].Consumer }) var buf bytes.Buffer err := htmlTpl.Execute(&buf, tpl) if err != nil { panic(err) } return buf.Bytes() }