2024-08-06 22:13:37 +07:00
|
|
|
package serve
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"errors"
|
2024-08-11 19:50:43 +07:00
|
|
|
"fmt"
|
2024-08-06 22:13:37 +07:00
|
|
|
"log/slog"
|
2024-08-11 19:50:43 +07:00
|
|
|
"net"
|
2024-08-06 22:13:37 +07:00
|
|
|
"net/http"
|
2024-08-07 10:41:00 +07:00
|
|
|
"os"
|
2024-08-15 09:36:06 +07:00
|
|
|
"path/filepath"
|
2024-08-06 22:13:37 +07:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"connectrpc.com/connect"
|
2024-08-12 20:20:12 +07:00
|
|
|
"connectrpc.com/otelconnect"
|
2024-08-08 20:49:18 +07:00
|
|
|
"connectrpc.com/validate"
|
2024-08-15 11:01:57 +07:00
|
|
|
"github.com/XSAM/otelsql"
|
2024-08-08 13:41:18 +07:00
|
|
|
sqldblogger "github.com/simukti/sqldb-logger"
|
2024-08-06 22:13:37 +07:00
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/stephenafamo/bob"
|
|
|
|
"github.com/tigorlazuardi/bluemage/go/api"
|
2024-08-11 19:50:43 +07:00
|
|
|
"github.com/tigorlazuardi/bluemage/go/config"
|
2024-08-15 20:39:04 +07:00
|
|
|
v1DeviceConnect "github.com/tigorlazuardi/bluemage/go/gen/proto/device/v1/v1connect"
|
|
|
|
v1SubredditsConnect "github.com/tigorlazuardi/bluemage/go/gen/proto/subreddits/v1/v1connect"
|
|
|
|
"github.com/tigorlazuardi/bluemage/go/gen/reddit"
|
2024-08-06 22:13:37 +07:00
|
|
|
"github.com/tigorlazuardi/bluemage/go/pkg/errs"
|
2024-08-07 10:41:00 +07:00
|
|
|
"github.com/tigorlazuardi/bluemage/go/pkg/log"
|
2024-08-12 20:20:12 +07:00
|
|
|
"github.com/tigorlazuardi/bluemage/go/pkg/telemetry"
|
2024-08-06 22:13:37 +07:00
|
|
|
"github.com/tigorlazuardi/bluemage/go/server"
|
2024-08-15 11:01:57 +07:00
|
|
|
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
2024-08-06 22:13:37 +07:00
|
|
|
"golang.org/x/net/http2"
|
|
|
|
"golang.org/x/net/http2/h2c"
|
|
|
|
)
|
|
|
|
|
|
|
|
var Cmd = &cobra.Command{
|
|
|
|
Use: "serve",
|
|
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
2024-08-11 19:50:43 +07:00
|
|
|
ctx := cmd.Context()
|
|
|
|
cfg := config.FromContext(ctx)
|
2024-08-11 21:45:11 +07:00
|
|
|
var cleanups []func() error
|
|
|
|
defer func() {
|
|
|
|
var e []error
|
|
|
|
for _, c := range cleanups {
|
|
|
|
e = append(e, c())
|
|
|
|
}
|
|
|
|
if err := errors.Join(e...); err != nil {
|
|
|
|
fmt.Println(err.Error())
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
logHandler, cleanup := log.NewHandler(cfg)
|
|
|
|
cleanups = append(cleanups, cleanup)
|
|
|
|
slog.SetDefault(slog.New(logHandler))
|
2024-08-11 19:50:43 +07:00
|
|
|
|
2024-08-12 20:20:12 +07:00
|
|
|
tele, err := telemetry.New(ctx, cfg)
|
|
|
|
if err != nil {
|
|
|
|
return errs.Wrap(err, "failed to create telemetry")
|
|
|
|
}
|
|
|
|
cleanups = append(cleanups, tele.Close)
|
|
|
|
|
2024-08-11 19:50:43 +07:00
|
|
|
if err := os.MkdirAll(cfg.String("download.directory"), 0755); err != nil {
|
|
|
|
return errs.Wrapw(err, "failed to create download directory", "path", cfg.String("download.directory"))
|
|
|
|
}
|
|
|
|
|
2024-08-15 09:36:06 +07:00
|
|
|
dbpath := cfg.String("db.path")
|
|
|
|
dir := filepath.Dir(dbpath)
|
|
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
|
|
return errs.Wrapw(err, "failed to create database directory", "path", dir)
|
|
|
|
}
|
|
|
|
|
|
|
|
dsn := fmt.Sprintf("file:%s", cfg.String("db.path"))
|
2024-08-15 11:01:57 +07:00
|
|
|
sqldb, err := otelsql.Open(cfg.String("db.driver"), dsn, otelsql.WithAttributes(semconv.DBSystemSqlite))
|
2024-08-06 22:13:37 +07:00
|
|
|
if err != nil {
|
2024-08-11 19:50:43 +07:00
|
|
|
return errs.Wrapw(err, "failed to open database",
|
|
|
|
"driver", cfg.String("db.driver"),
|
2024-08-15 09:36:06 +07:00
|
|
|
"dsn", dsn,
|
2024-08-11 19:50:43 +07:00
|
|
|
)
|
2024-08-06 22:13:37 +07:00
|
|
|
}
|
2024-08-11 21:45:11 +07:00
|
|
|
cleanups = append(cleanups, sqldb.Close)
|
2024-08-15 11:01:57 +07:00
|
|
|
if err := otelsql.RegisterDBStatsMetrics(sqldb, otelsql.WithAttributes(semconv.DBSystemSqlite)); err != nil {
|
|
|
|
return errs.Wrapw(
|
|
|
|
err, "failed to register database stats metrics",
|
|
|
|
"driver", cfg.String("db.driver"),
|
|
|
|
"dsn", dsn,
|
|
|
|
)
|
|
|
|
}
|
2024-08-06 22:13:37 +07:00
|
|
|
|
2024-08-08 13:41:18 +07:00
|
|
|
sqldb = sqldblogger.OpenDriver(
|
2024-08-15 09:36:06 +07:00
|
|
|
dsn,
|
2024-08-08 13:41:18 +07:00
|
|
|
sqldb.Driver(),
|
|
|
|
log.SQLLogger{},
|
|
|
|
sqldblogger.WithSQLQueryAsMessage(true),
|
|
|
|
)
|
|
|
|
db := bob.New(sqldb)
|
2024-08-07 10:41:00 +07:00
|
|
|
|
2024-08-15 20:39:04 +07:00
|
|
|
client, err := reddit.NewClient("https://reddit.com", reddit.WithClient(&http.Client{
|
|
|
|
Transport: log.NewRoundTripper(http.DefaultTransport),
|
|
|
|
}))
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
2024-08-06 22:13:37 +07:00
|
|
|
api := &api.API{
|
2024-08-13 09:40:02 +07:00
|
|
|
Executor: db,
|
|
|
|
DB: sqldb,
|
2024-08-15 20:39:04 +07:00
|
|
|
Reddit: client,
|
2024-08-06 22:13:37 +07:00
|
|
|
}
|
|
|
|
|
2024-08-08 20:49:18 +07:00
|
|
|
validationInterceptor, err := validate.NewInterceptor()
|
|
|
|
if err != nil {
|
|
|
|
return errs.Wrap(err, "failed to create validation interceptor")
|
|
|
|
}
|
|
|
|
|
2024-08-12 20:20:12 +07:00
|
|
|
otelInterceptor, err := otelconnect.NewInterceptor()
|
|
|
|
if err != nil {
|
|
|
|
return errs.Wrap(err, "failed to create otel interceptor")
|
|
|
|
}
|
|
|
|
|
2024-08-15 20:39:04 +07:00
|
|
|
interceptors := []connect.Interceptor{
|
2024-08-08 20:49:18 +07:00
|
|
|
validationInterceptor,
|
2024-08-12 20:20:12 +07:00
|
|
|
otelInterceptor,
|
2024-08-15 09:36:06 +07:00
|
|
|
server.ErrorMessageInterceptor(),
|
2024-08-08 20:49:18 +07:00
|
|
|
server.LogInterceptor(),
|
2024-08-15 20:39:04 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
handlerOpts := []connect.HandlerOption{
|
|
|
|
connect.WithInterceptors(interceptors...),
|
|
|
|
}
|
|
|
|
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
mux.Handle(v1DeviceConnect.NewDeviceServiceHandler(&server.DeviceHandler{API: api}, handlerOpts...))
|
|
|
|
mux.Handle(v1SubredditsConnect.NewSubredditsServiceHandler(&server.SubredditHandler{API: api}, handlerOpts...))
|
2024-08-06 22:13:37 +07:00
|
|
|
|
|
|
|
server := &http.Server{
|
2024-08-11 19:50:43 +07:00
|
|
|
Addr: fmt.Sprintf("%s:%s", cfg.String("http.host"), cfg.String("http.port")),
|
2024-08-06 22:13:37 +07:00
|
|
|
Handler: h2c.NewHandler(server.WithCORS(mux), &http2.Server{}),
|
2024-08-11 19:50:43 +07:00
|
|
|
BaseContext: func(net.Listener) context.Context {
|
|
|
|
return ctx
|
|
|
|
},
|
2024-08-06 22:13:37 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
go func() {
|
2024-08-11 19:50:43 +07:00
|
|
|
<-ctx.Done()
|
2024-08-06 22:13:37 +07:00
|
|
|
slog.Info("Exit signal received. Shutting down server")
|
|
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
|
|
|
defer cancel()
|
|
|
|
_ = server.Shutdown(shutdownCtx)
|
|
|
|
}()
|
|
|
|
|
|
|
|
slog.Info("ConnectRPC server started", "addr", server.Addr)
|
|
|
|
err = server.ListenAndServe()
|
|
|
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
|
|
return errs.Wrap(err, "failed to serve")
|
|
|
|
}
|
2024-08-08 22:51:53 +07:00
|
|
|
slog.Info("ConnectRPC server stopped")
|
2024-08-11 21:45:11 +07:00
|
|
|
return nil
|
2024-08-06 22:13:37 +07:00
|
|
|
},
|
|
|
|
SilenceUsage: true,
|
|
|
|
}
|
2024-08-11 19:50:43 +07:00
|
|
|
|
|
|
|
func Register(cmd *cobra.Command, config *config.Config) {
|
|
|
|
}
|