Feat: Sincronizador.
This commit is contained in:
commit
f81a1d5a81
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@ -0,0 +1,7 @@
|
||||
.git
|
||||
*.md
|
||||
*.zip
|
||||
*.log
|
||||
#*.env
|
||||
tmp/
|
||||
tests/
|
||||
40
.env
Normal file
40
.env
Normal file
@ -0,0 +1,40 @@
|
||||
#Configuración de la base de datos
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5434
|
||||
DB_USER=admin
|
||||
DB_PASSWORD=admin
|
||||
DB_NAME=gotestdb
|
||||
DB_MAX_OPEN_CONNS=25
|
||||
DB_MAX_IDLE_CONNS=10
|
||||
DB_CONN_MAX_LIFETIME=30m
|
||||
#Redis
|
||||
REDIS_HOST=10.0.0.112
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=TuPasswordSegura123
|
||||
REDIS_DB=0
|
||||
REDIS_TTL=10m
|
||||
REDIS_SUBSCRIBE=cron:reload
|
||||
#Log
|
||||
LOG_FILE_PATH=logs/syncronizador.log
|
||||
LOG_LEVEL=debug
|
||||
LOG_MAX_SIZE=10
|
||||
LOG_MAX_BACKUPS=7
|
||||
LOG_MAX_AGE=30
|
||||
LOG_COMPRESS=true
|
||||
|
||||
#Cliente Rest
|
||||
TIMEOUT_SECONDS= 30
|
||||
RETRY_COUNT=3
|
||||
TLS_SKIP_VERIFY=true
|
||||
LOG_REQUESTS=false
|
||||
ENABLE_DEBUG= false
|
||||
|
||||
WHERE_UNITS_BUSINESS=company_name = @company_name AND company_db = @company_db AND status = 'A'
|
||||
|
||||
ENVIRONMENT=development
|
||||
# Elastic
|
||||
#ELASTIC_URL=http://host.docker.internal:9200
|
||||
ELASTIC_URL=http://10.0.0.124:9200
|
||||
ELASTIC_ENABLED=true
|
||||
|
||||
ENCRYPTION_KEY=12345678901234567890123456789012
|
||||
167
.gitignore
vendored
Normal file
167
.gitignore
vendored
Normal file
@ -0,0 +1,167 @@
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/go,goland
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=go,goland
|
||||
|
||||
### Go ###
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
### GoLand ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# AWS User-specific
|
||||
.idea/**/aws.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# SonarLint plugin
|
||||
.idea/sonarlint/
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### GoLand Patch ###
|
||||
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
|
||||
|
||||
# *.iml
|
||||
# modules.xml
|
||||
# .idea/misc.xml
|
||||
# *.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
# https://plugins.jetbrains.com/plugin/7973-sonarlint
|
||||
.idea/**/sonarlint/
|
||||
|
||||
# SonarQube Plugin
|
||||
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
|
||||
.idea/**/sonarIssues.xml
|
||||
|
||||
# Markdown Navigator plugin
|
||||
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
|
||||
.idea/**/markdown-navigator.xml
|
||||
.idea/**/markdown-navigator-enh.xml
|
||||
.idea/**/markdown-navigator/
|
||||
|
||||
# Cache file creation bug
|
||||
# See https://youtrack.jetbrains.com/issue/JBR-2257
|
||||
.idea/$CACHE_FILE$
|
||||
|
||||
# CodeStream plugin
|
||||
# https://plugins.jetbrains.com/plugin/12206-codestream
|
||||
.idea/codestream.xml
|
||||
|
||||
# Azure Toolkit for IntelliJ plugin
|
||||
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
|
||||
.idea/**/azureSettings.xml
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/go,goland
|
||||
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
.ionide
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode
|
||||
|
||||
logs/rest_myapps_orbit.log
|
||||
30
Dockerfile
Normal file
30
Dockerfile
Normal file
@ -0,0 +1,30 @@
|
||||
# Stage 1: Compila el binario
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copiar módulos primero (cache)
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copiar todo y compilar
|
||||
COPY . .
|
||||
RUN go build -o go-sync-service ./cmd/go-sync-service
|
||||
|
||||
# Stage 2: Imagen final mínima
|
||||
FROM alpine:latest
|
||||
|
||||
WORKDIR /root/
|
||||
COPY --from=builder /app/go-sync-service .
|
||||
|
||||
COPY .env .env
|
||||
|
||||
|
||||
ENV TZ=America/La_Paz
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
# Puerto si lo usas
|
||||
EXPOSE 9100
|
||||
|
||||
# Ejecutar binario
|
||||
CMD ["./go-sync-service"]
|
||||
32
Dockerfile copy
Normal file
32
Dockerfile copy
Normal file
@ -0,0 +1,32 @@
|
||||
# ---------------------------------------------------
|
||||
# 1) Builder: compila API, worker y prepara goose
|
||||
# ---------------------------------------------------
|
||||
FROM golang:1.24 AS builder
|
||||
|
||||
|
||||
RUN apt-get update && apt-get install -y tzdata
|
||||
|
||||
ENV TZ=America/La_Paz
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
# Configurar el directorio de trabajo dentro del contenedor
|
||||
WORKDIR /app
|
||||
|
||||
# 1.1 Dependencias (caché)
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod tidy
|
||||
|
||||
# 1.2 Código fuente
|
||||
COPY . .
|
||||
|
||||
# Instalar OpenSSL (necesario para generar los certificados)
|
||||
#RUN apt-get update && apt-get install -y openssl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copiar el script entrypoint a la carpeta /docker-entrypoint-init.d/ y dar permisos de ejecución
|
||||
#COPY scripts/entrypoint.sh /docker-entrypoint-init.d/entrypoint.sh
|
||||
#RUN chmod +x /docker-entrypoint-init.d/entrypoint.sh
|
||||
|
||||
# Configurar el entrypoint para que ejecute el script
|
||||
#ENTRYPOINT ["/bin/sh", "/docker-entrypoint-init.d/entrypoint.sh"]
|
||||
|
||||
# Comando que se ejecutará tras el entrypoint (por ejemplo, levantar la API)
|
||||
CMD ["go", "run", "cmd/go-sync-service/main.go"]
|
||||
198
README.md
Normal file
198
README.md
Normal file
@ -0,0 +1,198 @@
|
||||
\# 📦 go-sync-service
|
||||
|
||||
|
||||
|
||||
\*\*go-sync-service\*\* es un microservicio escrito en Go para la ejecución de tareas de sincronización basadas en cron, integrando Redis como sistema de control y configuración dinámica, y PostgreSQL mediante GORM como base de datos. Está diseñado con una arquitectura modular y desacoplada para facilitar su reutilización y escalabilidad.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
\## 🚀 Características
|
||||
|
||||
|
||||
|
||||
\- 🔁 Ejecución de tareas periódicas con `robfig/cron`
|
||||
|
||||
\- 📦 Carga de configuraciones de tareas desde Redis (configuración dinámica)
|
||||
|
||||
\- 🧩 Arquitectura limpia con separación por capas (`ports`, `dto`, `config`, `scheduler`)
|
||||
|
||||
\- 🛠️ Servicio desacoplado de sincronización con posibilidad de extensiones
|
||||
|
||||
\- 🗂️ Organización clara del código con submódulos reutilizables
|
||||
|
||||
\- 🗃️ Persistencia con PostgreSQL vía `gorm.io/gorm`
|
||||
|
||||
\- 🧠 Logger centralizado
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
\## 🛠️ Requisitos
|
||||
|
||||
|
||||
|
||||
\- Go 1.20+
|
||||
|
||||
\- Redis
|
||||
|
||||
\- PostgreSQL
|
||||
|
||||
\- Docker (opcional para despliegue)
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
\## 📁 Estructura del Proyecto
|
||||
|
||||
|
||||
|
||||
```
|
||||
|
||||
go-sync-service/
|
||||
|
||||
├── cmd/
|
||||
|
||||
│ └── go-sync-service/ # Punto de entrada: main.go
|
||||
|
||||
├── internal/
|
||||
|
||||
│ ├── config/ # Configuración de BD, Redis, logger
|
||||
|
||||
│ ├── scheduler/ # Orquestador de cron jobs
|
||||
|
||||
│ ├── sync/ # Lógica de sincronización (fetchers)
|
||||
|
||||
│ ├── domain/
|
||||
|
||||
│ │ ├── dto/ # Estructuras de datos
|
||||
|
||||
│ │ └── ports/ # Interfaces de abstracción
|
||||
|
||||
├── README.md
|
||||
|
||||
└── go.mod / go.sum
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
\## ▶️ Cómo ejecutar
|
||||
|
||||
|
||||
|
||||
\### 1. Clonar el proyecto
|
||||
|
||||
|
||||
|
||||
```bash
|
||||
|
||||
git clone https://github.com/tuusuario/go-sync-service.git
|
||||
|
||||
cd go-sync-service
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
\### 2. Configurar variables de entorno
|
||||
|
||||
|
||||
|
||||
Crear un archivo `.env` o configurar los valores necesarios:
|
||||
|
||||
|
||||
|
||||
```env
|
||||
|
||||
DB\_HOST=localhost
|
||||
|
||||
DB\_PORT=5432
|
||||
|
||||
DB\_NAME=sync\_db
|
||||
|
||||
DB\_USER=postgres
|
||||
|
||||
DB\_PASS=secret
|
||||
|
||||
|
||||
|
||||
REDIS\_ADDR=localhost:6379
|
||||
|
||||
REDIS\_PASS=
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
\### 3. Ejecutar el servicio
|
||||
|
||||
|
||||
|
||||
```bash
|
||||
|
||||
go run cmd/go-sync-service/main.go
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
\## 🧪 Pruebas
|
||||
|
||||
|
||||
|
||||
Puedes lanzar las pruebas (si las implementas) con:
|
||||
|
||||
|
||||
|
||||
```bash
|
||||
|
||||
go test ./...
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
\## 📌 Notas técnicas
|
||||
|
||||
|
||||
|
||||
\- Las configuraciones de cron (`CronConfigList`) se cargan desde Redis mediante un esquema de deserialización JSON.
|
||||
|
||||
\- El archivo `internal/scheduler/manager.go` coordina la carga y reinicio dinámico de tareas cuando hay cambios en la configuración Redis.
|
||||
|
||||
\- La interfaz `RedisConfigProvider` permite desacoplar la fuente de datos de configuración, haciéndolo fácilmente intercambiable.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
\## 🧑💻 Autor
|
||||
|
||||
|
||||
|
||||
Desarrollado por \[tuusuario]
|
||||
|
||||
68
cmd/go-sync-service/main.go
Normal file
68
cmd/go-sync-service/main.go
Normal file
@ -0,0 +1,68 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/tuusuario/go-sync-service/internal/config"
|
||||
"github.com/tuusuario/go-sync-service/internal/scheduler"
|
||||
"github.com/tuusuario/go-sync-service/metrics"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Cargar configuración
|
||||
conf, err := config.LoadConfig()
|
||||
if err != nil {
|
||||
fmt.Println("❌ Error cargando configuración:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Inicializar logger con configuración
|
||||
config.InitLogger(conf)
|
||||
config.Log.Info("🚀 Iniciando servicio: go-sync-service")
|
||||
|
||||
// Conexión a Redis
|
||||
redisClient := config.GetRedisClient(conf)
|
||||
if err := redisClient.Ping(context.Background()).Err(); err != nil {
|
||||
config.Log.Fatalf("❌ Redis no disponible: %v", err)
|
||||
}
|
||||
config.Log.Info("✅ Redis conectado")
|
||||
|
||||
// Crear proveedor de configuración desde Redis
|
||||
redisManager := config.NewRedisManager(redisClient)
|
||||
redisConfigProvider := config.NewRedisProvider(redisManager, context.Background())
|
||||
|
||||
// Conexión a Base de Datos
|
||||
database := config.GetDatabaseConnection(conf)
|
||||
if database == nil {
|
||||
config.Log.Fatal("❌ No se pudo establecer la conexión con la base de datos.")
|
||||
}
|
||||
config.Log.Info("✅ Conexión a base de datos establecida")
|
||||
|
||||
scheduler.Start(context.Background(), redisClient, redisConfigProvider, database)
|
||||
config.Log.Info("✅ Scheduler en ejecución y escuchando recargas")
|
||||
|
||||
//metrics Grafana
|
||||
metrics.Register()
|
||||
startMetricsServer()
|
||||
|
||||
// Esperar señal del sistema para cerrar ordenadamente
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||||
<-stop
|
||||
|
||||
config.Log.Info("🛑 Señal de apagado recibida, cerrando servicio...")
|
||||
}
|
||||
|
||||
func startMetricsServer() {
|
||||
go func() {
|
||||
http.Handle("/metrics", promhttp.Handler())
|
||||
config.Log.Info("📊 Servidor de métricas en :9100/metrics")
|
||||
http.ListenAndServe(":9100", nil)
|
||||
}()
|
||||
}
|
||||
57
elastic/elastic_hook.go
Normal file
57
elastic/elastic_hook.go
Normal file
@ -0,0 +1,57 @@
|
||||
package elastic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/elastic/go-elasticsearch"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ElasticHook struct {
|
||||
Client *elasticsearch.Client
|
||||
Index string
|
||||
}
|
||||
|
||||
func NewElasticHook(client *elasticsearch.Client, index string) *ElasticHook {
|
||||
return &ElasticHook{
|
||||
Client: client,
|
||||
Index: index,
|
||||
}
|
||||
}
|
||||
|
||||
func (hook *ElasticHook) Fire(entry *logrus.Entry) error {
|
||||
doc := map[string]interface{}{
|
||||
"@timestamp": entry.Time.Format(time.RFC3339),
|
||||
"timestamp": entry.Time.Format(time.RFC3339),
|
||||
"level": entry.Level.String(),
|
||||
"message": entry.Message,
|
||||
"module": func() interface{} {
|
||||
if val, ok := entry.Data["module"]; ok {
|
||||
return val
|
||||
}
|
||||
return "default"
|
||||
}(),
|
||||
"file": entry.Caller.File,
|
||||
"line": entry.Caller.Line,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := json.NewEncoder(&buf).Encode(doc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := hook.Client.Index(
|
||||
hook.Index,
|
||||
&buf,
|
||||
hook.Client.Index.WithContext(context.Background()),
|
||||
hook.Client.Index.WithRefresh("true"),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (hook *ElasticHook) Levels() []logrus.Level {
|
||||
return logrus.AllLevels
|
||||
}
|
||||
60
go.mod
Normal file
60
go.mod
Normal file
@ -0,0 +1,60 @@
|
||||
module github.com/tuusuario/go-sync-service
|
||||
|
||||
go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/go-resty/resty/v2 v2.16.5
|
||||
github.com/jackc/pgx/v5 v5.7.5 // indirect
|
||||
github.com/redis/go-redis/v9 v9.10.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.12.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gorm.io/gorm v1.30.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/viper v1.20.1
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
)
|
||||
|
||||
require github.com/elastic/go-elasticsearch v0.0.0
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
)
|
||||
119
go.sum
Normal file
119
go.sum
Normal file
@ -0,0 +1,119 @@
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/elastic/go-elasticsearch v0.0.0 h1:Pd5fqOuBxKxv83b0+xOAJDAkziWYwFinWnBO0y+TZaA=
|
||||
github.com/elastic/go-elasticsearch v0.0.0/go.mod h1:TkBSJBuTyFdBnrNqoPc54FN0vKf5c04IdM4zuStJ7xg=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
|
||||
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
||||
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs=
|
||||
github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
||||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
75
internal/config/config.go
Normal file
75
internal/config/config.go
Normal file
@ -0,0 +1,75 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Config almacena las configuraciones globales
|
||||
type Config struct {
|
||||
DBHost string `mapstructure:"DB_HOST"`
|
||||
DBPort int `mapstructure:"DB_PORT"`
|
||||
DBUser string `mapstructure:"DB_USER"`
|
||||
DBPassword string `mapstructure:"DB_PASSWORD"`
|
||||
DBName string `mapstructure:"DB_NAME"`
|
||||
DBMaxOpenConns int `mapstructure:"DB_MAX_OPEN_CONNS"`
|
||||
DBMaxIdleConns int `mapstructure:"DB_MAX_IDLE_CONNS"`
|
||||
DBConnMaxLifetime time.Duration `mapstructure:"DB_CONN_MAX_LIFETIME"`
|
||||
|
||||
// Redis
|
||||
RedisHost string `mapstructure:"REDIS_HOST"`
|
||||
RedisPort int `mapstructure:"REDIS_PORT"`
|
||||
RedisPassword string `mapstructure:"REDIS_PASSWORD"`
|
||||
RedisDB int `mapstructure:"REDIS_DB"`
|
||||
RedisTTL time.Duration `mapstructure:"REDIS_TTL"` // tiempo de vida por defecto de claves
|
||||
RedisSubscribe string `mapstructure:"REDIS_SUBSCRIBE"`
|
||||
|
||||
LogFilePath string `mapstructure:"LOG_FILE_PATH"`
|
||||
LogLevel string `mapstructure:"LOG_LEVEL"`
|
||||
LogMaxSize int `mapstructure:"LOG_MAX_SIZE"`
|
||||
LogMaxBackups int `mapstructure:"LOG_MAX_BACKUPS"`
|
||||
LogMaxAge int `mapstructure:"LOG_MAX_AGE"`
|
||||
LogCompress bool `mapstructure:"LOG_COMPRESS"`
|
||||
Environment string `mapstructure:"ENVIRONMENT"`
|
||||
|
||||
WhereUnitsBusiness string `mapstructure:"WHERE_UNITS_BUSINESS"`
|
||||
ElasticURL string `mapstructure:"ELASTIC_URL"`
|
||||
ElasticEnabled bool `mapstructure:"ELASTIC_ENABLED"`
|
||||
|
||||
TimeoutSeconds int `mapstructure:"TIMEOUT_SECONDS"`
|
||||
RetryCount int `mapstructure:"RETRY_COUNT"`
|
||||
TLSSkipVerify bool `mapstructure:"TLS_SKIP_VERIFY"`
|
||||
LogRequests bool `mapstructure:"LOG_REQUESTS"`
|
||||
EnableDebug bool `mapstructure:"ENABLE_DEBUG"`
|
||||
}
|
||||
|
||||
var GlobalConfig *Config
|
||||
|
||||
// LoadConfig carga las variables de entorno usando viper
|
||||
func LoadConfig() (*Config, error) {
|
||||
viper.SetConfigName(".env")
|
||||
viper.SetConfigType("env")
|
||||
viper.AddConfigPath(".")
|
||||
viper.AutomaticEnv()
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
fmt.Println("⚠️ No se pudo leer el archivo .env, usando variables de entorno del sistema")
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := viper.Unmarshal(&config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
GlobalConfig = &config
|
||||
|
||||
// --- 👇 **clave**: exportar ENCRYPTION_KEY al entorno del proceso ---
|
||||
if k := viper.GetString("ENCRYPTION_KEY"); k != "" {
|
||||
_ = os.Setenv("ENCRYPTION_KEY", k)
|
||||
}
|
||||
|
||||
fmt.Println("✅ Configuración .env cargada correctamente.")
|
||||
return &config, nil
|
||||
}
|
||||
63
internal/config/database.go
Normal file
63
internal/config/database.go
Normal file
@ -0,0 +1,63 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
db *gorm.DB
|
||||
once sync.Once
|
||||
dbErr error
|
||||
)
|
||||
|
||||
// GetDB retorna la conexión a la base de datos
|
||||
func GetDatabaseConnection(cfg *Config) *gorm.DB {
|
||||
once.Do(func() {
|
||||
dsn := fmt.Sprintf(
|
||||
"host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
|
||||
cfg.DBHost, cfg.DBPort, cfg.DBUser, cfg.DBPassword, cfg.DBName,
|
||||
)
|
||||
|
||||
// Reintentos automáticos
|
||||
maxRetries := 3
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
db, dbErr = gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if dbErr == nil {
|
||||
break
|
||||
}
|
||||
log.Printf("⚠️ Intento %d de conexión fallido: %v", i+1, dbErr)
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
if dbErr != nil {
|
||||
log.Fatalf("❌ No se pudo conectar a la BD después de varios intentos: %v", dbErr)
|
||||
}
|
||||
|
||||
// Configuración del pool
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
log.Fatalf("❌ Error al obtener objeto SQL: %v", err)
|
||||
}
|
||||
|
||||
// Validación inmediata
|
||||
if err := sqlDB.Ping(); err != nil {
|
||||
log.Fatalf("❌ Error al hacer ping a la BD: %v", err)
|
||||
}
|
||||
|
||||
sqlDB.SetMaxOpenConns(cfg.DBMaxOpenConns) // Conexiones máximas
|
||||
sqlDB.SetMaxIdleConns(cfg.DBMaxIdleConns) // Conexiones inactivas permitidas
|
||||
sqlDB.SetConnMaxLifetime(cfg.DBConnMaxLifetime * time.Minute) // Tiempo máximo de vida
|
||||
|
||||
log.Println("✅ Conexión a la BD establecida correctamente")
|
||||
})
|
||||
|
||||
return db
|
||||
}
|
||||
7
internal/config/keys.go
Normal file
7
internal/config/keys.go
Normal file
@ -0,0 +1,7 @@
|
||||
package config
|
||||
|
||||
const (
|
||||
RestConfig = "parametros:rest_config"
|
||||
RestLogin = "parametros:rest_login"
|
||||
CronConfig = "parametros:cron_config"
|
||||
)
|
||||
70
internal/config/logger.go
Normal file
70
internal/config/logger.go
Normal file
@ -0,0 +1,70 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/elastic/go-elasticsearch"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/tuusuario/go-sync-service/elastic"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
// Log instancia global del logger
|
||||
var Log = logrus.New()
|
||||
|
||||
func InitLogger(cfg *Config) {
|
||||
|
||||
// Configurar rotación de logs con Lumberjack
|
||||
rotator := &lumberjack.Logger{
|
||||
Filename: cfg.LogFilePath, // Archivo de logs
|
||||
MaxSize: cfg.LogMaxSize, // Máximo tamaño en MB antes de rotar
|
||||
MaxBackups: cfg.LogMaxBackups, // Máximo número de archivos de respaldo
|
||||
MaxAge: cfg.LogMaxAge, // Máximo número de días para conservar logs
|
||||
Compress: cfg.LogCompress, // Comprimir logs antiguos
|
||||
}
|
||||
|
||||
// Configurar Logrus para escribir en el archivo rotado
|
||||
Log.SetOutput(io.MultiWriter(os.Stdout, rotator))
|
||||
Log.SetReportCaller(true) // 👈 esto agrega archivo y línea
|
||||
|
||||
// Formato JSON con timestamp
|
||||
/*Log.SetFormatter(&logrus.JSONFormatter{
|
||||
TimestampFormat: time.RFC3339, // Formato ISO 8601
|
||||
})*/
|
||||
|
||||
Log.SetFormatter(&logrus.JSONFormatter{
|
||||
TimestampFormat: time.RFC3339,
|
||||
CallerPrettyfier: func(f *runtime.Frame) (function string, file string) {
|
||||
// Extrae solo nombre del archivo y línea
|
||||
fnParts := strings.Split(f.Function, "/")
|
||||
return fnParts[len(fnParts)-1], fmt.Sprintf("%s:%d", filepath.Base(f.File), f.Line)
|
||||
},
|
||||
})
|
||||
|
||||
// Configurar nivel de logging (DEBUG, INFO, ERROR, etc.)
|
||||
level, err := logrus.ParseLevel(cfg.LogLevel)
|
||||
if err != nil {
|
||||
level = logrus.InfoLevel
|
||||
}
|
||||
Log.SetLevel(level)
|
||||
|
||||
//elastic
|
||||
if cfg.ElasticEnabled {
|
||||
Log.Debug("✅ Elasticsearch enabled")
|
||||
es, err := elasticsearch.NewClient(elasticsearch.Config{
|
||||
Addresses: []string{cfg.ElasticURL},
|
||||
})
|
||||
if err == nil {
|
||||
hook := elastic.NewElasticHook(es, "go-sync-service")
|
||||
Log.AddHook(hook)
|
||||
} else {
|
||||
Log.Error("No se pudo conectar a Elasticsearch: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
30
internal/config/redis.go
Normal file
30
internal/config/redis.go
Normal file
@ -0,0 +1,30 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func GetRedisClient(cfg *Config) *redis.Client {
|
||||
addr := fmt.Sprintf("%s:%d", cfg.RedisHost, cfg.RedisPort)
|
||||
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: addr,
|
||||
Password: cfg.RedisPassword,
|
||||
DB: cfg.RedisDB,
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := client.Ping(ctx).Result()
|
||||
if err != nil {
|
||||
log.Fatalf("❌ Redis connection failed: %v", err)
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
40
internal/config/redis_manager.go
Normal file
40
internal/config/redis_manager.go
Normal file
@ -0,0 +1,40 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type RedisManager struct {
|
||||
redis *redis.Client
|
||||
}
|
||||
|
||||
func NewRedisManager(redis *redis.Client) *RedisManager {
|
||||
return &RedisManager{
|
||||
redis: redis,
|
||||
}
|
||||
}
|
||||
|
||||
func (pm *RedisManager) GetRawValue(ctx context.Context, codigo string) (string, error) {
|
||||
|
||||
val, err := pm.redis.Get(ctx, codigo).Result()
|
||||
if err == redis.Nil {
|
||||
return "", fmt.Errorf("clave no encontrada en Redis: %s", codigo)
|
||||
} else if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func (pm *RedisManager) UpdateParam(ctx context.Context, codigo string, nuevoValor string, expiration time.Duration) error {
|
||||
err := pm.redis.Set(ctx, codigo, nuevoValor, expiration).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error actualizando parámetro en Redis [%s]: %w", codigo, err)
|
||||
}
|
||||
log.Printf("🔄 Parámetro actualizado en Redis: %s = %s", codigo, nuevoValor)
|
||||
return nil
|
||||
}
|
||||
37
internal/config/redis_provider.go
Normal file
37
internal/config/redis_provider.go
Normal file
@ -0,0 +1,37 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/tuusuario/go-sync-service/internal/domain/ports"
|
||||
)
|
||||
|
||||
type RedisProvider struct {
|
||||
ParamManager *RedisManager
|
||||
Ctx context.Context
|
||||
}
|
||||
|
||||
// UpdateParamInRedis implements ports.RedisConfigProvider.
|
||||
func (p *RedisProvider) UpdateParam(key string, value string, expiration time.Duration) error {
|
||||
return p.ParamManager.UpdateParam(p.Ctx, key, value, expiration)
|
||||
}
|
||||
|
||||
func NewRedisProvider(pm *RedisManager, ctx context.Context) ports.RedisConfigProvider {
|
||||
return &RedisProvider{ParamManager: pm, Ctx: ctx}
|
||||
}
|
||||
|
||||
func (p *RedisProvider) GetString(key string) (string, error) {
|
||||
return p.ParamManager.GetRawValue(p.Ctx, key)
|
||||
}
|
||||
|
||||
// GetInt64 retrieves an int64 configuration value from Redis, given its key.
|
||||
// It returns 0 and an error if the key is not found in Redis.
|
||||
func (p *RedisProvider) GetInt64(key string) (int64, error) {
|
||||
val, err := p.ParamManager.GetRawValue(p.Ctx, key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return strconv.ParseInt(val, 10, 64)
|
||||
}
|
||||
379
internal/db/operaciones.go
Normal file
379
internal/db/operaciones.go
Normal file
@ -0,0 +1,379 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tuusuario/go-sync-service/internal/config"
|
||||
"github.com/tuusuario/go-sync-service/internal/domain/dto"
|
||||
"github.com/tuusuario/go-sync-service/internal/domain/model"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type GormDatabase struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewGormDatabase(db *gorm.DB) *GormDatabase {
|
||||
return &GormDatabase{db: db}
|
||||
}
|
||||
|
||||
func (g *GormDatabase) SyncRows(persistencia dto.Persistencia, rawData *[]map[string]interface{}, company_db string) error {
|
||||
logPrefix := fmt.Sprintf("[🧹 Tabla: %s] ", persistencia.Table)
|
||||
config.Log.Println(logPrefix + " ✅ Inicializacion Syncing data...")
|
||||
|
||||
now := time.Now()
|
||||
batchSize := persistencia.BatchSize
|
||||
if batchSize <= 0 {
|
||||
batchSize = 100
|
||||
}
|
||||
|
||||
batch := make([]map[string]interface{}, 0, batchSize)
|
||||
|
||||
for i, item := range *rawData {
|
||||
record := map[string]interface{}{
|
||||
persistencia.CampoSync: now,
|
||||
}
|
||||
|
||||
// Copia de campos mapeados (con soporte a "a.b.c", "array[0].campo", "array[-1].campo", "array[].campo")
|
||||
for column, jsonPath := range persistencia.Fields {
|
||||
val := getNestedValue(item, jsonPath)
|
||||
record[column] = val
|
||||
}
|
||||
|
||||
// Campos estáticos
|
||||
for k, v := range persistencia.StaticFields {
|
||||
record[k] = v
|
||||
}
|
||||
|
||||
// Forzar columnas a string si fue configurado
|
||||
normalizeTypes(record, persistencia.StringifyFields)
|
||||
|
||||
// ========== NUEVO: construir PK compuesta si está configurada ==========
|
||||
if persistencia.PrimaryKeyName != "" && len(persistencia.PrimaryKeyConcat) > 0 {
|
||||
if key, ok := buildCompositeKey(record, company_db, persistencia.PrimaryKeyConcat, persistencia.PrimaryKeySeparator); ok {
|
||||
record[persistencia.PrimaryKeyName] = key
|
||||
}
|
||||
}
|
||||
|
||||
batch = append(batch, record)
|
||||
|
||||
// Procesar lote
|
||||
if len(batch) == batchSize || i == len(*rawData)-1 {
|
||||
config.Log.Debugf(logPrefix+" Procesando batch de %d registros", len(batch))
|
||||
|
||||
if len(persistencia.UpdateBy) > 0 {
|
||||
// Updates con múltiples campos
|
||||
for _, row := range batch {
|
||||
var whereParts []string
|
||||
var whereValues []interface{}
|
||||
|
||||
for campoTabla, campoServicio := range persistencia.UpdateBy {
|
||||
val, ok := row[campoServicio]
|
||||
if !ok || val == nil {
|
||||
continue
|
||||
}
|
||||
whereParts = append(whereParts, fmt.Sprintf("%s = ?", campoTabla))
|
||||
whereValues = append(whereValues, val)
|
||||
}
|
||||
if len(whereParts) < len(persistencia.UpdateBy) {
|
||||
config.Log.Warnf("⚠️ Registro incompleto para update (faltan claves): %+v", row)
|
||||
continue
|
||||
}
|
||||
|
||||
// Copia sin campos clave
|
||||
updateData := make(map[string]interface{})
|
||||
for k, v := range row {
|
||||
skip := false
|
||||
for campoTabla := range persistencia.UpdateBy {
|
||||
if k == campoTabla {
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !skip {
|
||||
updateData[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
res := g.db.Table(persistencia.Table).
|
||||
Where(strings.Join(whereParts, " AND "), whereValues...).
|
||||
Updates(updateData)
|
||||
|
||||
if res.Error != nil {
|
||||
config.Log.Errorf("%s ❌ Error en update: %v", logPrefix, res.Error)
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
config.Log.Warnf("%s ⚠️ Ninguna fila afectada con campos: %v valores: %v",
|
||||
logPrefix, strings.Join(whereParts, " AND "), printWhereValues(whereValues))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Inserts con conflicto por PK (UPSERT)
|
||||
cols := unionColumnList(batch) // union de columnas del lote
|
||||
|
||||
// Elegir PK: si hay compuesta (calculada), úsala; si no, usa PrimaryKey
|
||||
pkName := persistencia.PrimaryKey
|
||||
if persistencia.PrimaryKeyName != "" && len(persistencia.PrimaryKeyConcat) > 0 {
|
||||
pkName = persistencia.PrimaryKeyName
|
||||
}
|
||||
|
||||
err := g.db.Table(persistencia.Table).
|
||||
Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: pkName}},
|
||||
DoUpdates: clause.AssignmentColumns(cols),
|
||||
}).
|
||||
Create(&batch).Error
|
||||
|
||||
if err != nil {
|
||||
config.Log.Errorf("%s ❌ Error en batch insert: %v", logPrefix, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
batch = batch[:0]
|
||||
}
|
||||
}
|
||||
|
||||
if persistencia.Eliminacion.Enabled {
|
||||
err := g.db.Table(persistencia.Table).
|
||||
Where(persistencia.Eliminacion.Field+" < ?", now).
|
||||
Delete(nil).Error
|
||||
if err != nil {
|
||||
config.Log.Printf(logPrefix+"❌ Error eliminando obsoletos: %+v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
config.Log.Println(logPrefix + " ✅ Finalizacion Syncing data...")
|
||||
return nil
|
||||
}
|
||||
|
||||
func unionColumnList(batch []map[string]interface{}) []string {
|
||||
set := map[string]struct{}{}
|
||||
for _, r := range batch {
|
||||
for k := range r {
|
||||
set[k] = struct{}{}
|
||||
}
|
||||
}
|
||||
cols := make([]string, 0, len(set))
|
||||
for k := range set {
|
||||
cols = append(cols, k)
|
||||
}
|
||||
return cols
|
||||
}
|
||||
|
||||
func (g *GormDatabase) GetCredencialesFromTemplate(whereTemplate string, variables map[string]interface{}) (*model.CredencialesSAP, error) {
|
||||
var cred model.CredencialesSAP
|
||||
|
||||
query := whereTemplate
|
||||
var args []interface{}
|
||||
|
||||
config.Log.Debugf("🔎 Variables recibidas:")
|
||||
for k, v := range variables {
|
||||
placeholder := "@" + k
|
||||
query = strings.ReplaceAll(query, placeholder, "?")
|
||||
args = append(args, v)
|
||||
config.Log.Debugf(" %s = %v", k, v)
|
||||
}
|
||||
|
||||
config.Log.Debugf("📝 Consulta final construida:")
|
||||
config.Log.Debugf(" Query: %s", query)
|
||||
config.Log.Debugf(" Args: %v", args)
|
||||
|
||||
err := g.db.Where(query, args...).First(&cred).Error
|
||||
return &cred, err
|
||||
}
|
||||
|
||||
func printWhereValues(whereValues []interface{}) string {
|
||||
if len(whereValues) == 0 {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString(fmt.Sprintf("%v", whereValues[0]))
|
||||
for i := 1; i < len(whereValues); i++ {
|
||||
b.WriteString(", ")
|
||||
b.WriteString(fmt.Sprintf("%v", whereValues[i]))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// ===== Soporte de índices [n], [-1] y [] en rutas =====
|
||||
|
||||
var idxRe = regexp.MustCompile(`^([^\[\]]+)(?:\[(\-?\d*)\])?$`)
|
||||
|
||||
// toIfaceSlice intenta ver el valor como slice de interfaces
|
||||
func toIfaceSlice(v interface{}) ([]interface{}, bool) {
|
||||
switch s := v.(type) {
|
||||
case []interface{}:
|
||||
return s, true
|
||||
case []map[string]interface{}:
|
||||
out := make([]interface{}, len(s))
|
||||
for i := range s {
|
||||
out[i] = s[i]
|
||||
}
|
||||
return out, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
// getNestedValue con soporte para "contacts[0].field", "contacts[-1].field", "contacts[].field"
|
||||
func getNestedValue(data map[string]interface{}, path string) interface{} {
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(path, ".")
|
||||
var current interface{} = data
|
||||
|
||||
for _, part := range parts {
|
||||
// parsea nombre + índice opcional
|
||||
m := idxRe.FindStringSubmatch(part)
|
||||
if len(m) == 0 {
|
||||
return nil
|
||||
}
|
||||
key := m[1]
|
||||
idxStr := m[2] // "", "0", "1", "-1", ...
|
||||
|
||||
// paso de mapa (key)
|
||||
mp, ok := current.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var exists bool
|
||||
current, exists = mp[key]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
// si hay índice (o "[]"), tratar como slice
|
||||
if idxStr != "" || strings.HasSuffix(part, "[]") {
|
||||
arr, ok := toIfaceSlice(current)
|
||||
if !ok || len(arr) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var idx int
|
||||
if idxStr == "" {
|
||||
// "[]" -> primero
|
||||
idx = 0
|
||||
} else {
|
||||
n, err := strconv.Atoi(idxStr)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if n < 0 {
|
||||
n = len(arr) + n // -1 => último
|
||||
}
|
||||
idx = n
|
||||
}
|
||||
|
||||
if idx < 0 || idx >= len(arr) {
|
||||
return nil
|
||||
}
|
||||
current = arr[idx]
|
||||
}
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
// ===== NUEVO: helper para clave compuesta =====
|
||||
func buildCompositeKey(row map[string]interface{}, companyDB string, concat []string, sep string) (string, bool) {
|
||||
if len(concat) == 0 {
|
||||
return "", false
|
||||
}
|
||||
if sep == "" {
|
||||
sep = ":"
|
||||
}
|
||||
parts := make([]string, 0, len(concat))
|
||||
for _, item := range concat {
|
||||
if strings.HasPrefix(item, "@") { // tokens especiales
|
||||
token := strings.TrimPrefix(item, "@")
|
||||
switch {
|
||||
case token == "company_db":
|
||||
parts = append(parts, companyDB)
|
||||
case strings.HasPrefix(token, "literal="):
|
||||
parts = append(parts, strings.TrimPrefix(token, "literal="))
|
||||
default:
|
||||
// Token desconocido -> vacío (o puedes return false para forzar)
|
||||
parts = append(parts, "")
|
||||
}
|
||||
continue
|
||||
}
|
||||
// tomar del row (columna ya mapeada en record)
|
||||
parts = append(parts, toString(row[item]))
|
||||
}
|
||||
return strings.Join(parts, sep), true
|
||||
}
|
||||
|
||||
// ===== Normalización de tipos =====
|
||||
func normalizeTypes(m map[string]interface{}, stringifyFields []string) {
|
||||
if len(stringifyFields) == 0 {
|
||||
return
|
||||
}
|
||||
sf := make(map[string]struct{}, len(stringifyFields))
|
||||
for _, f := range stringifyFields {
|
||||
sf[f] = struct{}{}
|
||||
}
|
||||
for k, v := range m {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := sf[k]; ok {
|
||||
m[k] = toString(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Casteo genérico a string =====
|
||||
func toString(v interface{}) string {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
return t
|
||||
case fmt.Stringer:
|
||||
return t.String()
|
||||
case float64:
|
||||
if t == float64(int64(t)) {
|
||||
return fmt.Sprintf("%d", int64(t))
|
||||
}
|
||||
return fmt.Sprintf("%v", t)
|
||||
case float32:
|
||||
if t == float32(int64(t)) {
|
||||
return fmt.Sprintf("%d", int64(t))
|
||||
}
|
||||
return fmt.Sprintf("%v", t)
|
||||
case int:
|
||||
return fmt.Sprintf("%d", t)
|
||||
case int8:
|
||||
return fmt.Sprintf("%d", t)
|
||||
case int16:
|
||||
return fmt.Sprintf("%d", t)
|
||||
case int32:
|
||||
return fmt.Sprintf("%d", t)
|
||||
case int64:
|
||||
return fmt.Sprintf("%d", t)
|
||||
case uint:
|
||||
return fmt.Sprintf("%d", t)
|
||||
case uint8:
|
||||
return fmt.Sprintf("%d", t)
|
||||
case uint16:
|
||||
return fmt.Sprintf("%d", t)
|
||||
case uint32:
|
||||
return fmt.Sprintf("%d", t)
|
||||
case uint64:
|
||||
return fmt.Sprintf("%d", t)
|
||||
case bool:
|
||||
if t {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
case time.Time:
|
||||
return t.Format(time.RFC3339)
|
||||
default:
|
||||
return fmt.Sprintf("%v", t)
|
||||
}
|
||||
}
|
||||
22
internal/domain/dto/cron.go
Normal file
22
internal/domain/dto/cron.go
Normal file
@ -0,0 +1,22 @@
|
||||
package dto
|
||||
|
||||
type CronConfigList struct {
|
||||
Crons []CronJob `json:"crons"`
|
||||
}
|
||||
|
||||
type CronJob struct {
|
||||
Nombre string `json:"nombre"`
|
||||
UnidadNegocio UnidadNegocio `json:"unidad_negocio"`
|
||||
Configuracion ConfiguracionCron `json:"configuracion"`
|
||||
}
|
||||
|
||||
type UnidadNegocio struct {
|
||||
CompanyName string `json:"company_name"`
|
||||
CompanyDB string `json:"company_db"`
|
||||
}
|
||||
|
||||
type ConfiguracionCron struct {
|
||||
Ejecucion string `json:"ejecucion"` // Ej: "@every 2m"
|
||||
Proceso []string `json:"proceso"` // Lista de claves para otras configuraciones
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
69
internal/domain/dto/servicio.go
Normal file
69
internal/domain/dto/servicio.go
Normal file
@ -0,0 +1,69 @@
|
||||
package dto
|
||||
|
||||
type JobConfig struct {
|
||||
Auth ServiceConfig `json:"auth,omitempty"`
|
||||
Service ServiceConfig `json:"service,omitempty"`
|
||||
Persistencia Persistencia `json:"persistencia,omitempty"`
|
||||
}
|
||||
|
||||
type ServiceConfig struct {
|
||||
GQL bool `json:"gql,omitempty"`
|
||||
Method string `json:"method"`
|
||||
Path string `json:"path"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
Rest *RestOptions `json:"rest,omitempty"`
|
||||
GraphQL *GraphQLOptions `json:"graphql,omitempty"`
|
||||
}
|
||||
|
||||
type RestOptions struct {
|
||||
Body any `json:"body"`
|
||||
Query map[string]string `json:"query"`
|
||||
Pagination *RestPagination `json:"pagination,omitempty"`
|
||||
}
|
||||
|
||||
type RestPagination struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Skip int `json:"skip"`
|
||||
Top int `json:"top"`
|
||||
}
|
||||
|
||||
type GraphQLOptions struct {
|
||||
Query string `json:"query"`
|
||||
RootField string `json:"root_field"`
|
||||
RowField string `json:"row_field"`
|
||||
Variables map[string]interface{} `json:"variables"`
|
||||
Pagination *GraphQLPagination `json:"pagination"`
|
||||
}
|
||||
|
||||
type GraphQLPagination struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
CursorField string `json:"cursorField"`
|
||||
HasNextField string `json:"hasNextField"`
|
||||
LimitField string `json:"limitField"`
|
||||
CursorParam string `json:"cursorParam"`
|
||||
}
|
||||
|
||||
type Persistencia struct {
|
||||
Table string `json:"table"`
|
||||
BatchSize int `json:"batch_size"`
|
||||
CampoSync string `json:"campo_sync"`
|
||||
PrimaryKey string `json:"primary_key"`
|
||||
Fields map[string]string `json:"fields"`
|
||||
StaticFields map[string]interface{} `json:"static_fields"`
|
||||
UpdateBy map[string]string `json:"update_by"`
|
||||
Eliminacion struct {
|
||||
Field string `json:"field"`
|
||||
Enabled bool `json:"enabled"`
|
||||
} `json:"soft_delete"`
|
||||
StringifyFields []string `json:"stringify_fields"`
|
||||
|
||||
// 👇 NUEVO: clave compuesta precomputada
|
||||
PrimaryKeyName string `json:"primary_key_name"` // p.ej. "pk_redis"
|
||||
PrimaryKeyConcat []string `json:"primary_key_concat"` // p.ej. ["@company_db","card_code"]
|
||||
PrimaryKeySeparator string `json:"primary_key_separator"` // p.ej. ":"
|
||||
}
|
||||
|
||||
type Eliminacion struct {
|
||||
Field string `json:"field"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
9
internal/domain/dto/session.go
Normal file
9
internal/domain/dto/session.go
Normal file
@ -0,0 +1,9 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
|
||||
type SessionData struct {
|
||||
SessionId string `json:"session_id"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
EndPoint string `json:"end_point"`
|
||||
}
|
||||
24
internal/domain/model/credencial_sap.go
Normal file
24
internal/domain/model/credencial_sap.go
Normal file
@ -0,0 +1,24 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type CredencialesSAP struct {
|
||||
ID int `gorm:"column:id;primaryKey;autoIncrement"`
|
||||
SerieSAP string `gorm:"column:serie_sap"`
|
||||
CompanyName string `gorm:"column:company_name"`
|
||||
BranchID int `gorm:"column:branch_id"`
|
||||
CompanyDB string `gorm:"column:company_db"`
|
||||
UserName string `gorm:"column:user_name"`
|
||||
Password string `gorm:"column:password"` // ⚠️ Considera cifrar si es necesario
|
||||
Status string `gorm:"column:status"` // Ej: 'A' activo, 'I' inactivo
|
||||
|
||||
UserCreated string `gorm:"column:user_created"`
|
||||
DateCreated time.Time `gorm:"column:date_created"`
|
||||
UserUpdated string `gorm:"column:user_updated"`
|
||||
DateUpdated time.Time `gorm:"column:date_updated"`
|
||||
EndPoint string `gorm:"column:end_point"`
|
||||
}
|
||||
|
||||
func (CredencialesSAP) TableName() string {
|
||||
return "business_units"
|
||||
}
|
||||
12
internal/domain/ports/database_port.go
Normal file
12
internal/domain/ports/database_port.go
Normal file
@ -0,0 +1,12 @@
|
||||
package ports
|
||||
|
||||
import (
|
||||
"github.com/tuusuario/go-sync-service/internal/domain/dto"
|
||||
"github.com/tuusuario/go-sync-service/internal/domain/model"
|
||||
)
|
||||
|
||||
type Database interface {
|
||||
SyncRows(persistencia dto.Persistencia, data *[]map[string]interface{}, company_db string) error
|
||||
//GetByTemplate(filterKey string, variables map[string]interface{}) (any, error)
|
||||
GetCredencialesFromTemplate(whereTemplate string, variables map[string]interface{}) (*model.CredencialesSAP, error)
|
||||
}
|
||||
11
internal/domain/ports/redis_provider_port.go
Normal file
11
internal/domain/ports/redis_provider_port.go
Normal file
@ -0,0 +1,11 @@
|
||||
package ports
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type RedisConfigProvider interface {
|
||||
GetString(key string) (string, error)
|
||||
GetInt64(key string) (int64, error)
|
||||
UpdateParam(key string, value string, expiration time.Duration) error
|
||||
}
|
||||
7
internal/domain/ports/scheduler_port.go
Normal file
7
internal/domain/ports/scheduler_port.go
Normal file
@ -0,0 +1,7 @@
|
||||
package ports
|
||||
|
||||
type Scheduler interface {
|
||||
Start()
|
||||
Stop()
|
||||
Reload() error
|
||||
}
|
||||
5
internal/domain/ports/session_port.go
Normal file
5
internal/domain/ports/session_port.go
Normal file
@ -0,0 +1,5 @@
|
||||
package ports
|
||||
|
||||
type SessionProvider interface {
|
||||
GetSession(endpoint, user, password, company string) (string, error)
|
||||
}
|
||||
55
internal/http/client.go
Normal file
55
internal/http/client.go
Normal file
@ -0,0 +1,55 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/tuusuario/go-sync-service/internal/config"
|
||||
)
|
||||
|
||||
var (
|
||||
clientInstance *resty.Client
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
func InitClient() {
|
||||
//Inicializacion del client
|
||||
once.Do(func() {
|
||||
client := resty.New().
|
||||
SetTimeout(time.Duration(config.GlobalConfig.TimeoutSeconds) * time.Second)
|
||||
client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: config.GlobalConfig.TLSSkipVerify})
|
||||
|
||||
if config.GlobalConfig.LogRequests {
|
||||
var start time.Time // 👈 aquí declaramos la variable de tiempo
|
||||
|
||||
client.OnBeforeRequest(func(c *resty.Client, r *resty.Request) error {
|
||||
start = time.Now()
|
||||
config.Log.Printf("➡️ Request: %s %s\nHeaders: %v\nBody: %v\n",
|
||||
r.Method, r.URL, r.Header, r.Body)
|
||||
return nil
|
||||
})
|
||||
|
||||
client.OnAfterResponse(func(c *resty.Client, r *resty.Response) error {
|
||||
duration := time.Since(start)
|
||||
config.Log.Printf("⬅️ Response: %s %s | Status: %d | Duration: %s\nHeaders: %v\nBody: %s\n",
|
||||
r.Request.Method, r.Request.URL, r.StatusCode(), duration,
|
||||
r.Header(), r.String())
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
client.SetDebug(config.GlobalConfig.EnableDebug)
|
||||
|
||||
config.Log.Printf("🚀 Inicializando Resty client...")
|
||||
clientInstance = client
|
||||
})
|
||||
}
|
||||
|
||||
func GetClient() *resty.Client {
|
||||
if clientInstance == nil {
|
||||
panic("❌ Resty client no inicializado. Llama a InitClient primero.")
|
||||
}
|
||||
return clientInstance
|
||||
}
|
||||
66
internal/http/sendrequest.go
Normal file
66
internal/http/sendrequest.go
Normal file
@ -0,0 +1,66 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/tuusuario/go-sync-service/internal/config"
|
||||
"github.com/tuusuario/go-sync-service/internal/domain/dto"
|
||||
)
|
||||
|
||||
func SendRequest(host string, opts dto.ServiceConfig) (*resty.Response, error) {
|
||||
client := GetClient()
|
||||
req := client.R()
|
||||
|
||||
// Establecer encabezados
|
||||
for k, v := range opts.Headers {
|
||||
req.SetHeader(k, v)
|
||||
}
|
||||
|
||||
// Construir la URL completa
|
||||
if host == "" {
|
||||
return nil, errors.New("el host no puede estar vacío")
|
||||
}
|
||||
if opts.Path == "" {
|
||||
return nil, errors.New("el path no puede estar vacío")
|
||||
}
|
||||
url := fmt.Sprintf("%s%s", host, opts.Path)
|
||||
req.SetHeader("Content-Type", "application/json")
|
||||
|
||||
// Si es GraphQL
|
||||
if opts.GraphQL != nil {
|
||||
payload := map[string]interface{}{
|
||||
"query": opts.GraphQL.Query,
|
||||
"variables": opts.GraphQL.Variables,
|
||||
}
|
||||
|
||||
req.SetBody(payload)
|
||||
return req.Post(url)
|
||||
}
|
||||
|
||||
// Si es REST
|
||||
if opts.Rest != nil {
|
||||
if opts.Rest.Query != nil {
|
||||
req.SetQueryParams(opts.Rest.Query)
|
||||
}
|
||||
if opts.Rest.Body != nil {
|
||||
req.SetBody(opts.Rest.Body)
|
||||
config.Log.Println("📦 Body:", opts.Rest.Body)
|
||||
}
|
||||
}
|
||||
|
||||
// Método HTTP
|
||||
switch opts.Method {
|
||||
case "GET":
|
||||
return req.Get(url)
|
||||
case "POST":
|
||||
return req.Post(url)
|
||||
case "PUT":
|
||||
return req.Put(url)
|
||||
case "DELETE":
|
||||
return req.Delete(url)
|
||||
default:
|
||||
return nil, fmt.Errorf("método HTTP no soportado: %s", opts.Method)
|
||||
}
|
||||
}
|
||||
153
internal/http/session.go
Normal file
153
internal/http/session.go
Normal file
@ -0,0 +1,153 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt" // NEW
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tuusuario/go-sync-service/internal/config"
|
||||
"github.com/tuusuario/go-sync-service/internal/domain/dto"
|
||||
"github.com/tuusuario/go-sync-service/internal/domain/model"
|
||||
"github.com/tuusuario/go-sync-service/internal/domain/ports"
|
||||
"github.com/tuusuario/go-sync-service/internal/security"
|
||||
)
|
||||
|
||||
var (
|
||||
redisKey = "session:SAP"
|
||||
mutex sync.Mutex
|
||||
)
|
||||
|
||||
type SAPSessionProvider struct{}
|
||||
|
||||
func GetSession(cfg ports.RedisConfigProvider, job dto.CronJob, auth dto.ServiceConfig, dbcore ports.Database, logPrefix string) (*dto.SessionData, error) {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
redisKey = "session:" + job.UnidadNegocio.CompanyName + ":" + job.UnidadNegocio.CompanyDB
|
||||
|
||||
if sess, err := loadSessionFromRedis(cfg, logPrefix); err == nil && time.Now().Before(sess.ExpiresAt) {
|
||||
config.Log.Printf("%v 🔑 Sesión obtenida de Redis %v", logPrefix, redisKey)
|
||||
return sess, nil
|
||||
}
|
||||
|
||||
parametros := map[string]interface{}{
|
||||
"company_name": job.UnidadNegocio.CompanyName,
|
||||
"company_db": job.UnidadNegocio.CompanyDB,
|
||||
}
|
||||
|
||||
credencial, err := dbcore.GetCredencialesFromTemplate(config.GlobalConfig.WhereUnitsBusiness, parametros)
|
||||
if err != nil {
|
||||
config.Log.Printf("%v ❌ Error al obtener credenciales: %v", logPrefix, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// DESCIFRAR PASSWORD (AES-GCM)
|
||||
// ==============================
|
||||
key, err := security.LoadEncryptionKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s %v", logPrefix, err)
|
||||
}
|
||||
// Suponemos que credencial.Password viene como base64(nonce||cipher) generado por EncryptAESGCM
|
||||
plainPass, err := security.DecryptAESGCM(credencial.Password, key)
|
||||
if err != nil {
|
||||
config.Log.Printf("%v ❌ Error al descifrar password: %v", logPrefix, err)
|
||||
return nil, err
|
||||
}
|
||||
credencial.Password = plainPass
|
||||
|
||||
// No loguees secretos. Si necesitas log, hazlo sin password:
|
||||
config.Log.Debugf("%v Obteniendo credenciales para CompanyDB=%s UserName=%s (password oculto)",
|
||||
logPrefix, credencial.CompanyDB, credencial.UserName)
|
||||
|
||||
config.Log.Printf("%v 🔑 Realizando login...", logPrefix)
|
||||
|
||||
mySession := &dto.SessionData{EndPoint: credencial.EndPoint}
|
||||
|
||||
if auth.Rest == nil {
|
||||
auth.Rest = &dto.RestOptions{}
|
||||
}
|
||||
|
||||
// 3. Preparar el body del login (usa cred.Password en claro ya descifrado)
|
||||
auth = prepareAuthBody(auth, credencial, logPrefix)
|
||||
|
||||
config.Log.Debugf(" %v Url: %v + %v", logPrefix, credencial.EndPoint, auth.Path)
|
||||
resp, err := SendRequest(credencial.EndPoint, auth)
|
||||
if err != nil || resp.IsError() {
|
||||
config.Log.Printf("%v ❌ Error al autenticar: %v", logPrefix, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if auth.GQL {
|
||||
var dataGraphql struct {
|
||||
Token string `json:"token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
if err := json.Unmarshal(resp.Body(), &dataGraphql); err != nil {
|
||||
config.Log.Printf("%v ❌ Error al parsear sesión graphql: %v", logPrefix, err)
|
||||
return nil, err
|
||||
}
|
||||
mySession.SessionId = dataGraphql.Token
|
||||
mySession.ExpiresAt = time.Now().Add(10 * time.Minute)
|
||||
} else {
|
||||
var dataRest struct {
|
||||
SessionId string `json:"SessionId"`
|
||||
SessionTimeout int `json:"SessionTimeout"`
|
||||
}
|
||||
if err := json.Unmarshal(resp.Body(), &dataRest); err != nil {
|
||||
config.Log.Printf("%v ❌ Error al parsear sesión rest: %v", logPrefix, err)
|
||||
return nil, err
|
||||
}
|
||||
mySession.SessionId = dataRest.SessionId
|
||||
mySession.ExpiresAt = time.Now().Add(time.Duration(dataRest.SessionTimeout) * time.Minute)
|
||||
}
|
||||
|
||||
config.Log.Printf("%v ✅ Sesión obtenida", logPrefix)
|
||||
saveSessionToRedis(mySession, cfg, logPrefix)
|
||||
return mySession, nil
|
||||
}
|
||||
|
||||
func loadSessionFromRedis(cfg ports.RedisConfigProvider, logPrefix string) (*dto.SessionData, error) {
|
||||
raw, err := cfg.GetString(redisKey)
|
||||
if err != nil {
|
||||
config.Log.Printf("%v ⚠️ No se pudo obtener sesión de Redis: %v", logPrefix, err)
|
||||
return nil, err
|
||||
}
|
||||
var sess dto.SessionData
|
||||
if err := json.Unmarshal([]byte(raw), &sess); err != nil {
|
||||
config.Log.Printf("%v ⚠️ No se pudo parsear sesión de Redis: %v", logPrefix, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
return &sess, nil
|
||||
}
|
||||
|
||||
func saveSessionToRedis(sess *dto.SessionData, cfg ports.RedisConfigProvider, logPrefix string) {
|
||||
data, _ := json.Marshal(sess)
|
||||
ttl := time.Until(sess.ExpiresAt)
|
||||
err := cfg.UpdateParam(redisKey, string(data), ttl)
|
||||
if err != nil {
|
||||
config.Log.Printf("%v ⚠️ No se pudo guardar sesión en Redis: %v", logPrefix, err)
|
||||
}
|
||||
}
|
||||
|
||||
func prepareAuthBody(auth dto.ServiceConfig, cred *model.CredencialesSAP, logPrefix string) dto.ServiceConfig {
|
||||
if auth.Rest == nil {
|
||||
auth.Rest = &dto.RestOptions{}
|
||||
}
|
||||
|
||||
if auth.GQL {
|
||||
config.Log.Debugf("%v 🧠 Preparando auth para GraphQL", logPrefix)
|
||||
auth.Rest.Body = map[string]string{
|
||||
"username": cred.UserName,
|
||||
"password": cred.Password,
|
||||
}
|
||||
} else {
|
||||
config.Log.Debugf("%v🧠 Preparando auth para REST", logPrefix)
|
||||
auth.Rest.Body = map[string]string{
|
||||
"CompanyDB": cred.CompanyDB,
|
||||
"UserName": cred.UserName,
|
||||
"Password": cred.Password,
|
||||
}
|
||||
}
|
||||
return auth
|
||||
}
|
||||
98
internal/scheduler/ListenCronReload
Normal file
98
internal/scheduler/ListenCronReload
Normal file
@ -0,0 +1,98 @@
|
||||
/* package scheduler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/tuusuario/go-sync-service/internal/config"
|
||||
"github.com/tuusuario/go-sync-service/internal/domain/dto"
|
||||
"github.com/tuusuario/go-sync-service/internal/domain/ports"
|
||||
fetcher "github.com/tuusuario/go-sync-service/internal/sync"
|
||||
"github.com/tuusuario/go-sync-service/internal/utils"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var currentCron *cron.Cron
|
||||
|
||||
func ListenCronReload(ctx context.Context, redisClient *redis.Client, cfg ports.RedisConfigProvider, database *gorm.DB) {
|
||||
pubsub := redisClient.Subscribe(ctx, "cron:reload")
|
||||
ch := pubsub.Channel()
|
||||
|
||||
config.Log.Println("👂 Escuchando cambios de configuración en cron:reload")
|
||||
|
||||
for msg := range ch {
|
||||
if msg.Payload == "reload" {
|
||||
config.Log.Println("🔄 Recargando trabajos cron desde Redis...")
|
||||
|
||||
lista_cron, err := utils.CargarDesdeRedis[dto.CronJobList](cfg, config.CronConfig)
|
||||
if err != nil {
|
||||
config.Log.Printf("❌ Error al cargar nueva configuración de crons: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if currentCron != nil {
|
||||
currentCron.Stop()
|
||||
}
|
||||
|
||||
newCron := cron.New()
|
||||
|
||||
for _, job := range lista_cron.Crons {
|
||||
if !job.Enabled {
|
||||
config.Log.WithField("job", job.Nombre).Warn("⚠️ Job desactivado")
|
||||
continue
|
||||
}
|
||||
job := job // evitar captura de variable
|
||||
_, err := newCron.AddFunc(job.Ejecucion, func() {
|
||||
config.Log.Printf("🚀 Ejecutando job: %s", job.Nombre)
|
||||
|
||||
fetcher.SyncData(cfg, database, job)
|
||||
})
|
||||
if err != nil {
|
||||
config.Log.Printf("❌ Error registrando nuevo cron job %s: %v", job.Nombre, err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
currentCron = newCron
|
||||
currentCron.Start()
|
||||
|
||||
config.Log.Println("✅ Crons recargados exitosamente.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func LoadAndStartCrons(cfg ports.RedisConfigProvider, database *gorm.DB) {
|
||||
lista_cron, err := utils.CargarDesdeRedis[dto.CronJobList](cfg, config.CronConfig)
|
||||
if err != nil {
|
||||
config.Log.Errorf("❌ Error al cargar configuración de crons: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
newCron := cron.New()
|
||||
for _, job := range lista_cron.Crons {
|
||||
if !job.Enabled {
|
||||
config.Log.WithField("job", job.Nombre).Warn("⚠️ Job desactivado")
|
||||
continue
|
||||
}
|
||||
job := job
|
||||
config.Log.WithFields(map[string]interface{}{
|
||||
"job": job.Nombre,
|
||||
"cron": job.Ejecucion,
|
||||
}).Info("📝 Registrando cron")
|
||||
|
||||
_, err := newCron.AddFunc(job.Ejecucion, func() {
|
||||
config.Log.Infof("🚀 Ejecutando job: %s", job.Nombre)
|
||||
fetcher.SyncData(cfg, database, job)
|
||||
})
|
||||
if err != nil {
|
||||
config.Log.WithField("job", job.Nombre).Errorf("❌ Error registrando cron: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
newCron.Start()
|
||||
currentCron = newCron
|
||||
config.Log.Info("✅ Cron jobs cargados correctamente al iniciar")
|
||||
}
|
||||
*/
|
||||
26
internal/scheduler/listener.go
Normal file
26
internal/scheduler/listener.go
Normal file
@ -0,0 +1,26 @@
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/tuusuario/go-sync-service/internal/config"
|
||||
"github.com/tuusuario/go-sync-service/internal/domain/ports"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func listenCronReload(ctx context.Context, redisClient *redis.Client, cfg ports.RedisConfigProvider, dbConn *gorm.DB) {
|
||||
pubsub := redisClient.Subscribe(ctx, config.GlobalConfig.RedisSubscribe)
|
||||
ch := pubsub.Channel()
|
||||
|
||||
config.Log.Infof("👂 Escuchando cambios en %v...", config.GlobalConfig.RedisSubscribe)
|
||||
|
||||
for msg := range ch {
|
||||
if msg.Payload == "reload" {
|
||||
config.Log.Info("🔄 Recargando configuración de cron...")
|
||||
if err := loadAndStartJobs(ctx, cfg, dbConn); err != nil {
|
||||
config.Log.Errorf("❌ Error al recargar cron jobs: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
62
internal/scheduler/manager.go
Normal file
62
internal/scheduler/manager.go
Normal file
@ -0,0 +1,62 @@
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/robfig/cron/v3"
|
||||
|
||||
"github.com/tuusuario/go-sync-service/internal/config"
|
||||
"github.com/tuusuario/go-sync-service/internal/domain/dto"
|
||||
"github.com/tuusuario/go-sync-service/internal/domain/ports"
|
||||
fetcher "github.com/tuusuario/go-sync-service/internal/sync"
|
||||
"github.com/tuusuario/go-sync-service/internal/utils"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var currentCron *cron.Cron
|
||||
|
||||
func Start(ctx context.Context, redisClient *redis.Client, cfg ports.RedisConfigProvider, database *gorm.DB) {
|
||||
config.Log.Info("🚀 Iniciando Scheduler...")
|
||||
|
||||
if err := loadAndStartJobs(ctx, cfg, database); err != nil {
|
||||
config.Log.Errorf("❌ Error inicializando jobs: %v", err)
|
||||
}
|
||||
go listenCronReload(ctx, redisClient, cfg, database)
|
||||
}
|
||||
|
||||
func loadAndStartJobs(ctx context.Context, cfg ports.RedisConfigProvider, dbConn *gorm.DB) error {
|
||||
lista, err := utils.CargarDesdeRedis[dto.CronConfigList](cfg, config.CronConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if currentCron != nil {
|
||||
currentCron.Stop()
|
||||
}
|
||||
|
||||
newCron := cron.New()
|
||||
|
||||
for _, job := range lista.Crons {
|
||||
//Valida si el cron esta habilitado
|
||||
if !job.Configuracion.Enabled {
|
||||
config.Log.WithField("job", job.Nombre).Warn("⏸️ Job desactivado")
|
||||
continue
|
||||
}
|
||||
job := job // closure-safe
|
||||
_, err := newCron.AddFunc(job.Configuracion.Ejecucion, func() {
|
||||
config.Log.Infof("🚀 Ejecutando job: %s", job.Nombre)
|
||||
//cargar Job
|
||||
fetcher.SyncData(cfg, dbConn, job)
|
||||
})
|
||||
if err != nil {
|
||||
config.Log.Errorf("❌ Error registrando job %s: %v", job.Nombre, err)
|
||||
}
|
||||
}
|
||||
|
||||
newCron.Start()
|
||||
currentCron = newCron
|
||||
config.Log.Info("✅ Jobs registrados exitosamente")
|
||||
return nil
|
||||
}
|
||||
53
internal/security/aesgcm.go
Normal file
53
internal/security/aesgcm.go
Normal file
@ -0,0 +1,53 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
)
|
||||
|
||||
func DecryptAESGCM(b64 string, key []byte) (string, error) {
|
||||
if len(key) != 32 {
|
||||
return "", errors.New("encryption key must be 32 bytes for AES-256")
|
||||
}
|
||||
raw, err := base64.StdEncoding.DecodeString(b64)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Probar con nonce de 12 y 16 bytes
|
||||
for _, ns := range []int{12, 16} {
|
||||
gcm, err := cipher.NewGCMWithNonceSize(block, ns)
|
||||
if err != nil {
|
||||
continue
|
||||
} // si no soporta ese ns, prueba el siguiente
|
||||
tagSize := gcm.Overhead()
|
||||
if len(raw) < ns+tagSize+1 { // al menos 1 byte de CT
|
||||
continue
|
||||
}
|
||||
nonce := raw[:ns]
|
||||
rest := raw[ns:]
|
||||
|
||||
// A) nonce || ciphertext || tag (nativo Go)
|
||||
if pt, err := gcm.Open(nil, nonce, rest, nil); err == nil {
|
||||
return string(pt), nil
|
||||
}
|
||||
|
||||
// B) nonce || tag || ciphertext (PyCryptodome ejemplo previo)
|
||||
if len(rest) > tagSize {
|
||||
tag := rest[:tagSize]
|
||||
ct := rest[tagSize:]
|
||||
repacked := append(ct, tag...) // => ciphertext||tag
|
||||
if pt, err := gcm.Open(nil, nonce, repacked, nil); err == nil {
|
||||
return string(pt), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("GCM auth failed: wrong key, bad data, or unsupported layout")
|
||||
}
|
||||
52
internal/security/crypto_key.go
Normal file
52
internal/security/crypto_key.go
Normal file
@ -0,0 +1,52 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
keyOnce sync.Once
|
||||
keyBuf []byte
|
||||
keyErr error
|
||||
)
|
||||
|
||||
// LoadEncryptionKey soporta raw (32 chars), base64(32 bytes) o hex(32 bytes).
|
||||
func LoadEncryptionKey() ([]byte, error) {
|
||||
keyOnce.Do(func() {
|
||||
v := strings.TrimSpace(os.Getenv("ENCRYPTION_KEY"))
|
||||
if v == "" {
|
||||
keyErr = errors.New("ENCRYPTION_KEY no definida")
|
||||
return
|
||||
}
|
||||
|
||||
// 1) raw 32 chars
|
||||
if len(v) == 32 {
|
||||
keyBuf = []byte(v)
|
||||
return
|
||||
}
|
||||
|
||||
// 2) base64 (std / url)
|
||||
if b, err := base64.StdEncoding.DecodeString(v); err == nil && len(b) == 32 {
|
||||
keyBuf = b
|
||||
return
|
||||
}
|
||||
if b, err := base64.URLEncoding.DecodeString(v); err == nil && len(b) == 32 {
|
||||
keyBuf = b
|
||||
return
|
||||
}
|
||||
|
||||
// 3) hex (64 chars -> 32 bytes)
|
||||
if b, err := hex.DecodeString(v); err == nil && len(b) == 32 {
|
||||
keyBuf = b
|
||||
return
|
||||
}
|
||||
|
||||
keyErr = errors.New("ENCRYPTION_KEY inválida: debe ser 32 chars raw, base64 de 32 bytes o hex de 32 bytes")
|
||||
})
|
||||
return keyBuf, keyErr
|
||||
}
|
||||
206
internal/sync/fetcher.go
Normal file
206
internal/sync/fetcher.go
Normal file
@ -0,0 +1,206 @@
|
||||
package fetcher
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/tuusuario/go-sync-service/internal/config"
|
||||
"github.com/tuusuario/go-sync-service/internal/db"
|
||||
"github.com/tuusuario/go-sync-service/internal/domain/dto"
|
||||
"github.com/tuusuario/go-sync-service/internal/domain/ports"
|
||||
"github.com/tuusuario/go-sync-service/internal/http"
|
||||
"github.com/tuusuario/go-sync-service/internal/utils"
|
||||
"github.com/tuusuario/go-sync-service/metrics"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func SyncData(redis ports.RedisConfigProvider, database *gorm.DB, job dto.CronJob) {
|
||||
start := time.Now()
|
||||
logPrefix := fmt.Sprintf("[🧩 Job: %s] ", job.Nombre)
|
||||
config.Log.Printf("%s Iniciando sincronización...", logPrefix)
|
||||
|
||||
var dbcore ports.Database = db.NewGormDatabase(database)
|
||||
var hasError bool
|
||||
|
||||
http.InitClient()
|
||||
|
||||
if len(job.Configuracion.Proceso) == 0 {
|
||||
config.Log.Printf(" %s ⚠️ No hay procesos configurados para este job", logPrefix)
|
||||
goto END
|
||||
}
|
||||
|
||||
for _, proceso := range job.Configuracion.Proceso {
|
||||
config.Log.Printf(logPrefix+" Iniciando proceso %s", proceso)
|
||||
|
||||
jobIndividual, err := utils.CargarDesdeRedis[dto.JobConfig](redis, proceso)
|
||||
if err != nil {
|
||||
config.Log.Printf(logPrefix+" ❌ Error al obtener configuración del proceso: %v", err)
|
||||
hasError = true
|
||||
continue
|
||||
}
|
||||
//Obtener Session
|
||||
session, err := http.GetSession(redis, job, jobIndividual.Auth, dbcore, logPrefix)
|
||||
if err != nil {
|
||||
config.Log.Println(logPrefix + " ❌ No se pudo obtener sesión")
|
||||
hasError = true
|
||||
continue
|
||||
}
|
||||
if session == nil || session.SessionId == "" {
|
||||
config.Log.Println(logPrefix + " ❌ Sesión inválida o vacía")
|
||||
hasError = true
|
||||
continue
|
||||
}
|
||||
|
||||
if jobIndividual.Service.GQL {
|
||||
jobIndividual.Service.Headers["Authorization"] = "Bearer " + session.SessionId
|
||||
} else {
|
||||
jobIndividual.Service.Headers["Cookie"] = "B1SESSION=" + session.SessionId
|
||||
}
|
||||
|
||||
response, err := FetchAllPaginatedManual[map[string]interface{}](session.EndPoint, jobIndividual.Service, logPrefix)
|
||||
if err != nil {
|
||||
config.Log.Printf(logPrefix+" ❌ Error al obtener data: %v", err)
|
||||
hasError = true
|
||||
continue
|
||||
}
|
||||
|
||||
config.Log.Printf("%s Cantidad de elementos: %v", logPrefix, len(*response))
|
||||
|
||||
err = dbcore.SyncRows(jobIndividual.Persistencia, response, job.UnidadNegocio.CompanyDB)
|
||||
if err != nil {
|
||||
config.Log.Printf(logPrefix+" ❌ Error al guardar en base de datos: %v", err)
|
||||
hasError = true
|
||||
}
|
||||
}
|
||||
|
||||
END:
|
||||
duration := time.Since(start).Seconds()
|
||||
jobName := job.Nombre
|
||||
|
||||
metrics.CronDuration.WithLabelValues(jobName).Observe(duration)
|
||||
|
||||
if hasError {
|
||||
metrics.CronError.WithLabelValues(jobName).Inc()
|
||||
metrics.CronLastError.WithLabelValues(jobName).Set(float64(time.Now().Unix()))
|
||||
} else {
|
||||
metrics.CronSuccess.WithLabelValues(jobName).Inc()
|
||||
metrics.CronLastSuccess.WithLabelValues(jobName).Set(float64(time.Now().Unix()))
|
||||
}
|
||||
|
||||
config.Log.Printf("%s ⏱ Duración total: %.2fs", logPrefix, duration)
|
||||
}
|
||||
|
||||
func FetchAllPaginatedManual[T any](host string, service dto.ServiceConfig, logPrefix string) (*[]T, error) {
|
||||
var all []T
|
||||
|
||||
// REST paginación
|
||||
if service.Rest != nil && service.Rest.Pagination != nil && service.Rest.Pagination.Enabled {
|
||||
skip := service.Rest.Pagination.Skip
|
||||
top := service.Rest.Pagination.Top
|
||||
|
||||
for {
|
||||
// Actualizar parámetros de paginación
|
||||
service.Rest.Query["$skip"] = strconv.Itoa(skip)
|
||||
service.Rest.Query["$top"] = strconv.Itoa(top)
|
||||
|
||||
resp, err := http.SendRequest(host, service)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s ❌ error en la petición: %w", logPrefix, err)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Value []T `json:"value"`
|
||||
}
|
||||
if err := json.Unmarshal(resp.Body(), &result); err != nil {
|
||||
return nil, fmt.Errorf("%s ❌ error parseando respuesta: %w", logPrefix, err)
|
||||
}
|
||||
|
||||
if len(result.Value) == 0 {
|
||||
config.Log.Printf(" %s ❌ No hay más elementos", logPrefix)
|
||||
break
|
||||
}
|
||||
|
||||
all = append(all, result.Value...)
|
||||
skip += top
|
||||
}
|
||||
return &all, nil
|
||||
}
|
||||
|
||||
// GraphQL paginación
|
||||
if service.GraphQL != nil && service.GraphQL.Pagination != nil && service.GraphQL.Pagination.Enabled {
|
||||
for {
|
||||
resp, err := http.SendRequest(host, service)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s ❌ error en la petición: %w", logPrefix, err)
|
||||
}
|
||||
|
||||
var raw map[string]any
|
||||
if err := json.Unmarshal(resp.Body(), &raw); err != nil {
|
||||
return nil, fmt.Errorf("%s ❌ error parseando respuesta GraphQL: %w", logPrefix, err)
|
||||
}
|
||||
|
||||
data, ok := raw["data"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s ❌ no se encontró 'data' en la respuesta GraphQL", logPrefix)
|
||||
}
|
||||
|
||||
root, ok := data[service.GraphQL.RootField].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s ❌ no se encontró '%v' en la respuesta GraphQL", logPrefix, service.GraphQL.RootField)
|
||||
}
|
||||
|
||||
// Obtener y parsear filas
|
||||
rows, ok := root[service.GraphQL.RowField].([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s ❌ no se encontró '%v' en la respuesta GraphQL", logPrefix, service.GraphQL.RowField)
|
||||
}
|
||||
|
||||
for _, r := range rows {
|
||||
jsonRow, _ := json.Marshal(r)
|
||||
|
||||
var item T
|
||||
if err := json.Unmarshal(jsonRow, &item); err != nil {
|
||||
config.Log.Printf("%s ⚠️ error parseando fila: %v", logPrefix, err)
|
||||
continue
|
||||
}
|
||||
all = append(all, item)
|
||||
}
|
||||
|
||||
// Evaluar paginación
|
||||
meta, ok := root["meta"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s ❌ no se encontró 'meta' para paginación", logPrefix)
|
||||
}
|
||||
|
||||
hasNext, ok := meta["hasNextPage"].(bool)
|
||||
if !ok || !hasNext {
|
||||
break
|
||||
}
|
||||
|
||||
// Avanzar cursor (página)
|
||||
if nextPage, ok := meta["next"]; ok {
|
||||
service.GraphQL.Variables[service.GraphQL.Pagination.CursorParam] = nextPage
|
||||
} else {
|
||||
break // no hay campo next
|
||||
}
|
||||
}
|
||||
return &all, nil
|
||||
}
|
||||
|
||||
// Sin paginación
|
||||
resp, err := http.SendRequest(host, service)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s ❌ error en la petición: %w", logPrefix, err)
|
||||
}
|
||||
var result struct {
|
||||
Value []T `json:"value"`
|
||||
}
|
||||
if err := json.Unmarshal(resp.Body(), &result); err != nil {
|
||||
return nil, fmt.Errorf("%s ❌ error parseando respuesta final: %w", logPrefix, err)
|
||||
}
|
||||
all = append(all, result.Value...)
|
||||
|
||||
return &all, nil
|
||||
}
|
||||
29
internal/utils/redis_loader.go
Normal file
29
internal/utils/redis_loader.go
Normal file
@ -0,0 +1,29 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/tuusuario/go-sync-service/internal/config"
|
||||
"github.com/tuusuario/go-sync-service/internal/domain/ports"
|
||||
)
|
||||
|
||||
// Función genérica para deserializar un JSON de Redis a cualquier tipo
|
||||
func CargarDesdeRedis[T any](cfg ports.RedisConfigProvider, clave string) (*T, error) {
|
||||
data, err := cfg.GetString(clave)
|
||||
if err != nil {
|
||||
|
||||
config.Log.Errorf("⚠️ error al obtener config de Redis [%s]: %s", clave, err)
|
||||
return nil, fmt.Errorf("error al obtener clave [%s] de redis: %w", clave, err)
|
||||
|
||||
}
|
||||
config.Log.Debugf("🔑 Clave [%s] obtenida de Redis: %s", clave, data)
|
||||
var result T
|
||||
if err := json.Unmarshal([]byte(data), &result); err != nil {
|
||||
config.Log.Errorf("❌ error al parsear JSON [%s]: %s", clave, err)
|
||||
return nil, fmt.Errorf("error al parsear JSON [%s]: %w", clave, err)
|
||||
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
1748
logs/syncronizador.log
Normal file
1748
logs/syncronizador.log
Normal file
File diff suppressed because one or more lines are too long
56
metrics/metrics.go
Normal file
56
metrics/metrics.go
Normal file
@ -0,0 +1,56 @@
|
||||
// metrics/metrics.go
|
||||
package metrics
|
||||
|
||||
import "github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
var (
|
||||
// Cuántas veces se ejecutó exitosamente cada job
|
||||
CronSuccess = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "cron_job_success_total",
|
||||
Help: "Total de ejecuciones exitosas por cron job",
|
||||
},
|
||||
[]string{"job"},
|
||||
)
|
||||
|
||||
// Cuántas veces falló
|
||||
CronError = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "cron_job_error_total",
|
||||
Help: "Total de errores por cron job",
|
||||
},
|
||||
[]string{"job"},
|
||||
)
|
||||
|
||||
// Tiempo de ejecución de cada job
|
||||
CronDuration = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "cron_job_duration_seconds",
|
||||
Help: "Duración de ejecución por cron job",
|
||||
Buckets: prometheus.DefBuckets,
|
||||
},
|
||||
[]string{"job"},
|
||||
)
|
||||
|
||||
// Última ejecución exitosa (timestamp Unix)
|
||||
CronLastSuccess = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "cron_job_last_success_timestamp",
|
||||
Help: "Timestamp de la última ejecución exitosa por cron job",
|
||||
},
|
||||
[]string{"job"},
|
||||
)
|
||||
|
||||
// Último error (timestamp Unix)
|
||||
CronLastError = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "cron_job_last_error_timestamp",
|
||||
Help: "Timestamp del último error por cron job",
|
||||
},
|
||||
[]string{"job"},
|
||||
)
|
||||
)
|
||||
|
||||
func Register() {
|
||||
prometheus.MustRegister(CronSuccess, CronError, CronDuration, CronLastSuccess, CronLastError)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user