[NOD-310] Implement REST server in API server (#389)

* [NOD-310] Implement REST server in API server

* [NOD-310] MetaData -> Metadata

* [NOD-310] Make custom context methods instead of custom request functions

* [NOD-310] change "Request ID" prefix to "RID" and convert to apiServerContext with newAPIServerContext everywhere
This commit is contained in:
Ori Newman 2019-09-01 17:03:43 +03:00 committed by Svarog
parent 893b8a88c8
commit 169e96e851
11 changed files with 213 additions and 29 deletions

View File

@ -2,6 +2,7 @@ package main
import (
"errors"
"github.com/daglabs/btcd/apiserver/logger"
"github.com/daglabs/btcd/util"
"github.com/jessevdk/go-flags"
"path/filepath"
@ -14,8 +15,9 @@ const (
var (
// Default configuration options
defaultLogDir = util.AppDataDir("apiserver", false)
defaultDBHost = "localhost:3306"
defaultLogDir = util.AppDataDir("apiserver", false)
defaultDBAddr = "localhost:3306"
defaultHTTPListen = "0.0.0.0:8080"
)
type config struct {
@ -28,12 +30,14 @@ type config struct {
DBHost string `long:"dbhost" description:"Database host"`
DBUser string `long:"dbuser" description:"Database user" required:"true"`
DBPassword string `long:"dbpass" description:"Database password" required:"true"`
HTTPListen string `long:"listen" description:"HTTP address to listen on (default: 0.0.0.0:8080)"`
}
func parseConfig() (*config, error) {
cfg := &config{
LogDir: defaultLogDir,
DBHost: defaultDBHost,
LogDir: defaultLogDir,
DBHost: defaultDBAddr,
HTTPListen: defaultHTTPListen,
}
parser := flags.NewParser(cfg, flags.PrintErrors|flags.HelpFlag)
_, err := parser.Parse()
@ -52,7 +56,7 @@ func parseConfig() (*config, error) {
logFile := filepath.Join(cfg.LogDir, defaultLogFilename)
errLogFile := filepath.Join(cfg.LogDir, defaultErrLogFilename)
initLog(logFile, errLogFile)
logger.InitLog(logFile, errLogFile)
return cfg, nil
}

View File

@ -1,27 +1,11 @@
package main
import (
"fmt"
"github.com/daglabs/btcd/logs"
"github.com/daglabs/btcd/logger"
"github.com/daglabs/btcd/util/panics"
"os"
)
var (
backendLog = logs.NewBackend()
log = backendLog.Logger("APIS")
spawn = panics.GoroutineWrapperFunc(log, backendLog)
log = logger.BackendLog.Logger("APIS")
spawn = panics.GoroutineWrapperFunc(log, logger.BackendLog)
)
func initLog(logFile, errLogFile string) {
err := backendLog.AddLogFile(logFile, logs.LevelTrace)
if err != nil {
fmt.Fprintf(os.Stderr, "Error adding log file %s as log rotator for level %s: %s", logFile, logs.LevelTrace, err)
os.Exit(1)
}
err = backendLog.AddLogFile(errLogFile, logs.LevelWarn)
if err != nil {
fmt.Fprintf(os.Stderr, "Error adding log file %s as log rotator for level %s: %s", errLogFile, logs.LevelWarn, err)
os.Exit(1)
}
}

View File

@ -0,0 +1,24 @@
package logger
import (
"fmt"
"github.com/daglabs/btcd/logs"
"os"
)
// BackendLog is the logging backend used to create all subsystem loggers.
var BackendLog = logs.NewBackend()
// InitLog attaches log file and error log file to the backend log.
func InitLog(logFile, errLogFile string) {
err := BackendLog.AddLogFile(logFile, logs.LevelTrace)
if err != nil {
fmt.Fprintf(os.Stderr, "Error adding log file %s as log rotator for level %s: %s", logFile, logs.LevelTrace, err)
os.Exit(1)
}
err = BackendLog.AddLogFile(errLogFile, logs.LevelWarn)
if err != nil {
fmt.Fprintf(os.Stderr, "Error adding log file %s as log rotator for level %s: %s", errLogFile, logs.LevelWarn, err)
os.Exit(1)
}
}

View File

@ -2,12 +2,14 @@ package main
import (
"fmt"
"github.com/daglabs/btcd/apiserver/server"
"github.com/daglabs/btcd/logger"
"github.com/daglabs/btcd/signal"
"github.com/daglabs/btcd/util/panics"
)
func main() {
defer panics.HandlePanic(log, backendLog)
defer panics.HandlePanic(log, logger.BackendLog)
cfg, err := parseConfig()
if err != nil {
@ -18,13 +20,17 @@ func main() {
if err != nil {
panic(fmt.Errorf("Error connecting to servers: %s", err))
}
defer disconnect(client)
shutdownServer := server.Start(cfg.HTTPListen)
defer func() {
shutdownServer()
disconnectFromNode(client)
}()
interrupt := signal.InterruptListener()
<-interrupt
}
func disconnect(client *apiServerClient) {
func disconnectFromNode(client *apiServerClient) {
log.Infof("Disconnecting client")
client.Disconnect()
}

View File

@ -0,0 +1,62 @@
package server
import (
"context"
"fmt"
)
type contextKey string
const (
contextKeyRequestID contextKey = "REQUEST_ID"
)
type apiServerContext struct {
context.Context
}
func newAPIServerContext(ctx context.Context) *apiServerContext {
if asCtx, ok := ctx.(*apiServerContext); ok {
return asCtx
}
return &apiServerContext{Context: ctx}
}
func (ctx *apiServerContext) setRequestID(requestID uint64) context.Context {
context.WithValue(ctx, contextKeyRequestID, nextRequestID)
return ctx
}
func (ctx *apiServerContext) requestID() uint64 {
id := ctx.Value(contextKeyRequestID)
return id.(uint64)
}
func (ctx *apiServerContext) getLogString(format string, params ...interface{}) string {
params = append(params, ctx.requestID())
return fmt.Sprintf("RID %d: "+format, params)
}
func (ctx *apiServerContext) tracef(format string, params ...interface{}) {
log.Tracef(ctx.getLogString(format, params))
}
func (ctx *apiServerContext) debugf(format string, params ...interface{}) {
log.Debugf(ctx.getLogString(format, params))
}
func (ctx *apiServerContext) infof(format string, params ...interface{}) {
log.Infof(ctx.getLogString(format, params))
}
func (ctx *apiServerContext) warnf(format string, params ...interface{}) {
log.Warnf(ctx.getLogString(format, params))
}
func (ctx *apiServerContext) errorf(format string, params ...interface{}) {
log.Errorf(ctx.getLogString(format, params))
}
func (ctx *apiServerContext) criticalf(format string, params ...interface{}) {
log.Criticalf(ctx.getLogString(format, params))
}

9
apiserver/server/log.go Normal file
View File

@ -0,0 +1,9 @@
package server
import "github.com/daglabs/btcd/util/panics"
import "github.com/daglabs/btcd/apiserver/logger"
var (
log = logger.BackendLog.Logger("REST")
spawn = panics.GoroutineWrapperFunc(log, logger.BackendLog)
)

View File

@ -0,0 +1,48 @@
package server
import (
"fmt"
"net/http"
)
var nextRequestID uint64 = 1
func addRequestMetadataMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rCtx := newAPIServerContext(r.Context()).setRequestID(nextRequestID)
r.WithContext(rCtx)
nextRequestID++
next.ServeHTTP(w, r)
})
}
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := newAPIServerContext(r.Context())
ctx.infof("Method: %s URI: %s", r.Method, r.RequestURI)
next.ServeHTTP(w, r)
})
}
func recoveryMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := newAPIServerContext(r.Context())
var errStr string
defer func() {
recoveryErr := recover()
if recoveryErr != nil {
switch t := recoveryErr.(type) {
case string:
errStr = t
case error:
errStr = t.Error()
default:
errStr = "unknown error"
}
ctx.errorf("got error: %s", errStr)
http.Error(w, fmt.Sprintf("got error in request %d: %s", ctx.requestID(), errStr), http.StatusInternalServerError)
}
}()
h.ServeHTTP(w, r)
})
}

View File

@ -0,0 +1,44 @@
package server
import (
"context"
"fmt"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"net/http"
"time"
)
const gracefulShutdownTimeout = 30 * time.Second
func mainHandler(w http.ResponseWriter, r *http.Request) {
_, err := fmt.Fprintf(w, "API Server is running")
if err != nil {
panic(err)
}
}
// Start starts the HTTP REST server and returns a
// function to gracefully shutdown it.
func Start(listenAddr string) func() {
router := mux.NewRouter()
router.Use(addRequestMetadataMiddleware)
router.Use(recoveryMiddleware)
router.Use(loggingMiddleware)
router.HandleFunc("/", mainHandler)
httpServer := &http.Server{
Addr: listenAddr,
Handler: handlers.CORS()(router),
}
spawn(func() {
log.Errorf("%s", httpServer.ListenAndServe())
})
return func() {
ctx, cancel := context.WithTimeout(context.Background(), gracefulShutdownTimeout)
defer cancel()
err := httpServer.Shutdown(ctx)
if err != nil {
log.Errorf("Error shutting down http httpServer: %s", err)
}
}
}

2
go.mod
View File

@ -12,6 +12,8 @@ require (
github.com/btcsuite/winsvc v1.0.0
github.com/davecgh/go-spew v1.1.1
github.com/golang/protobuf v1.3.1 // indirect
github.com/gorilla/handlers v1.4.2
github.com/gorilla/mux v1.6.2
github.com/jessevdk/go-flags v1.4.0
github.com/jinzhu/gorm v1.9.10
github.com/jrick/logrotate v1.0.0

3
go.sum
View File

@ -55,6 +55,9 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg=
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=

View File

@ -24,8 +24,6 @@ import (
// InitLog.
var (
// BackendLog is the logging backend used to create all subsystem loggers.
// The backend must not be used before the log rotator has been initialized,
// or data races and/or nil pointer dereferences will occur.
BackendLog = logs.NewBackend()
adxrLog = BackendLog.Logger("ADXR")