크론구조 개선
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
@@ -17,8 +18,8 @@ import (
|
||||
const (
|
||||
defaultPort = "8080"
|
||||
defaultDBPath = "/initial_data/GeoLite2-City.mmdb"
|
||||
defaultCron = ""
|
||||
defaultScript = "./scripts/dump_and_import.sh"
|
||||
defaultCron = "5 0 * * *" // 매일 00:05 KST
|
||||
defaultJob = "user-program-sync"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -26,7 +27,7 @@ func main() {
|
||||
dbPath := env("GEOIP_DB_PATH", defaultDBPath)
|
||||
dbURL := os.Getenv("DATABASE_URL")
|
||||
lookupQuery := os.Getenv("GEOIP_LOOKUP_QUERY")
|
||||
port := env("PORT", defaultPort)
|
||||
port := env("SERVICE_PORT", defaultPort)
|
||||
|
||||
resolver, err := geo.NewResolver(geo.Config{
|
||||
Backend: backend,
|
||||
@@ -112,6 +113,21 @@ func env(key, fallback string) string {
|
||||
return fallback
|
||||
}
|
||||
|
||||
func envBool(key string, fallback bool) bool {
|
||||
val := os.Getenv(key)
|
||||
if val == "" {
|
||||
return fallback
|
||||
}
|
||||
switch strings.ToLower(val) {
|
||||
case "1", "t", "true", "y", "yes", "on":
|
||||
return true
|
||||
case "0", "f", "false", "n", "no", "off":
|
||||
return false
|
||||
default:
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeDBURL(raw string) string {
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
@@ -121,15 +137,16 @@ func sanitizeDBURL(raw string) string {
|
||||
}
|
||||
|
||||
func maybeStartScheduler() func() context.Context {
|
||||
cronExpr := env("USER_PROGRAM_CRON", defaultCron)
|
||||
if cronExpr == "" {
|
||||
enabled := envBool("USER_PROGRAM_CRON_ENABLE", false)
|
||||
if !enabled {
|
||||
return nil
|
||||
}
|
||||
script := env("USER_PROGRAM_SCRIPT", defaultScript)
|
||||
cronExpr := defaultCron
|
||||
command := defaultJob
|
||||
|
||||
sched, err := schedule.Start(schedule.Config{
|
||||
CronExpr: cronExpr,
|
||||
ScriptPath: script,
|
||||
Command: command,
|
||||
Logger: log.Default(),
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
295
cmd/user_program_dump/main.go
Normal file
295
cmd/user_program_dump/main.go
Normal file
@@ -0,0 +1,295 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultUpdateDir = "./update_data"
|
||||
defaultTable = "user_program_info"
|
||||
defaultDB = "user_program_info"
|
||||
defaultDumpTimeout = 5 * time.Minute
|
||||
)
|
||||
|
||||
type config struct {
|
||||
host string
|
||||
port int
|
||||
user string
|
||||
password string
|
||||
database string
|
||||
table string
|
||||
updateDir string
|
||||
target time.Time
|
||||
}
|
||||
|
||||
func main() {
|
||||
logger := log.New(os.Stdout, "[dump] ", log.LstdFlags)
|
||||
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
log.Fatalf("config error: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultDumpTimeout)
|
||||
defer cancel()
|
||||
|
||||
dsn := (&mysql.Config{
|
||||
User: cfg.user,
|
||||
Passwd: cfg.password,
|
||||
Net: "tcp",
|
||||
Addr: netAddr(cfg.host, cfg.port),
|
||||
DBName: cfg.database,
|
||||
Params: map[string]string{"parseTime": "true", "loc": "UTC", "charset": "utf8mb4"},
|
||||
AllowNativePasswords: true,
|
||||
}).FormatDSN()
|
||||
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open mysql connection: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
log.Fatalf("failed to ping mysql: %v", err)
|
||||
}
|
||||
|
||||
if _, err := db.ExecContext(ctx, "SET time_zone = '+00:00'"); err != nil {
|
||||
log.Fatalf("failed to set timezone: %v", err)
|
||||
}
|
||||
|
||||
outPath, err := dumpToCSV(ctx, db, cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("dump failed: %v", err)
|
||||
}
|
||||
|
||||
logger.Printf("dumped %s to %s", cfg.target.Format("2006-01-02"), outPath)
|
||||
}
|
||||
|
||||
func loadConfig() (config, error) {
|
||||
port, err := strconv.Atoi(env("USER_PROGRAM_INFO_PORT", "3306"))
|
||||
if err != nil {
|
||||
return config{}, fmt.Errorf("invalid USER_PROGRAM_INFO_PORT: %w", err)
|
||||
}
|
||||
|
||||
target, err := resolveTargetDate(env("USER_PROGRAM_TARGET_DATE", ""))
|
||||
if err != nil {
|
||||
return config{}, err
|
||||
}
|
||||
|
||||
table := env("USER_PROGRAM_INFO_TABLE", defaultTable)
|
||||
if !regexp.MustCompile(`^[a-zA-Z0-9_]+$`).MatchString(table) {
|
||||
return config{}, fmt.Errorf("invalid table name: %s", table)
|
||||
}
|
||||
|
||||
updateDir := env("USER_PROGRAM_UPDATE_DIR", defaultUpdateDir)
|
||||
if updateDir == "" {
|
||||
updateDir = defaultUpdateDir
|
||||
}
|
||||
if err := os.MkdirAll(updateDir, 0o755); err != nil {
|
||||
return config{}, fmt.Errorf("creating update dir: %w", err)
|
||||
}
|
||||
|
||||
return config{
|
||||
host: envRequired("USER_PROGRAM_INFO_HOST"),
|
||||
port: port,
|
||||
user: envRequired("USER_PROGRAM_INFO_USERNAME"),
|
||||
password: envRequired("USER_PROGRAM_INFO_PASSWORD"),
|
||||
database: env("USER_PROGRAM_INFO_DB", defaultDB),
|
||||
table: table,
|
||||
updateDir: updateDir,
|
||||
target: target,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func dumpToCSV(ctx context.Context, db *sql.DB, cfg config) (string, error) {
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
id,
|
||||
product_name,
|
||||
login_id,
|
||||
user_employee_id,
|
||||
login_version,
|
||||
login_public_ip,
|
||||
login_local_ip,
|
||||
user_company,
|
||||
user_department,
|
||||
user_position,
|
||||
user_login_time,
|
||||
created_at,
|
||||
user_family_flag
|
||||
FROM %s
|
||||
WHERE DATE(CONVERT_TZ(created_at, '+00:00', '+09:00')) = ?;`, cfg.table)
|
||||
|
||||
rows, err := db.QueryContext(ctx, query, cfg.target.Format("2006-01-02"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
filename := fmt.Sprintf("user_program_info_%s.csv", cfg.target.Format("20060102"))
|
||||
outPath := filepath.Join(cfg.updateDir, filename)
|
||||
tmpPath := outPath + ".tmp"
|
||||
|
||||
f, err := os.Create(tmpPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
writer := csv.NewWriter(f)
|
||||
defer writer.Flush()
|
||||
|
||||
header := []string{
|
||||
"id",
|
||||
"product_name",
|
||||
"login_id",
|
||||
"user_employee_id",
|
||||
"login_version",
|
||||
"login_public_ip",
|
||||
"login_local_ip",
|
||||
"user_company",
|
||||
"user_department",
|
||||
"user_position",
|
||||
"user_login_time",
|
||||
"created_at",
|
||||
"user_family_flag",
|
||||
}
|
||||
if err := writer.Write(header); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
record, err := scanRow(rows)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := writer.Write(record); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
writer.Flush()
|
||||
if err := writer.Error(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := os.Rename(tmpPath, outPath); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return outPath, nil
|
||||
}
|
||||
|
||||
func scanRow(rows *sql.Rows) ([]string, error) {
|
||||
var (
|
||||
id sql.NullInt64
|
||||
productName sql.NullString
|
||||
loginID sql.NullString
|
||||
employeeID sql.NullString
|
||||
loginVersion sql.NullString
|
||||
loginPublicIP sql.NullString
|
||||
loginLocalIP sql.NullString
|
||||
userCompany sql.NullString
|
||||
userDepartment sql.NullString
|
||||
userPosition sql.NullString
|
||||
userLoginTime sql.NullString
|
||||
createdAt sql.NullString
|
||||
userFamilyFlag sql.NullString
|
||||
)
|
||||
|
||||
if err := rows.Scan(
|
||||
&id,
|
||||
&productName,
|
||||
&loginID,
|
||||
&employeeID,
|
||||
&loginVersion,
|
||||
&loginPublicIP,
|
||||
&loginLocalIP,
|
||||
&userCompany,
|
||||
&userDepartment,
|
||||
&userPosition,
|
||||
&userLoginTime,
|
||||
&createdAt,
|
||||
&userFamilyFlag,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !id.Valid {
|
||||
return nil, fmt.Errorf("row missing id")
|
||||
}
|
||||
|
||||
return []string{
|
||||
strconv.FormatInt(id.Int64, 10),
|
||||
nullToString(productName),
|
||||
nullToString(loginID),
|
||||
nullToString(employeeID),
|
||||
nullToString(loginVersion),
|
||||
nullToString(loginPublicIP),
|
||||
nullToString(loginLocalIP),
|
||||
nullToString(userCompany),
|
||||
nullToString(userDepartment),
|
||||
nullToString(userPosition),
|
||||
nullToString(userLoginTime),
|
||||
nullToString(createdAt),
|
||||
nullToString(userFamilyFlag),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func resolveTargetDate(raw string) (time.Time, error) {
|
||||
if raw == "" {
|
||||
now := time.Now().In(kst())
|
||||
yesterday := now.AddDate(0, 0, -1)
|
||||
return time.Date(yesterday.Year(), yesterday.Month(), yesterday.Day(), 0, 0, 0, 0, kst()), nil
|
||||
}
|
||||
t, err := time.ParseInLocation("2006-01-02", raw, kst())
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("invalid USER_PROGRAM_TARGET_DATE: %w", err)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func kst() *time.Location {
|
||||
loc, err := time.LoadLocation("Asia/Seoul")
|
||||
if err != nil {
|
||||
return time.FixedZone("KST", 9*60*60)
|
||||
}
|
||||
return loc
|
||||
}
|
||||
|
||||
func env(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func envRequired(key string) string {
|
||||
v := os.Getenv(key)
|
||||
if v == "" {
|
||||
log.Fatalf("%s is required", key)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func netAddr(host string, port int) string {
|
||||
return fmt.Sprintf("%s:%d", host, port)
|
||||
}
|
||||
|
||||
func nullToString(v sql.NullString) string {
|
||||
if v.Valid {
|
||||
return v.String
|
||||
}
|
||||
return ""
|
||||
}
|
||||
75
cmd/user_program_sync/main.go
Normal file
75
cmd/user_program_sync/main.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultUpdateDir = "/app/update_data"
|
||||
defaultLogDir = "/app/log"
|
||||
defaultSchema = "public"
|
||||
defaultTimeout = 15 * time.Minute
|
||||
)
|
||||
|
||||
func main() {
|
||||
logger := log.New(os.Stdout, "[sync] ", log.LstdFlags)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
updateDir := env("USER_PROGRAM_UPDATE_DIR", defaultUpdateDir)
|
||||
logDir := env("USER_PROGRAM_IMPORT_LOG_DIR", defaultLogDir)
|
||||
schema := env("USER_PROGRAM_INFO_SCHEMA", defaultSchema)
|
||||
|
||||
ensureDir(updateDir, logger)
|
||||
ensureDir(logDir, logger)
|
||||
|
||||
if err := runCmd(ctx, logger, "user-program-dump", map[string]string{
|
||||
"USER_PROGRAM_UPDATE_DIR": updateDir,
|
||||
}); err != nil {
|
||||
logger.Fatalf("dump failed: %v", err)
|
||||
}
|
||||
|
||||
if err := runCmd(ctx, logger, "user-program-import", map[string]string{
|
||||
"USER_PROGRAM_UPDATE_DIR": updateDir,
|
||||
"USER_PROGRAM_IMPORT_LOG_DIR": logDir,
|
||||
"USER_PROGRAM_INFO_SCHEMA": schema,
|
||||
}); err != nil {
|
||||
logger.Fatalf("import failed: %v", err)
|
||||
}
|
||||
|
||||
logger.Printf("sync completed (update_dir=%s, log_dir=%s, schema=%s)", updateDir, logDir, schema)
|
||||
}
|
||||
|
||||
func runCmd(ctx context.Context, logger *log.Logger, command string, extraEnv map[string]string) error {
|
||||
cmd := exec.CommandContext(ctx, command)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = os.Environ()
|
||||
for k, v := range extraEnv {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
cmd.Env = append(cmd.Env, k+"="+v)
|
||||
}
|
||||
logger.Printf("running %s", filepath.Base(command))
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func ensureDir(path string, logger *log.Logger) {
|
||||
if err := os.MkdirAll(path, 0o755); err != nil {
|
||||
logger.Fatalf("failed to create dir %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func env(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
Reference in New Issue
Block a user