Недавно на работе меня попросили придумать рабочую задачку для студентов. Поскольку я работаю в инфраструктурной команде, мои повседневные задачи вряд ли подходят для их домашек или курсовых работ. Чтобы найти подходящую идею, я начал перебирать инструменты, которыми мы с командой часто пользуемся. Большинство из них интегрированы с чатами и ботами, и один из ключевых инструментов — это Алерт Бот. Он отслеживает логи и отправляет оповещения, если происходит что-то необычное. Это позволяет нам быстрее обнаруживать и устранять инциденты.

Когда я придумал эту задачу для студентов, мне пришло в голову, что подобный функционал может быть полезен многим. Возможно, кто-то сможет адаптировать это решение под свои нужды и облегчить себе жизнь, автоматизируя процесс мониторинга и реагирования на инциденты. Собственно этим я бы и хотел поделиться в своей статье.

Описание задачи

Итак, студентам предлагается разработать бота, который будет анализировать логи сервера и отправлять уведомления в Telegram, если обнаруживает ошибки или другие аномалии. Основные требования к проекту:

  1. Мониторинг логов: Бот должен периодически проверять указанный файл логов и искать в нем ошибки.

  2. Фильтрация данных: Необходимо отфильтровать важные сообщения (например, с уровнем ERROR или CRITICAL) и игнорировать менее значимые (DEBUG, INFO).

  3. Отправка уведомлений: При обнаружении ошибок бот должен отправлять сообщение в Telegram-чат.

  4. Конфигурируемость: Настройки, такие как путь к логам, частота проверки и идентификатор чата, должны быть легко изменяемы.

Архитектура решения

Наш бот будет состоять из следующих основных компонентов:

  1. Лог-файл: Файл, содержащий журналы событий сервера, который бот будет анализировать.

  2. Анализатор логов: Модуль, который читает файл логов и извлекает из него важные сообщения.

  3. Telegram-бот: Модуль, который отправляет найденные ошибки в указанный чат.

Эти компоненты должны взаимодействовать между собой, обеспечивая непрерывный мониторинг логов и оперативное уведомление о проблемах.

Генерация логов

Давайте начнем с моделирования деятельности сервера, чтобы у нас были логи для анализа. Мы создадим скрипт на Go, который будет генерировать логи с разными уровнями сообщений (например, INFO, WARNING, ERROR) в случайном порядке.

Предположим, что наш "сервер" будет периодически генерировать события с различными уровнями важности. Для этого напишем скрипт, который будет записывать логи в файл.

1. Создание функции для чтения и записи строчек в файл

Сначала создадим функции, которые будут считывать логи и писать их:

func readLines(fileName string) ([]string, error) {
  file, err := os.Open(fileName)
  if os.IsNotExist(err) {
    return []string{}, nil
  } else if err != nil {
    return nil, err
  }
  defer file.Close()

  var lines []string
  scanner := bufio.NewScanner(file)
  for scanner.Scan() {
    lines = append(lines, scanner.Text())
  }
  return lines, scanner.Err()
}

func writeLines(lines []string, fileName string) error {
  file, err := os.Create(fileName)
  if err != nil {
    return err
  }
  defer file.Close()

  writer := bufio.NewWriter(file)
  for _, line := range lines {
    fmt.Fprintln(writer, line)
  }
  return writer.Flush()
}

2. Функции для генерации логов

Теперь создадим функции, которые будут генерировать нам непосредственно сами логи

var (
  logLevels = []string{"DEBUG", "INFO", "ERROR"}
  mu        sync.Mutex
)

func generateLog() {
  mu.Lock()
  defer mu.Unlock()

  lines, err := readLines(logFileName)
  if err != nil {
    fmt.Println("Error reading log file:", err)
    return
  }

  if len(lines) >= maxLines {
    lines = lines[len(lines)-maxLines+1:]
  }

  logLine := fmt.Sprintf("%s [%s] %s\n", time.Now().Format(time.RFC3339), logLevels[rand.Intn(len(logLevels))], generateRandomMessage())
  lines = append(lines, logLine)

  err = writeLines(lines, logFileName)
  if err != nil {
    fmt.Println("Error writing to log file:", err)
  }
}

func generateRandomMessage() string {
  messages := []string{
    "User logged in",
    "File uploaded",
    "Error processing request",
    "User logged out",
    "Database connection established",
    "Invalid input received",
  }
  return messages[rand.Intn(len(messages))]
}

3. Основная функция для генерации логов

Теперь объединим всё вместе в основной функции, которая будет периодически генерировать и записывать лог-сообщения:

const (
  logFileName = "logs.log"
  maxLines    = 200
)

func main() {
  rand.Seed(time.Now().UnixNano())

  go func() {
    for {
      generateLog()
      time.Sleep(time.Millisecond * 100)
    }
  }()

  select {}
}

Важно помнить, что логи не должны храниться бесконечно. Со временем их количество может значительно возрасти, что приведет к увеличению объема файла и возможным проблемам с производительностью. Чтобы этого избежать, следует установить разумные ограничения на количество сохраняемых записей. В данном примере мы ограничиваемся хранением последних 200 логов

Продолжение разработки бота на Go

Теперь, когда у нас есть сгенерированные логи, приступим к разработке бота, который будет их анализировать и отправлять уведомления в Telegram. Ниже описаны основные шаги для создания такого бота.

1. Установка необходимых библиотек

Для работы с Telegram и анализа логов нам потребуются несколько внешних библиотек. В Go это можно сделать с помощью go get:

go get -u github.com/go-telegram-bot-api/telegram-bot-api/v5

Эта библиотека предоставит нам интерфейс для взаимодействия с Telegram.

2. Чтение и анализ логов

Первый шаг в нашем боте — это чтение логов и поиск в них ошибок. Мы будем использовать функцию для чтения файла и фильтрации важных сообщений.

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
    "time"
)

func readLogs(logFilePath string, logChannel chan<- string) error {
    file, err := os.Open(logFilePath)
    if err != nil {
        return err
    }
    defer file.Close()

    reader := bufio.NewReader(file)

    for {
        line, err := reader.ReadString('\n')
        if err != nil {
            if err.Error() == "EOF" {
                continue
            } else {
                log.Printf("Error reading log file: %v", err)
                continue
            }
        }

        line = strings.TrimSpace(line)
        if strings.Contains(line, "ERROR") || strings.Contains(line, "CRITICAL") {
            logChannel <- line
        }
    }
}

3. Отправка уведомлений в Telegram

Следующим шагом будет отправка уведомлений в Telegram. Для этого воспользуемся библиотекой telegram-bot-api.

package main

import (
    "log"
    "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)

func sendTelegramNotification(bot *tgbotapi.BotAPI, chatID int64, message string) {
    msg := tgbotapi.NewMessage(chatID, message)
    _, err := bot.Send(msg)
    if err != nil {
        log.Printf("Error sending message: %v", err)
    }
}

4. Основная функция для мониторинга логов и отправки уведомлений

Теперь объединим всё вместе в основной функции. Она будет периодически проверять логи и отправлять уведомления, если обнаружены ошибки.

package main

import (
    "fmt"
    "log"
    "time"
    "os"

    "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)

func main() {
    logFilePath := "server.log"
    botToken := "YOUR_TELEGRAM_BOT_TOKEN" 
    chatID := int64(YOUR_CHAT_ID)         

    bot, err := tgbotapi.NewBotAPI(botToken)
    if err != nil {
        log.Panic(err)
    }

    log.Printf("Authorized on account %s", bot.Self.UserName)

    logChannel := make(chan string)

    // Запуск горутины для непрерывного чтения логов
    go func() {
        err := readLogs(logFilePath, logChannel)
        if err != nil {
            log.Fatalf("Error reading log file: %v", err)
        }
    }()

    // Основной цикл для обработки сообщений из канала и отправки их в Telegram
    for logMessage := range logChannel {
        sendTelegramNotification(bot, chatID, logMessage)
    }
}

5. Конфигурируемость

Для удобства можно вынести параметры, такие как путь к логам, токен бота и идентификатор чата, в конфигурационный файл или использовать переменные окружения. Это упростит адаптацию бота к разным условиям эксплуатации.

Пример использования переменных окружения:

package main

import (
    "os"
    "log"
)

func main() {
    logFilePath := os.Getenv("LOG_FILE_PATH")
    botToken := os.Getenv("TELEGRAM_BOT_TOKEN")
    chatID := os.Getenv("TELEGRAM_CHAT_ID")

    if logFilePath == "" || botToken == "" || chatID == "" {
        log.Fatal("Missing necessary environment variables")
    }

    // Остальной код...
}

Теперь, когда бот готов, его можно развернуть на сервере или в облаке и использовать для мониторинга логов в реальных условиях. Опытные разработчики, вероятно, уже знакомы с подобными задачами, но надеюсь, что эта статья оказалась полезной для тех, кто только начинает свой путь в программировании, работает над своими пет-проектами или делает первые шаги в индустрии. Создание таких инструментов — отличный способ приобрести навыки, которые пригодятся в реальной работе.

Комментарии (6)


  1. johhy13
    13.08.2024 18:41
    +1

    // В первой распечатке можно написать вместо
    if os.IsNotExist(err) {
        return []string{}, nil  
    } else if err != nil { 
       return nil, err  
    } 
    
    // Как то так 
    if err != nil {
      if os.IsNotExist(err) {
         return []string{}, nil  
      }
    
      return nil, err
    }

    Но кажется что смысла возвращать пустой слайс нет (далее может быть проблема)


    1. BincomAD Автор
      13.08.2024 18:41
      +2

      Привет, спасибо за замечание) А какие могут быть потенциально проблемы с пустой строкой?

      В моем понимании это как бы означает не ошибку, а то что логи еще не писались в файл, нет логов - нет файла, собственно поэтому логически это не вынесено под иф с ошибкой)


  1. AndreiNekrasOn
    13.08.2024 18:41

    В такой реализации получается, что читаем полностью весь файл логов и на каждую ошибку отсылаем по уведомлению, и повторно отсылаем те же уведомления (+ возможно новые) каждые десять секунд?


    1. BincomAD Автор
      13.08.2024 18:41
      +2

      Да, изначально подумал, что как тестовый пример пойдет, но сейчас вижу, что многие сохраняют и возможно будут использовать этот код, поэтому подправил код и предлагаю запустить постоянно читающую логи горутину, писать их в канал, а из канала потом вычитывать)


  1. manyakRus
    13.08.2024 18:41
    +1

    https://github.com/ManyakRus/telegram_loki
    я уже сделал такой :-) очень полезный


    1. Onetouchman
      13.08.2024 18:41

      Я немного не понимаю чем плох встроенный алерт менеджер с рендером и отправкой сообщений из графаны в телеграмм с картинками по настроеным алертам на панель например с теми же логами из локи