Автоматизация выполнения SQL-файлов с помощью Go: опыт разработки и настройка уведомлений

Автоматизация выполнения SQL-файлов с помощью Go: опыт разработки и настройка уведомлений

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

Шаг 1. Инициализация проекта и конфигурация приложения

Первый этап заключался в разработке структуры проекта и создании файла конфигурации. Для гибкости работы все настройки — параметры подключения к базе данных, информация для SMTP-уведомлений и пути к рабочим папкам — вынесены в отдельный файл config.yml. Благодаря этому можно легко изменять параметры без изменения исходного кода.

database:
  host: localhost
  port: 3306
  user: root
  password: secret
  name: mydb

smtp:
  server: smtp.example.com
  port: 587
  username: user@example.com
  password: smtp-password
  from: noreply@example.com
  to: admin@example.com

paths:
  input: ./sql/input
  output: ./sql/processed
  error: ./sql/errors

Шаг 2. Подключение к базе данных

После загрузки настроек из файла необходимо установить соединение с базой данных. Для этого я использовал пакет для работы с MySQL. Необходимо установить соединение с базой и проверить корректность взаимодействия с БД.

func initDB() {
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true&multiStatements=true",
		config.Database.User,
		config.Database.Password,
		config.Database.Host,
		config.Database.Port,
		config.Database.Name)

	var err error
	db, err = sql.Open("mysql", dsn)
	if err != nil {
		log.Fatalf("Error connecting to database: %v", err)
	}

	err = db.Ping()
	if err != nil {
		log.Fatalf("Error pinging database: %v", err)
	}
}

Шаг 3. Мониторинг папки и обработка файлов

Основная функциональность скрипта заключается в постоянном наблюдении за заданной директорией. Каждые несколько секунд приложение проверяет наличие новых файлов SQL, и если они обнаружены, оно читает содержимое файла и пытается выполнить SQL-запросы в базе.

Особенности этого этапа:

  • Проверка каждого файла на наличие лишних символов (например, BOM) – это важно для корректного выполнения запроса.
  • Логирование ошибок чтения или исполнения SQL-запросов для анализа аварийных ситуаций.
  • Разделение файлов на успешно выполненные и те, что привели к ошибке, посредством переноса их в соответствующие папки (done и failed).
func processFiles() {
	files, err := os.ReadDir(config.Paths.Input)
	if err != nil {
		log.Printf("Error reading input directory: %v", err)
		return
	}

	for _, file := range files {
		if file.IsDir() {
			continue
		}

		filePath := filepath.Join(config.Paths.Input, file.Name())
		processFile(filePath)
	}
}

func processFile(filePath string) {
	data, err := os.ReadFile(filePath)
	if err != nil {
		log.Printf("Error reading file %s: %v", filePath, err)
		return
	}

	err = executeSQL(string(data))
	if err != nil {
		handleError(filePath, err)
	} else {
		handleSuccess(filePath)
	}
}

Шаг 4. Реализация логики выполнения SQL-запросов

Каждый SQL-файл проходит проверку перед исполнением, после чего с помощью db.Exec выполняется SQL-запрос. Если запрос успешно выполнен, файл перемещается в папку архивированных скриптов; в противном случае он переносится в папку ошибок. Такой подход позволяет избежать повторного исполнения одного и того же запроса и помогает отследить, какие файлы требуют повторного внимания.

func executeSQL(query string) error {
	if strings.Contains(query, "\ufeff") {
		query = strings.ReplaceAll(query, "\ufeff", "")
	}

	_, err := db.Exec(query)
	return err
}

Шаг 5. Отправка отчёта по выполнению на электронную почту

В завершении работы скрипта важным моментом является отправка отчёта администратору. При каждом выполнении операции (успешном или с ошибкой) отправляется соответствующее уведомление через SMTP. Использование защищённого TLS-соединения обеспечивает безопасность передачи данных. Это позволяет оперативно получать обратную связь по работе скрипта и предотвращать возможные проблемы в работе системы.

func handleError(filePath string, err error) {
	log.Printf("Error executing SQL: %v", err)
	sendEmail("SQL Execution Error", fmt.Sprintf("Error: %v\nFile: %s", err, filePath))
	moveFile(filePath, config.Paths.Failed)
}

Преимущества и недостатки подхода

Преимущества:

  • Автоматизация рутинных задач позволяет значительно сократить время на выполнение операций и снизить риск ошибок.
  • Гибкость настроек через внешний конфигурационный файл дает возможность легко адаптировать скрипт под различные среды.
  • Отправка отчётов по электронной почте обеспечивает оперативное информирование ответственных лиц.

Недостатки:

  • Постоянная проверка дисковой папки может создавать нагрузку на систему при большом количестве файлов, если не реализована оптимальная стратегия кеширования или очередей.
  • В случае большого объёма SQL-запросов могут возникнуть вопросы синхронизации, особенно если запросы требуют значительных ресурсов.
  • Наличие минимальной обработки ошибок в спринтовых запросах требует дополнительного мониторинга для своевременного реагирования.

Заключение

Разработка данного микросервиса стала для меня уникальным опытом в области автоматизации рутинных задач с использованием Go. Правильное разделение логики на конфигурацию, подключение к базе, мониторинг файлов и уведомления делает систему гибкой и масштабируемой. Такой подход позволяет не только минимизировать риски, связанные с ручным выполнением SQL-скриптов, но и оперативно реагировать на возникающие проблемы через систему отчётности.

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

package main

import (
	"crypto/tls"
	"database/sql"
	"fmt"
	_ "github.com/octoper/go-ray"
	"log"
	"net/smtp"
	"os"
	"path/filepath"
	"strings"
	"time"

	_ "github.com/go-sql-driver/mysql"
	"gopkg.in/yaml.v3"
)

type Config struct {
	Database struct {
		Host     string `yaml:"host"`
		Port     int    `yaml:"port"`
		User     string `yaml:"user"`
		Password string `yaml:"password"`
		Name     string `yaml:"name"`
	} `yaml:"database"`

	SMTP struct {
		Server   string `yaml:"server"`
		Port     int    `yaml:"port"`
		Username string `yaml:"username"`
		Password string `yaml:"password"`
		From     string `yaml:"from"`
		To       string `yaml:"to"`
	} `yaml:"smtp"`

	Paths struct {
		Input  string `yaml:"input"`
		Done   string `yaml:"done"`
		Failed string `yaml:"failed"`
	} `yaml:"paths"`
}

var (
	config Config
	db     *sql.DB
)

func main() {
	loadConfig("config.yml")

	initDB()
	defer db.Close()

	for {
		processFiles()
		time.Sleep(5 * time.Second) // Проверка каждые 5 секунд
	}
}

func loadConfig(filename string) {
	data, err := os.ReadFile(filename)
	if err != nil {
		log.Fatalf("Error reading config file: %v", err)
	}

	err = yaml.Unmarshal(data, &config)
	if err != nil {
		log.Fatalf("Error parsing config file: %v", err)
	}
}

func initDB() {
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true&multiStatements=true",
		config.Database.User,
		config.Database.Password,
		config.Database.Host,
		config.Database.Port,
		config.Database.Name)

	var err error
	db, err = sql.Open("mysql", dsn)
	if err != nil {
		log.Fatalf("Error connecting to database: %v", err)
	}

	err = db.Ping()
	if err != nil {
		log.Fatalf("Error pinging database: %v", err)
	}
}

func processFiles() {
	files, err := os.ReadDir(config.Paths.Input)
	if err != nil {
		log.Printf("Error reading input directory: %v", err)
		return
	}

	for _, file := range files {
		if file.IsDir() {
			continue
		}

		filePath := filepath.Join(config.Paths.Input, file.Name())
		processFile(filePath)
	}
}

func processFile(filePath string) {
	data, err := os.ReadFile(filePath)
	if err != nil {
		log.Printf("Error reading file %s: %v", filePath, err)
		return
	}

	err = executeSQL(string(data))
	if err != nil {
		handleError(filePath, err)
	} else {
		handleSuccess(filePath)
	}
}

func executeSQL(query string) error {
	if strings.Contains(query, "\ufeff") {
		query = strings.ReplaceAll(query, "\ufeff", "")
	}

	_, err := db.Exec(query)
	return err
}

func handleError(filePath string, err error) {
	log.Printf("Error executing SQL: %v", err)
	sendEmail("SQL Execution Error", fmt.Sprintf("Error: %v\nFile: %s", err, filePath))
	moveFile(filePath, config.Paths.Failed)
}

func handleSuccess(filePath string) {
	log.Println("SQL executed successfully")
	sendEmail("SQL Execution Success", fmt.Sprintf("File: %s", filePath))
	moveFile(filePath, config.Paths.Done)
}

func moveFile(source, destDir string) {
	fileName := filepath.Base(source)
	destPath := filepath.Join(destDir, fileName)

	err := os.Rename(source, destPath)
	if err != nil {
		log.Printf("Error moving file: %v", err)
	}
}

func sendEmail(subject, body string) {
	tlsConfig := &tls.Config{
		ServerName:         config.SMTP.Server,
		InsecureSkipVerify: false,
	}

	conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", config.SMTP.Server, config.SMTP.Port), tlsConfig)
	if err != nil {
		log.Printf("Error creating TLS connection: %v", err)
		return
	}
	defer conn.Close()

	client, err := smtp.NewClient(conn, config.SMTP.Server)
	if err != nil {
		log.Printf("Error creating SMTP client: %v", err)
		return
	}
	defer client.Close()

	auth := smtp.PlainAuth("", config.SMTP.Username, config.SMTP.Password, config.SMTP.Server)
	if err := client.Auth(auth); err != nil {
		log.Printf("SMTP auth error: %v", err)
		return
	}

	if err := client.Mail(config.SMTP.From); err != nil {
		log.Printf("Mail command error: %v", err)
		return
	}
	if err := client.Rcpt(config.SMTP.To); err != nil {
		log.Printf("Rcpt command error: %v", err)
		return
	}

	wc, err := client.Data()
	if err != nil {
		log.Printf("Data command error: %v", err)
		return
	}
	defer wc.Close()

	msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s",
		config.SMTP.From,
		config.SMTP.To,
		subject,
		body,
	)

	if _, err = fmt.Fprint(wc, msg); err != nil {
		log.Printf("Error writing message: %v", err)
		return
	}
}

 

Популярное

Самые популярные посты

Как быть максимально продуктивным на удалённой работе?
Business

Как быть максимально продуктивным на удалённой работе?

Я запустил собственный бизнес и намеренно сделал всё возможное, чтобы работать из любой точки мира. Иногда я сижу с своём кабинете с большим 27-дюймовым монитором в своей квартире в г. Чебоксары. Иногда я нахожусь в офисе или в каком-нибудь кафе в другом городе.

Привет! Меня зовут Сергей Емельянов и я трудоголик
Business PHP

Привет! Меня зовут Сергей Емельянов и я трудоголик

Я программист. В душе я предприниматель. Я начал зарабатывать деньги с 11 лет, в суровые 90-е годы, сдавая стеклотару в местный магазин и обменивая её на сладости. Я зарабатывал столько, что хватало на разные вкусняшки.

Акция! Профессиональный разработчик CRM за 2000 руб. в час

Выделю время под ваш проект. Знания технологий Vtiger CRM, SuiteCRM, Laravel, Vue.js, Golang, React.js, Wordpress. Предлагаю варианты сотрудничества, которые помогут вам воспользоваться преимуществами внешнего опыта, оптимизировать затраты и снизить риски. Полная прозрачность всех этапов работы и учёт временных затрат. Оплачивайте только рабочие часы разработки после приемки задачи. Экономьте на платежах по его содержанию разработчика в штате. Возможно заключение договора по ИП. С чего начать, чтобы нанять профессионального разработчика на full-time? Просто заполните форму!

Telegram
@sergeyem
Telephone
+4915211100235
Email