feat: Sentinela v0.2.0 — Brazilian Financial Data API in Go

- 20 Go source files, single 16MB binary
- SQLite + FTS5 full-text search (pure Go, no CGO)
- BCB integration: Selic, CDI, IPCA, USD/BRL, EUR/BRL
- CVM integration: 2,524 companies from registry
- Fiber v2 REST API with 42 handlers
- Auto-seeds on first run (~5s for BCB + CVM)
- Token bucket rate limiter, optional API key auth
- Periodic sync scheduler (configurable)
- Graceful shutdown, structured logging (slog)
- All endpoints tested with real data
This commit is contained in:
2026-02-10 11:15:54 -03:00
commit f7c8b446bf
28 changed files with 1763 additions and 0 deletions

77
cmd/sentinela/main.go Normal file
View File

@@ -0,0 +1,77 @@
package main
import (
"fmt"
"log/slog"
"os"
"os/signal"
"syscall"
"time"
"github.com/sentinela-go/internal/api"
"github.com/sentinela-go/internal/config"
"github.com/sentinela-go/internal/db"
"github.com/sentinela-go/internal/fetcher"
)
func main() {
cfg := config.Load()
// Setup structured logging
level := slog.LevelInfo
if cfg.LogLevel == "debug" {
level = slog.LevelDebug
}
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: level})))
slog.Info("starting Sentinela", "port", cfg.Port)
// Initialize database
database, err := db.New(cfg.DatabasePath)
if err != nil {
slog.Error("failed to initialize database", "error", err)
os.Exit(1)
}
defer database.Close()
// Seed data if empty
if database.IsMarketEmpty() {
slog.Info("database is empty, seeding BCB data...")
if err := fetcher.FetchAllBCB(database); err != nil {
slog.Error("failed to seed BCB data", "error", err)
}
}
if database.IsEmpty() {
slog.Info("no companies found, seeding CVM data...")
if err := fetcher.FetchAllCVM(database); err != nil {
slog.Error("failed to seed CVM data", "error", err)
}
}
// Start scheduler
syncInterval, err := time.ParseDuration(cfg.SyncInterval)
if err != nil {
syncInterval = 30 * time.Minute
}
stopChan := make(chan struct{})
go fetcher.StartScheduler(database, syncInterval, stopChan)
// Create and start server
app := api.NewServer(cfg, database)
// Graceful shutdown
go func() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
slog.Info("shutting down...")
close(stopChan)
app.Shutdown()
}()
addr := fmt.Sprintf(":%d", cfg.Port)
if err := app.Listen(addr); err != nil {
slog.Error("server error", "error", err)
os.Exit(1)
}
}