package server import ( "database/sql" "fmt" "log" "net" "net/http" "strconv" "time" "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql/playground" "github.com/99designs/gqlgen/handler" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" goRedis "github.com/go-redis/redis/v8" _ "github.com/lib/pq" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/vaughan0/go-ini" "git.sr.ht/~sircmpwn/core-go/auth" "git.sr.ht/~sircmpwn/core-go/config" "git.sr.ht/~sircmpwn/core-go/database" "git.sr.ht/~sircmpwn/core-go/redis" ) var ( requestsProcessed = promauto.NewCounter(prometheus.CounterOpts{ Name: "api_requests_processed_total", Help: "Total number of API requests processed", }) requestDuration = promauto.NewHistogram(prometheus.HistogramOpts{ Name: "api_request_duration_millis", Help: "Duration of processed HTTP requests in milliseconds", Buckets: []float64{10, 20, 40, 80, 120, 300, 600, 900, 1800}, }) ) type Server struct { conf ini.File router chi.Router schema graphql.ExecutableSchema service string } func NewServer(service string, conf ini.File) *Server { server := &Server{ conf: conf, router: chi.NewRouter(), service: service, } return server } func (server *Server) Router() chi.Router { return server.router } func (server *Server) WithSchema(schema graphql.ExecutableSchema) *Server { server.schema = schema var ( complexity int err error ) if limit, ok := server.conf.Get( server.service + "::api", "max-complexity"); ok { complexity, err = strconv.Atoi(limit) if err != nil { panic(err) } } else { complexity = 250 } // TODO: Remove config parameter from EmailRecover rec := EmailRecover(server.conf, config.Debug, server.service) srv := handler.GraphQL(schema, handler.ComplexityLimit(complexity), handler.RecoverFunc(rec)) server.router.Handle("/query", srv) if config.Debug { server.router.Handle("/", playground.Handler("GraphQL playground", "/query")) } server.router.Handle("/query/metrics", promhttp.Handler()) return server } func (server *Server) WithDefaultMiddleware() *Server { pgcs, ok := server.conf.Get(server.service, "connection-string") if !ok { log.Fatalf("No connection string provided in config.ini") } db, err := sql.Open("postgres", pgcs) if err != nil { log.Fatalf("Failed to open a database connection: %v", err) } rcs, ok := server.conf.Get("sr.ht", "redis-host") if !ok { rcs = "redis://" } ropts, err := goRedis.ParseURL(rcs) if err != nil { log.Fatalf("Invalid sr.ht::redis-host in config.ini: %e", err) } rc := goRedis.NewClient(ropts) apiconf := fmt.Sprintf("%s::api", server.service) var timeout time.Duration if to, ok := server.conf.Get(apiconf, "max-duration"); ok { timeout, err = time.ParseDuration(to) if err != nil { panic(err) } } else { timeout = 3 * time.Second } server.router.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) end := time.Now() elapsed := end.Sub(start) requestDuration.Observe(float64(elapsed.Milliseconds())) requestsProcessed.Inc() }) }) server.router.Use(config.Middleware(server.conf, server.service)) server.router.Use(database.Middleware(db)) server.router.Use(redis.Middleware(rc)) server.router.Use(middleware.RealIP) server.router.Use(middleware.Logger) server.router.Use(middleware.Timeout(timeout)) server.router.Use(database.Middleware(db)) server.router.Use(auth.Middleware(server.conf, apiconf)) return server } func (server *Server) WithMiddleware( middlewares ...func(http.Handler) http.Handler) *Server { server.router.Use(middlewares...) return server } func (server *Server) MakeServer() (*http.Server, net.Listener) { listen, err := net.Listen("tcp", config.Addr) if err != nil { panic(err) } log.Printf("Running on %s", config.Addr) return &http.Server{Handler: server.router}, listen }