226 lines
6.2 KiB
Go
226 lines
6.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
|
|
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging"
|
|
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/recovery"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
"github.com/riverqueue/river"
|
|
"github.com/riverqueue/river/riverdriver/riverpgxv5"
|
|
"github.com/rs/zerolog"
|
|
"github.com/rs/zerolog/log"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/reflection"
|
|
"google.golang.org/grpc/status"
|
|
"gopkg.in/yaml.v2"
|
|
|
|
grpcprom "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus"
|
|
|
|
"homelab.lan/music-agregator/internal"
|
|
"homelab.lan/music-agregator/internal/config"
|
|
"homelab.lan/music-agregator/internal/database"
|
|
"homelab.lan/music-agregator/internal/hello"
|
|
"homelab.lan/music-agregator/internal/indexer"
|
|
"homelab.lan/music-agregator/internal/metadata"
|
|
"homelab.lan/music-agregator/internal/torrent"
|
|
"homelab.lan/music-agregator/internal/workers"
|
|
)
|
|
|
|
func main() {
|
|
log.Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).
|
|
With().Timestamp().Logger()
|
|
|
|
configPath := flag.String("config", "", "Path to the config file")
|
|
flag.Parse()
|
|
|
|
cfg, err := parseConfig(configPath)
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("Failed to load config")
|
|
}
|
|
log.Info().Interface("config", cfg).Msg("Loaded config")
|
|
|
|
serveGrpc(*cfg)
|
|
}
|
|
|
|
func interceptorLogger(l zerolog.Logger) logging.Logger {
|
|
return logging.LoggerFunc(func(ctx context.Context, lvl logging.Level, msg string, fields ...any) {
|
|
l := l.With().Fields(fields).Logger()
|
|
switch lvl {
|
|
case logging.LevelDebug:
|
|
l.Debug().Msg(msg)
|
|
case logging.LevelInfo:
|
|
l.Info().Msg(msg)
|
|
case logging.LevelWarn:
|
|
l.Warn().Msg(msg)
|
|
case logging.LevelError:
|
|
l.Error().Msg(msg)
|
|
default:
|
|
l.Info().Msg(msg)
|
|
}
|
|
})
|
|
}
|
|
|
|
func setupDatabase(ctx context.Context, cfg config.Config) *database.DB {
|
|
db, err := database.New(ctx, cfg.Database.URL)
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("failed to connect to database")
|
|
}
|
|
return db
|
|
}
|
|
|
|
type riverSetup struct {
|
|
client *river.Client[pgx.Tx]
|
|
cacheRefreshWorker *indexer.CacheRefreshWorker
|
|
}
|
|
|
|
func setupRiver(ctx context.Context, cfg config.Config, db *database.DB) *riverSetup {
|
|
cacheWorker := &indexer.CacheRefreshWorker{}
|
|
pollWorker := &workers.PollDownloadWorker{
|
|
Downloads: database.NewDownloadRepository(db.Pool),
|
|
DownloadFiles: database.NewDownloadFileRepository(db.Pool),
|
|
TorrentClient: torrent.MustNewTorrentClient(cfg),
|
|
}
|
|
|
|
riverWorkers := river.NewWorkers()
|
|
river.AddWorker(riverWorkers, cacheWorker)
|
|
river.AddWorker(riverWorkers, pollWorker)
|
|
|
|
riverClient, err := river.NewClient(riverpgxv5.New(db.Pool), &river.Config{
|
|
Queues: map[string]river.QueueConfig{
|
|
river.QueueDefault: {MaxWorkers: 4},
|
|
},
|
|
Workers: riverWorkers,
|
|
})
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("failed to create River client")
|
|
}
|
|
|
|
cacheWorker.RiverClient = riverClient
|
|
pollWorker.RiverClient = riverClient
|
|
|
|
if err := riverClient.Start(ctx); err != nil {
|
|
log.Fatal().Err(err).Msg("failed to start River client")
|
|
}
|
|
|
|
log.Info().Msg("River queue started")
|
|
|
|
return &riverSetup{
|
|
client: riverClient,
|
|
cacheRefreshWorker: cacheWorker,
|
|
}
|
|
}
|
|
|
|
func serveGrpc(config config.Config) {
|
|
srvMetrics := grpcprom.NewServerMetrics(
|
|
grpcprom.WithServerHandlingTimeHistogram(
|
|
grpcprom.WithHistogramBuckets([]float64{0.001, 0.01, 0.1, 0.3, 0.6, 1, 3, 6, 10, 30}),
|
|
),
|
|
)
|
|
|
|
logOpts := []logging.Option{
|
|
logging.WithLogOnEvents(logging.StartCall, logging.FinishCall),
|
|
}
|
|
|
|
recoveryOpts := []recovery.Option{
|
|
recovery.WithRecoveryHandler(func(p any) (err error) {
|
|
log.Error().Interface("panic", p).Msg("recovered from panic")
|
|
return status.Errorf(codes.Internal, "internal error")
|
|
}),
|
|
}
|
|
|
|
server := grpc.NewServer(
|
|
grpc.ChainUnaryInterceptor(
|
|
srvMetrics.UnaryServerInterceptor(),
|
|
logging.UnaryServerInterceptor(interceptorLogger(log.Logger), logOpts...),
|
|
recovery.UnaryServerInterceptor(recoveryOpts...),
|
|
),
|
|
grpc.ChainStreamInterceptor(
|
|
srvMetrics.StreamServerInterceptor(),
|
|
logging.StreamServerInterceptor(interceptorLogger(log.Logger), logOpts...),
|
|
recovery.StreamServerInterceptor(recoveryOpts...),
|
|
),
|
|
)
|
|
|
|
ctx := context.Background()
|
|
db := setupDatabase(ctx, config)
|
|
defer db.Close()
|
|
rs := setupRiver(ctx, config, db)
|
|
|
|
musiscAgregatorSeerver, err := internal.NewMusicAgregatorServer(config, rs.client, db)
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("failed to create MusicAgregatorServer")
|
|
}
|
|
indexerServer, err := indexer.NewIndexerServer(config, rs.client, rs.cacheRefreshWorker)
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("failed to create IndexerServer")
|
|
}
|
|
torrentServer, err := torrent.NewTorrentServer(config)
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("failed to create TorrentServer")
|
|
}
|
|
metadataServer, err := metadata.NewMetadataServer(config)
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("failed to create MetadataServer")
|
|
}
|
|
|
|
services := []internal.Registrable{
|
|
hello.NewHelloServer(),
|
|
indexerServer,
|
|
torrentServer,
|
|
metadataServer,
|
|
musiscAgregatorSeerver,
|
|
}
|
|
|
|
for _, service := range services {
|
|
service.Register(server)
|
|
}
|
|
|
|
srvMetrics.InitializeMetrics(server)
|
|
prometheus.MustRegister(srvMetrics)
|
|
reflection.Register(server)
|
|
|
|
go func() {
|
|
mux := http.NewServeMux()
|
|
mux.Handle("/metrics", promhttp.Handler())
|
|
log.Info().Msg("Prometheus metrics available at :9090/metrics")
|
|
if err := http.ListenAndServe(":9090", mux); err != nil {
|
|
log.Fatal().Err(err).Msg("Failed to start metrics server")
|
|
}
|
|
}()
|
|
|
|
listener, err := net.Listen("tcp", fmt.Sprintf("%v:%v", config.App.Host, config.App.Port))
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("Failed to listen")
|
|
}
|
|
|
|
log.Info().Str("addr", listener.Addr().String()).Msg("gRPC server listening")
|
|
server.Serve(listener)
|
|
}
|
|
|
|
func parseConfig(path *string) (*config.Config, error) {
|
|
f, err := os.Open(*path)
|
|
if err != nil {
|
|
log.Error().Str("path", *path).Msg("Unable to opent config by path")
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
cfg := config.NewConfig()
|
|
decoder := yaml.NewDecoder(f)
|
|
err = decoder.Decode(&cfg)
|
|
if err != nil {
|
|
// processError(err)
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|