mirror of https://git.sr.ht/~sircmpwn/gql.sr.ht
174 lines
4.2 KiB
Go
174 lines
4.2 KiB
Go
package gql
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
|
|
"git.sr.ht/~sircmpwn/getopt"
|
|
"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/gql.sr.ht/auth"
|
|
"git.sr.ht/~sircmpwn/gql.sr.ht/config"
|
|
"git.sr.ht/~sircmpwn/gql.sr.ht/crypto"
|
|
"git.sr.ht/~sircmpwn/gql.sr.ht/database"
|
|
"git.sr.ht/~sircmpwn/gql.sr.ht/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},
|
|
})
|
|
)
|
|
|
|
var (
|
|
debug bool
|
|
addr string
|
|
)
|
|
|
|
// Loads the application configuration, reads options from the command line,
|
|
// and initializes some internals based on these results.
|
|
func LoadConfig(defaultAddr string) ini.File {
|
|
addr = defaultAddr
|
|
var (
|
|
config ini.File
|
|
err error
|
|
)
|
|
opts, _, err := getopt.Getopts(os.Args, "b:d")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
switch opt.Option {
|
|
case 'b':
|
|
addr = opt.Value
|
|
case 'd':
|
|
debug = true
|
|
}
|
|
}
|
|
|
|
for _, path := range []string{"../config.ini", "/etc/sr.ht/config.ini"} {
|
|
config, err = ini.LoadFile(path)
|
|
if err == nil {
|
|
break
|
|
}
|
|
}
|
|
if err != nil {
|
|
log.Fatalf("Failed to load config file: %v", err)
|
|
}
|
|
|
|
crypto.InitCrypto(config)
|
|
return config
|
|
}
|
|
|
|
// Prepares a router with the sr.ht API middleware pre-configured. This
|
|
// connects to PostgreSQL to rig up the database middlewares.
|
|
func MakeRouter(service string, conf ini.File, schema graphql.ExecutableSchema,
|
|
middlewares ...func(http.Handler) http.Handler) chi.Router {
|
|
|
|
pgcs, ok := conf.Get(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 := 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", service)
|
|
|
|
var timeout time.Duration
|
|
if to, ok := conf.Get(apiconf, "max-duration"); ok {
|
|
timeout, err = time.ParseDuration(to)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
} else {
|
|
timeout = 3 * time.Second
|
|
}
|
|
|
|
router := chi.NewRouter()
|
|
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()
|
|
})
|
|
})
|
|
router.Use(config.Middleware(conf, service))
|
|
router.Use(database.Middleware(db))
|
|
router.Use(redis.Middleware(rc))
|
|
router.Use(middleware.RealIP)
|
|
router.Use(middleware.Logger)
|
|
router.Use(middleware.Timeout(timeout))
|
|
router.Use(database.Middleware(db))
|
|
router.Use(auth.Middleware(conf, apiconf))
|
|
router.Use(middlewares...)
|
|
|
|
var complexity int
|
|
if limit, ok := conf.Get(apiconf, "max-complexity"); ok {
|
|
complexity, err = strconv.Atoi(limit)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
} else {
|
|
complexity = 250
|
|
}
|
|
|
|
// XXX: EmailRecover doesn't need to take config now that it's on the
|
|
// request context
|
|
srv := handler.GraphQL(schema,
|
|
handler.ComplexityLimit(complexity),
|
|
handler.RecoverFunc(EmailRecover(conf, debug, service)))
|
|
|
|
router.Handle("/query", srv)
|
|
router.Handle("/query/metrics", promhttp.Handler())
|
|
|
|
if debug {
|
|
router.Handle("/", playground.Handler("GraphQL playground", "/query"))
|
|
}
|
|
|
|
return router
|
|
}
|
|
|
|
// Runs the API server.
|
|
func ListenAndServe(router chi.Router) {
|
|
log.Printf("running on %s", addr)
|
|
log.Fatal(http.ListenAndServe(addr, router))
|
|
}
|