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
Consumer |
Topic |
Partition |
Offset |
Timestamp |
{{range .ConsumerOffsets}}
{{.Consumer}} |
{{.Topic}} |
{{.Partition}} |
{{.Offset}} |
{{.Timestamp}} |
{{end}}
Consumer Groups
{{range .ConsumerGroups}}
{{.Consumer}} |
Consumer |
Topic |
Partition |
Leader |
{{range .Members}}
{{$id := .ID}}
{{range .Assignment}}
{{$id}} |
{{.Topic}} |
{{.Partition}} |
Yes |
{{end}}
{{end}}
{{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()
}