В современном мире автоматизация рутинных процессов помогает экономить время и избегать ошибок. В одном из проектов передо мной стояла задача — автоматически исполнять 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
}
}