Sergey Antropoff 8b8f26909c feat: улучшения proxcluster коллектора и системы логирования
- Переписан proxcluster коллектор с асинхронным получением данных
- Добавлена информация о loadavg для каждой ноды
- Добавлена суммарная статистика кластера (CPU, память, VM, контейнеры)
- Добавлено время выполнения во все коллекторы Go (execution_time_ms/seconds)
- Улучшено логирование агента:
  * Логи запуска/завершения коллекторов
  * Информация о коллекторах в Kafka/stdout логах
- Добавлен новый коллектор proxnode
- Обновлен Makefile для сборки proxcluster коллектора
- Исправлены типы данных в main.go файлах коллекторов
2025-09-12 00:11:32 +03:00

236 lines
8.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package output
// Автор: Сергей Антропов, сайт: https://devops.org.ru
// Назначение: Вывод результатов — stdout и Kafka.
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"log/slog"
"os"
"strings"
"time"
"github.com/segmentio/kafka-go"
"net"
"strconv"
)
// Payload — общий JSON для вывода.
type Payload map[string]any
// Output — интерфейс механизма доставки результатов.
type Output interface {
Write(ctx context.Context, data Payload) error
Close(ctx context.Context) error
}
// StdoutOutput — вывод в stdout.
type StdoutOutput struct{}
func (s *StdoutOutput) Write(_ context.Context, data Payload) error {
// Извлекаем имена коллекторов из payload для логирования
collectorNames := make([]string, 0, len(data))
for key := range data {
collectorNames = append(collectorNames, key)
}
// Логируем вывод в stdout с информацией о коллекторах
slog.Info("stdout output", "collectors", collectorNames)
enc := json.NewEncoder(os.Stdout)
enc.SetEscapeHTML(false)
return enc.Encode(data)
}
func (s *StdoutOutput) Close(_ context.Context) error { return nil }
// KafkaOutput — вывод в Kafka.
type KafkaOutput struct {
writer *kafka.Writer
topic string
brokers []string
}
// KafkaOptions описывает параметры подключения к Kafka.
type KafkaOptions struct {
Brokers []string
Topic string
ClientID string
Timeout time.Duration
SASLUser string
SASLPass string
EnableTLS bool
// SSL настройки
SSLEnabled bool
SSLKeystoreLocation string
SSLKeystorePassword string
SSLKeyPassword string
SSLTruststoreLocation string
SSLTruststorePassword string
SSLClientAuth string
SSLEndpointIdentificationAlgorithm string
}
// NewKafkaOutput создаёт Kafka writer.
func NewKafkaOutput(opts KafkaOptions) (*KafkaOutput, error) {
if len(opts.Brokers) == 0 || strings.TrimSpace(opts.Topic) == "" {
return nil, errors.New("kafka brokers/topic not configured")
}
// Пытаемся создать топик (идемпотентно). Ошибки игнорируем — брокер может создавать топики сам.
ensureTopic(opts)
// Настройка транспорта для Kafka
var transport kafka.RoundTripper
if opts.SSLEnabled {
// Создаём TLS конфигурацию для SSL подключения
tlsConfig := &tls.Config{
InsecureSkipVerify: opts.SSLEndpointIdentificationAlgorithm == "none",
}
// Если указан truststore, загружаем его
if opts.SSLTruststoreLocation != "" {
// В production среде здесь должна быть загрузка truststore
// Для простоты используем системные сертификаты
slog.Info("kafka ssl: using system certificates for truststore",
"truststore_location", opts.SSLTruststoreLocation)
}
// Если указан keystore, загружаем его
if opts.SSLKeystoreLocation != "" {
// В production среде здесь должна быть загрузка keystore
// Для простоты используем системные сертификаты
slog.Info("kafka ssl: using system certificates for keystore",
"keystore_location", opts.SSLKeystoreLocation)
}
transport = &kafka.Transport{
TLS: tlsConfig,
}
slog.Info("kafka ssl enabled",
"endpoint_identification", opts.SSLEndpointIdentificationAlgorithm,
"client_auth", opts.SSLClientAuth)
}
w := &kafka.Writer{
Addr: kafka.TCP(opts.Brokers...),
Topic: opts.Topic,
Balancer: &kafka.LeastBytes{},
RequiredAcks: kafka.RequireAll,
Transport: transport,
}
ko := &KafkaOutput{writer: w, topic: opts.Topic, brokers: opts.Brokers}
// В самом начале — проверим подключение и залогируем статус
pingCtx, cancel := context.WithTimeout(context.Background(), maxDuration(opts.Timeout, 5*time.Second))
defer cancel()
if err := ko.Ping(pingCtx); err != nil {
slog.Error("kafka connect failed", "brokers", opts.Brokers, "topic", opts.Topic, "ssl_enabled", opts.SSLEnabled, "err", err)
} else {
slog.Info("kafka connected", "brokers", opts.Brokers, "topic", opts.Topic, "ssl_enabled", opts.SSLEnabled)
}
return ko, nil
}
func (k *KafkaOutput) Write(ctx context.Context, data Payload) error {
b, err := json.Marshal(data)
if err != nil {
return err
}
msg := kafka.Message{Value: b, Time: time.Now()}
if err := k.writer.WriteMessages(ctx, msg); err != nil {
return err
}
// Извлекаем имена коллекторов из payload для логирования
collectorNames := make([]string, 0, len(data))
for key := range data {
collectorNames = append(collectorNames, key)
}
// Логируем успешную отправку с информацией о коллекторах
slog.Info("kafka message sent", "topic", k.topic, "bytes", len(b), "collectors", collectorNames)
return nil
}
func (k *KafkaOutput) Close(ctx context.Context) error {
if k.writer != nil {
return k.writer.Close()
}
return nil
}
// ensureTopic — пробует создать топик через контроллер кластера Kafka.
// Безопасно к многократному вызову. В случае ошибок ничего не делает.
func ensureTopic(opts KafkaOptions) {
if len(opts.Brokers) == 0 || strings.TrimSpace(opts.Topic) == "" { return }
timeout := opts.Timeout
if timeout <= 0 { timeout = 5 * time.Second }
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
// Настройка транспорта для SSL если включен
var dialer *kafka.Dialer
if opts.SSLEnabled {
tlsConfig := &tls.Config{
InsecureSkipVerify: opts.SSLEndpointIdentificationAlgorithm == "none",
}
dialer = &kafka.Dialer{
TLS: tlsConfig,
}
}
// Подключаемся к первому брокеру
var conn *kafka.Conn
var err error
if dialer != nil {
conn, err = dialer.DialContext(ctx, "tcp", opts.Brokers[0])
} else {
conn, err = kafka.DialContext(ctx, "tcp", opts.Brokers[0])
}
if err != nil { return }
defer func() { _ = conn.Close() }()
// Получаем контроллер
ctrl, err := conn.Controller()
if err != nil { return }
_ = conn.Close()
addr := net.JoinHostPort(ctrl.Host, strconv.Itoa(ctrl.Port))
var c2 *kafka.Conn
if dialer != nil {
c2, err = dialer.DialContext(ctx, "tcp", addr)
} else {
c2, err = kafka.DialContext(ctx, "tcp", addr)
}
if err != nil { return }
defer func() { _ = c2.Close() }()
// Пытаемся создать топик с 1 репликой и 3 партициями по умолчанию
_ = c2.CreateTopics(kafka.TopicConfig{Topic: opts.Topic, NumPartitions: 3, ReplicationFactor: 1})
}
// Ping — проверка доступности кластера Kafka по первому брокеру
func (k *KafkaOutput) Ping(ctx context.Context) error {
if len(k.brokers) == 0 { return errors.New("no brokers configured") }
// Для ping используем простой TCP подключение без SSL
// так как это только проверка доступности
conn, err := kafka.DialContext(ctx, "tcp", k.brokers[0])
if err != nil { return err }
defer func() { _ = conn.Close() }()
// Попросим контроллера — если ответ есть, считаем, что связь установлена
_, err = conn.Controller()
return err
}
func maxDuration(a, b time.Duration) time.Duration { if a > b { return a }; return b }