DB 시간포멧 수정. access log 추가
This commit is contained in:
@@ -3,13 +3,18 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||||
|
|
||||||
"geoip-rest/internal/geo"
|
"geoip-rest/internal/geo"
|
||||||
"geoip-rest/internal/schedule"
|
"geoip-rest/internal/schedule"
|
||||||
@@ -45,6 +50,8 @@ func main() {
|
|||||||
ReadBufferSize: 16 * 1024, // allow larger request headers (e.g., proxy cookies)
|
ReadBufferSize: 16 * 1024, // allow larger request headers (e.g., proxy cookies)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.Use(newFileLogger(env("ACCESS_LOG_PATH", "/log/api-access.log")))
|
||||||
|
|
||||||
app.Get("/", func(c *fiber.Ctx) error {
|
app.Get("/", func(c *fiber.Ctx) error {
|
||||||
return c.JSON(fiber.Map{
|
return c.JSON(fiber.Map{
|
||||||
"service": "geoip-rest",
|
"service": "geoip-rest",
|
||||||
@@ -107,6 +114,86 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newFileLogger(path string) fiber.Handler {
|
||||||
|
if path == "" {
|
||||||
|
return func(c *fiber.Ctx) error { return c.Next() }
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
log.Printf("access log disabled (mkdir failed: %v)", err)
|
||||||
|
return func(c *fiber.Ctx) error { return c.Next() }
|
||||||
|
}
|
||||||
|
maxBytes := int64(envInt("ACCESS_LOG_MAX_BYTES", 10*1024*1024))
|
||||||
|
writer, err := newRotatingWriter(path, maxBytes)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("access log disabled (open failed: %v)", err)
|
||||||
|
return func(c *fiber.Ctx) error { return c.Next() }
|
||||||
|
}
|
||||||
|
|
||||||
|
format := "${time} ${ip} ${method} ${path} ${protocol} ${status} ${latency_human} headers=${reqHeaders}\n"
|
||||||
|
cfg := logger.Config{
|
||||||
|
Format: format,
|
||||||
|
TimeFormat: time.RFC3339,
|
||||||
|
TimeZone: "Asia/Seoul",
|
||||||
|
Output: writer,
|
||||||
|
}
|
||||||
|
return logger.New(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
type rotatingWriter struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
path string
|
||||||
|
maxBytes int64
|
||||||
|
file *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRotatingWriter(path string, maxBytes int64) (*rotatingWriter, error) {
|
||||||
|
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &rotatingWriter{
|
||||||
|
path: path,
|
||||||
|
maxBytes: maxBytes,
|
||||||
|
file: f,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *rotatingWriter) Write(p []byte) (int, error) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
if err := w.rotateIfNeeded(len(p)); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return w.file.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *rotatingWriter) rotateIfNeeded(incoming int) error {
|
||||||
|
info, err := w.file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if info.Size()+int64(incoming) <= w.maxBytes {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_ = w.file.Close()
|
||||||
|
|
||||||
|
ts := time.Now().Format("20060102-150405")
|
||||||
|
rotated := fmt.Sprintf("%s.%s", w.path, ts)
|
||||||
|
if err := os.Rename(w.path, rotated); err != nil {
|
||||||
|
// attempt to reopen original to keep logging
|
||||||
|
w.file, _ = os.OpenFile(w.path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.OpenFile(w.path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w.file = f
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func env(key, fallback string) string {
|
func env(key, fallback string) string {
|
||||||
if val := os.Getenv(key); val != "" {
|
if val := os.Getenv(key); val != "" {
|
||||||
return val
|
return val
|
||||||
@@ -129,6 +216,18 @@ func envBool(key string, fallback bool) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func envInt(key string, fallback int) int {
|
||||||
|
val := os.Getenv(key)
|
||||||
|
if val == "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
parsed, err := strconv.Atoi(val)
|
||||||
|
if err != nil {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
|
||||||
func sanitizeDBURL(raw string) string {
|
func sanitizeDBURL(raw string) string {
|
||||||
u, err := url.Parse(raw)
|
u, err := url.Parse(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func main() {
|
|||||||
|
|
||||||
csvPath := env("USER_PROGRAM_INFO_CSV", defaultCSVPath)
|
csvPath := env("USER_PROGRAM_INFO_CSV", defaultCSVPath)
|
||||||
updateDir := env("USER_PROGRAM_UPDATE_DIR", defaultUpdateDir)
|
updateDir := env("USER_PROGRAM_UPDATE_DIR", defaultUpdateDir)
|
||||||
schema := env("USER_PROGRAM_INFO_SCHEMA", defaultSchema)
|
schema := env("USER_PROGRAM_INFO_SCHEMA", env("POSTGRES_SCHEMA", defaultSchema))
|
||||||
logDir := env("USER_PROGRAM_IMPORT_LOG_DIR", defaultLogDir)
|
logDir := env("USER_PROGRAM_IMPORT_LOG_DIR", defaultLogDir)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ var (
|
|||||||
timeLayouts = []string{
|
timeLayouts = []string{
|
||||||
"2006-01-02 15:04:05.000",
|
"2006-01-02 15:04:05.000",
|
||||||
"2006-01-02 15:04:05",
|
"2006-01-02 15:04:05",
|
||||||
|
time.RFC3339,
|
||||||
|
"2006-01-02T15:04:05.000Z07:00",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -63,6 +65,10 @@ func EnsureUserProgramReplica(ctx context.Context, conn *pgx.Conn, csvPath, sche
|
|||||||
logDir = "log"
|
logDir = "log"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := ensureSchema(ctx, conn, schema); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := createReplicaTable(ctx, conn, schema, ReplicaTable); err != nil {
|
if err := createReplicaTable(ctx, conn, schema, ReplicaTable); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -143,6 +149,14 @@ CREATE TABLE IF NOT EXISTS %s (
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ensureSchema(ctx context.Context, conn *pgx.Conn, schema string) error {
|
||||||
|
if schema == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := conn.Exec(ctx, fmt.Sprintf(`CREATE SCHEMA IF NOT EXISTS %s`, pgx.Identifier{schema}.Sanitize()))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
type importResult struct {
|
type importResult struct {
|
||||||
rowsCopied int64
|
rowsCopied int64
|
||||||
rowsUpserted int64
|
rowsUpserted int64
|
||||||
|
|||||||
@@ -69,11 +69,12 @@ func NewMySQLConfigFromEnv() (MySQLConfig, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewPathsFromEnv() (Paths, error) {
|
func NewPathsFromEnv() (Paths, error) {
|
||||||
|
schema := env("USER_PROGRAM_INFO_SCHEMA", env("POSTGRES_SCHEMA", DefaultSchema))
|
||||||
paths := Paths{
|
paths := Paths{
|
||||||
UpdateDir: env("USER_PROGRAM_UPDATE_DIR", DefaultUpdateDir),
|
UpdateDir: env("USER_PROGRAM_UPDATE_DIR", DefaultUpdateDir),
|
||||||
LogDir: env("USER_PROGRAM_IMPORT_LOG_DIR", DefaultLogDir),
|
LogDir: env("USER_PROGRAM_IMPORT_LOG_DIR", DefaultLogDir),
|
||||||
InitialCSV: env("USER_PROGRAM_INFO_CSV", DefaultInitialCSV),
|
InitialCSV: env("USER_PROGRAM_INFO_CSV", DefaultInitialCSV),
|
||||||
Schema: env("USER_PROGRAM_INFO_SCHEMA", DefaultSchema),
|
Schema: schema,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dir := range []string{paths.UpdateDir, paths.LogDir} {
|
for _, dir := range []string{paths.UpdateDir, paths.LogDir} {
|
||||||
|
|||||||
@@ -188,8 +188,8 @@ func scanRow(rows *sql.Rows) ([]string, error) {
|
|||||||
userCompany sql.NullString
|
userCompany sql.NullString
|
||||||
userDepartment sql.NullString
|
userDepartment sql.NullString
|
||||||
userPosition sql.NullString
|
userPosition sql.NullString
|
||||||
userLoginTime sql.NullString
|
userLoginTime sql.NullTime
|
||||||
createdAt sql.NullString
|
createdAt sql.NullTime
|
||||||
userFamilyFlag sql.NullString
|
userFamilyFlag sql.NullString
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -225,8 +225,8 @@ func scanRow(rows *sql.Rows) ([]string, error) {
|
|||||||
nullToString(userCompany),
|
nullToString(userCompany),
|
||||||
nullToString(userDepartment),
|
nullToString(userDepartment),
|
||||||
nullToString(userPosition),
|
nullToString(userPosition),
|
||||||
nullToString(userLoginTime),
|
formatTimestamp(userLoginTime),
|
||||||
nullToString(createdAt),
|
formatTimestamp(createdAt),
|
||||||
nullToString(userFamilyFlag),
|
nullToString(userFamilyFlag),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -241,3 +241,10 @@ func nullToString(v sql.NullString) string {
|
|||||||
func netAddr(host string, port int) string {
|
func netAddr(host string, port int) string {
|
||||||
return fmt.Sprintf("%s:%d", host, port)
|
return fmt.Sprintf("%s:%d", host, port)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func formatTimestamp(t sql.NullTime) string {
|
||||||
|
if !t.Valid {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return t.Time.In(kst()).Format("2006-01-02 15:04:05.000")
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user