Files
music-agregator/cmd/music-agregator/main.go
T

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
}