Ctrl K
ComunidadGuías…

internal/kafka/monitoring.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package kafka

import (
	"context"
	"fmt"

	"github.com/Shopify/sarama"
	"github.com/flexprice/flexprice/internal/config"
	"github.com/flexprice/flexprice/internal/logger"
)

// ConsumerLag represents the lag for a consumer group on a topic
type ConsumerLag struct {
	Topic         string
	ConsumerGroup string
	TotalLag      int64
	PartitionLags map[int32]int64
}

// MonitoringService provides Kafka monitoring capabilities
type MonitoringService struct {
	config *config.Configuration
	logger *logger.Logger
}

// NewMonitoringService creates a new Kafka monitoring service
func NewMonitoringService(cfg *config.Configuration, log *logger.Logger) *MonitoringService {
	return &MonitoringService{
		config: cfg,
		logger: log,
	}
}

// GetConsumerLag calculates the consumer lag for a given topic and consumer group
func (m *MonitoringService) GetConsumerLag(ctx context.Context, topic string, consumerGroup string) (*ConsumerLag, error) {
	// Create Sarama config
	saramaConfig, err := GetSaramaConfig(&m.config.Kafka)
	if err != nil {
		m.logger.Error(ctx, "failed to build Kafka client config", "error", err)
		return nil, err
	}
	saramaConfig.Consumer.Return.Errors = true

	// Create cluster admin client
	admin, err := sarama.NewClusterAdmin(m.config.Kafka.Brokers, saramaConfig)
	if err != nil {
		m.logger.Error(ctx, "failed to create Kafka admin client",
			"error", err,
			"brokers", m.config.Kafka.Brokers)
		return nil, fmt.Errorf("failed to create Kafka admin client: %w", err)
	}
	defer admin.Close()

	// Create client to get partition offsets
	client, err := sarama.NewClient(m.config.Kafka.Brokers, saramaConfig)
	if err != nil {
		m.logger.Error(ctx, "failed to create Kafka client",
			"error", err,
			"brokers", m.config.Kafka.Brokers)
		return nil, fmt.Errorf("failed to create Kafka client: %w", err)
	}
	defer client.Close()

	// Get partitions for the topic
	partitions, err := client.Partitions(topic)
	if err != nil {
		m.logger.Error(ctx, "failed to get partitions for topic",
			"error", err,
			"topic", topic)
		return nil, fmt.Errorf("failed to get partitions for topic %s: %w", topic, err)
	}

	m.logger.Info(ctx, "fetching consumer group offsets",
		"topic", topic,
		"consumer_group", consumerGroup,
		"partitions", partitions)

	// Use ClusterAdmin to fetch consumer group offsets - THIS IS THE CORRECT WAY
	offsetFetchResponse, err := admin.ListConsumerGroupOffsets(consumerGroup, map[string][]int32{
		topic: partitions,
	})
	if err != nil {
		m.logger.Error(ctx, "failed to list consumer group offsets",
			"error", err,
			"consumer_group", consumerGroup,
			"topic", topic)
		return nil, fmt.Errorf("failed to list consumer group offsets: %w", err)
	}

	// Calculate lag for each partition
	consumerLag := &ConsumerLag{
		Topic:         topic,
		ConsumerGroup: consumerGroup,
		TotalLag:      0,
		PartitionLags: make(map[int32]int64),
	}

	for _, partition := range partitions {
		// Get the latest offset (high water mark) for this partition
		latestOffset, err := client.GetOffset(topic, partition, sarama.OffsetNewest)
		if err != nil {
			m.logger.Info(ctx, "failed to get latest offset for partition",
				"error", err,
				"topic", topic,
				"partition", partition)
			continue
		}

		// Get consumer's current offset for this partition from the response
		block := offsetFetchResponse.GetBlock(topic, partition)
		if block == nil {
			m.logger.Info(ctx, "no offset block found for partition",
				"topic", topic,
				"partition", partition,
				"consumer_group", consumerGroup)
			// If no block found, assume consumer hasn't started - lag = all messages
			partitionLag := latestOffset
			if partitionLag < 0 {
				partitionLag = 0
			}
			consumerLag.PartitionLags[partition] = partitionLag
			consumerLag.TotalLag += partitionLag
			continue
		}

		consumerOffset := block.Offset

		m.logger.Debug(ctx, "partition offset details",
			"topic", topic,
			"partition", partition,
			"consumer_group", consumerGroup,
			"latest_offset", latestOffset,
			"consumer_offset", consumerOffset,
			"offset_metadata", block.Metadata)

		// If consumer offset is -1, it means no offset has been committed yet
		// In this case, the lag is the total number of messages in the partition
		if consumerOffset == -1 {
			partitionLag := latestOffset
			if partitionLag < 0 {
				partitionLag = 0
			}
			consumerLag.PartitionLags[partition] = partitionLag
			consumerLag.TotalLag += partitionLag

			m.logger.Info(ctx, "partition has no committed offset",
				"topic", topic,
				"partition", partition,
				"consumer_group", consumerGroup,
				"lag", partitionLag)
			continue
		}

		// Calculate lag for this partition
		// Lag = Latest Offset - Consumer's Committed Offset
		partitionLag := latestOffset - consumerOffset
		if partitionLag < 0 {
			partitionLag = 0
		}

		consumerLag.PartitionLags[partition] = partitionLag
		consumerLag.TotalLag += partitionLag

		m.logger.Info(ctx, "partition lag calculated",
			"topic", topic,
			"partition", partition,
			"consumer_group", consumerGroup,
			"latest_offset", latestOffset,
			"consumer_offset", consumerOffset,
			"lag", partitionLag)
	}

	m.logger.Info(ctx, "total consumer lag calculated",
		"topic", topic,
		"consumer_group", consumerGroup,
		"total_lag", consumerLag.TotalLag,
		"partitions", len(partitions),
		"partition_details", consumerLag.PartitionLags)

	return consumerLag, nil
}

// GetMultipleConsumerLags fetches lag for multiple topic/consumer-group pairs
func (m *MonitoringService) GetMultipleConsumerLags(ctx context.Context, configs []struct {
	Topic         string
	ConsumerGroup string
}) (map[string]*ConsumerLag, error) {
	results := make(map[string]*ConsumerLag)

	for _, cfg := range configs {
		key := fmt.Sprintf("%s:%s", cfg.Topic, cfg.ConsumerGroup)
		lag, err := m.GetConsumerLag(ctx, cfg.Topic, cfg.ConsumerGroup)
		if err != nil {
			m.logger.Info(ctx, "failed to get consumer lag",
				"error", err,
				"topic", cfg.Topic,
				"consumer_group", cfg.ConsumerGroup)
			// Store zero lag on error to avoid blocking the entire response
			results[key] = &ConsumerLag{
				Topic:         cfg.Topic,
				ConsumerGroup: cfg.ConsumerGroup,
				TotalLag:      0,
				PartitionLags: make(map[int32]int64),
			}
			continue
		}
		results[key] = lag
	}

	return results, nil
}