Initial commit
This commit is contained in:
commit
16e8e9ec19
|
@ -0,0 +1,18 @@
|
||||||
|
Copyright 2018 Gregory Eremin
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,37 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/localhots/koff"
|
||||||
|
"github.com/localhots/pretty"
|
||||||
|
)
|
||||||
|
|
||||||
|
const topicName = "__consumer_offsets"
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
for msg := range c.Messages() {
|
||||||
|
if msg.GroupMessage != nil {
|
||||||
|
pretty.Println("MESSAGE", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,156 @@
|
||||||
|
package koff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Shopify/sarama"
|
||||||
|
"github.com/localhots/gobelt/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const topicName = "__consumer_offsets"
|
||||||
|
|
||||||
|
// Consumer reads messages from __consumer_offsets topic and decodes them into
|
||||||
|
// OffsetMessages.
|
||||||
|
type Consumer struct {
|
||||||
|
client sarama.Client
|
||||||
|
cons sarama.Consumer
|
||||||
|
pcs map[int32]sarama.PartitionConsumer
|
||||||
|
msgs chan Message
|
||||||
|
watch bool
|
||||||
|
|
||||||
|
lock sync.Mutex
|
||||||
|
wg sync.WaitGroup
|
||||||
|
pticker *time.Ticker
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConsumer creates a new Kafka offsets topic consumer.
|
||||||
|
func NewConsumer(brokers []string, watch bool) (*Consumer, error) {
|
||||||
|
log.Info(context.TODO(), "Creating client")
|
||||||
|
client, err := sarama.NewClient(brokers, sarama.NewConfig())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info(context.TODO(), "Creating consumer")
|
||||||
|
sc, err := sarama.NewConsumerFromClient(client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c := &Consumer{
|
||||||
|
client: client,
|
||||||
|
cons: sc,
|
||||||
|
pcs: make(map[int32]sarama.PartitionConsumer),
|
||||||
|
msgs: make(chan Message),
|
||||||
|
watch: watch,
|
||||||
|
}
|
||||||
|
log.Info(context.TODO(), "Setting up partition consumers")
|
||||||
|
if err := c.setupPartitionConsumers(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if watch {
|
||||||
|
c.wg.Add(1)
|
||||||
|
go c.keepPartitionConsumersUpdated(30 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Messages returns a read only channel of offset messages.
|
||||||
|
func (c *Consumer) Messages() <-chan Message {
|
||||||
|
return c.msgs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close shuts down the consumer.
|
||||||
|
func (c *Consumer) Close() error {
|
||||||
|
c.pticker.Stop()
|
||||||
|
for _, pc := range c.pcs {
|
||||||
|
if err := pc.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := c.cons.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.wg.Wait()
|
||||||
|
close(c.msgs)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) keepPartitionConsumersUpdated(interval time.Duration) {
|
||||||
|
c.pticker = time.NewTicker(interval)
|
||||||
|
for range c.pticker.C {
|
||||||
|
c.setupPartitionConsumers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) setupPartitionConsumers() error {
|
||||||
|
log.Info(context.TODO(), "Fetching partition list")
|
||||||
|
partitions, err := c.cons.Partitions(topicName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, partition := range partitions {
|
||||||
|
if _, ok := c.pcs[partition]; !ok {
|
||||||
|
c.wg.Add(1)
|
||||||
|
go c.consumePartition(partition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pmap := make(map[int32]struct{}, len(partitions))
|
||||||
|
for _, partition := range partitions {
|
||||||
|
pmap[partition] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for partition, pc := range c.pcs {
|
||||||
|
if _, ok := pmap[partition]; !ok {
|
||||||
|
err := pc.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.lock.Lock()
|
||||||
|
delete(c.pcs, partition)
|
||||||
|
c.lock.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) consumePartition(partition int32) error {
|
||||||
|
defer c.wg.Done()
|
||||||
|
|
||||||
|
pc, err := c.cons.ConsumePartition(topicName, partition, sarama.OffsetOldest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Info(context.TODO(), "Started partition consumer", log.F{"p": partition})
|
||||||
|
|
||||||
|
c.lock.Lock()
|
||||||
|
c.pcs[partition] = pc
|
||||||
|
c.lock.Unlock()
|
||||||
|
|
||||||
|
var maxOffset *int64
|
||||||
|
if c.watch {
|
||||||
|
off, err := c.client.GetOffset(topicName, partition, sarama.OffsetNewest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
maxOffset = &off
|
||||||
|
}
|
||||||
|
|
||||||
|
for msg := range pc.Messages() {
|
||||||
|
if msg.Value == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c.msgs <- Decode(msg.Key, msg.Value)
|
||||||
|
if maxOffset != nil && msg.Offset == *maxOffset {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,218 @@
|
||||||
|
package koff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"runtime/debug"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Message is the main structure that wraps a consumer offsets topic message.
|
||||||
|
type Message struct {
|
||||||
|
Consumer string
|
||||||
|
OffsetMessage *OffsetMessage
|
||||||
|
GroupMessage *GroupMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// OffsetMessage is a kind of message that carries individual consumer offset.
|
||||||
|
type OffsetMessage struct {
|
||||||
|
Topic string
|
||||||
|
Partition int32
|
||||||
|
Offset int64
|
||||||
|
Metadata string
|
||||||
|
CommittedAt time.Time
|
||||||
|
ExpiresAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupMessage contains consumer group metadata.
|
||||||
|
type GroupMessage struct {
|
||||||
|
ProtocolType string
|
||||||
|
GenerationID int32
|
||||||
|
LeaderID string
|
||||||
|
Protocol string
|
||||||
|
Members []GroupMember
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupMember contains metadata for a consumer group member.
|
||||||
|
type GroupMember struct {
|
||||||
|
ID string
|
||||||
|
ClientID string
|
||||||
|
ClientHost string
|
||||||
|
SessionTimeout int32
|
||||||
|
RebalanceTimeout int32
|
||||||
|
Subscription []TopicAndPartition
|
||||||
|
Assignment []TopicAndPartition
|
||||||
|
}
|
||||||
|
|
||||||
|
// TopicAndPartition is a tuple of topic and partition.
|
||||||
|
type TopicAndPartition struct {
|
||||||
|
Topic string
|
||||||
|
Partition int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode decodes message key and value into an OffsetMessage.
|
||||||
|
func Decode(key, val []byte) Message {
|
||||||
|
var m Message
|
||||||
|
m.decodeKey(key)
|
||||||
|
if m.OffsetMessage != nil {
|
||||||
|
m.decodeOffsetMessage(val)
|
||||||
|
} else {
|
||||||
|
m.decodeGroupMetadata(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key structure:
|
||||||
|
// [2] Version, uint16 big endian
|
||||||
|
// [2] Consumer name length, uint16 big endian
|
||||||
|
// [^] Consumer name
|
||||||
|
// if Version is 0 or 1 {
|
||||||
|
// [2] Topic length, uint16 big endian
|
||||||
|
// [^] Topic
|
||||||
|
// [4] Partition, uint32 big endian
|
||||||
|
// }
|
||||||
|
func (m *Message) decodeKey(key []byte) {
|
||||||
|
buf := &buffer{data: key}
|
||||||
|
version := buf.readInt16()
|
||||||
|
m.Consumer = buf.readString()
|
||||||
|
if version < 2 {
|
||||||
|
m.OffsetMessage = &OffsetMessage{
|
||||||
|
Topic: buf.readString(),
|
||||||
|
Partition: buf.readInt32(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value structure:
|
||||||
|
// [2] Version, uint16 big endian
|
||||||
|
// [8] Offset, uint32 big endian
|
||||||
|
// [2] Meta length, uint16 big endian
|
||||||
|
// [^] Meta
|
||||||
|
// [8] Commit time, unix timestamp with millisecond precision
|
||||||
|
// if Version is 1 {
|
||||||
|
// [8] Expire time, unix timestamp with millisecond precision
|
||||||
|
// }
|
||||||
|
func (m *Message) decodeOffsetMessage(val []byte) {
|
||||||
|
buf := &buffer{data: val}
|
||||||
|
om := m.OffsetMessage
|
||||||
|
version := buf.readInt16()
|
||||||
|
om.Offset = buf.readInt64()
|
||||||
|
om.Metadata = buf.readString()
|
||||||
|
om.CommittedAt = makeTime(buf.readInt64())
|
||||||
|
if version == 1 {
|
||||||
|
om.ExpiresAt = makeTime(buf.readInt64())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Message) decodeGroupMetadata(val []byte) {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
debug.PrintStack()
|
||||||
|
fmt.Println(val, m)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
buf := &buffer{data: val}
|
||||||
|
m.GroupMessage = &GroupMessage{Members: []GroupMember{}}
|
||||||
|
gm := m.GroupMessage
|
||||||
|
version := buf.readInt16()
|
||||||
|
if version > 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gm.ProtocolType = buf.readString()
|
||||||
|
gm.GenerationID = buf.readInt32()
|
||||||
|
if gm.GenerationID > 1 {
|
||||||
|
next := buf.readInt32()
|
||||||
|
if next == -1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf.pos -= 4
|
||||||
|
}
|
||||||
|
|
||||||
|
gm.LeaderID = buf.readString()
|
||||||
|
gm.Protocol = buf.readString()
|
||||||
|
|
||||||
|
ary := buf.readInt32()
|
||||||
|
if ary == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gm.Members = append(gm.Members, GroupMember{
|
||||||
|
ID: buf.readString(),
|
||||||
|
ClientID: buf.readString(),
|
||||||
|
ClientHost: buf.readString(),
|
||||||
|
SessionTimeout: buf.readInt32(),
|
||||||
|
RebalanceTimeout: buf.readInt32(),
|
||||||
|
})
|
||||||
|
gm.Members[0].Subscription = readAssignment(buf)
|
||||||
|
gm.Members[0].Assignment = readAssignment(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAssignment(buf *buffer) []TopicAndPartition {
|
||||||
|
ass := []TopicAndPartition{}
|
||||||
|
buf.skip(2) // Eh
|
||||||
|
size := buf.readInt32()
|
||||||
|
if size == 0 {
|
||||||
|
buf.skip(4)
|
||||||
|
return ass
|
||||||
|
}
|
||||||
|
for i := 0; i < int(size); i++ {
|
||||||
|
ass = append(ass, readTopicAndSubscription(buf))
|
||||||
|
buf.skip(4) // Hash key or smth
|
||||||
|
}
|
||||||
|
return ass
|
||||||
|
}
|
||||||
|
|
||||||
|
func readTopicAndSubscription(buf *buffer) TopicAndPartition {
|
||||||
|
return TopicAndPartition{
|
||||||
|
Topic: buf.readString(),
|
||||||
|
Partition: buf.readInt32(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeTime(ts int64) time.Time {
|
||||||
|
return time.Unix(ts/1000, (ts%1000)*1000000)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Buffer
|
||||||
|
//
|
||||||
|
|
||||||
|
type buffer struct {
|
||||||
|
data []byte
|
||||||
|
pos int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buffer) skip(n int) {
|
||||||
|
b.pos += n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buffer) readBytes(n int) []byte {
|
||||||
|
b.pos += n
|
||||||
|
return b.data[b.pos-n : b.pos]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buffer) readInt16() int16 {
|
||||||
|
i := binary.BigEndian.Uint16(b.data[b.pos:])
|
||||||
|
b.pos += 2
|
||||||
|
return int16(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buffer) readInt32() int32 {
|
||||||
|
i := binary.BigEndian.Uint32(b.data[b.pos:])
|
||||||
|
b.pos += 4
|
||||||
|
return int32(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buffer) readInt64() int64 {
|
||||||
|
i := binary.BigEndian.Uint64(b.data[b.pos:])
|
||||||
|
b.pos += 8
|
||||||
|
return int64(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buffer) readString() string {
|
||||||
|
strlen := int(b.readInt16())
|
||||||
|
b.pos += strlen
|
||||||
|
return string(b.data[b.pos-strlen : b.pos])
|
||||||
|
}
|
Loading…
Reference in New Issue