api.go 4.6 KB
Newer Older
1 2 3
package api

import (
Hamdi Allam's avatar
Hamdi Allam committed
4
	"context"
5
	"fmt"
6
	"net"
Will Cory's avatar
Will Cory committed
7
	"net/http"
Will Cory's avatar
Will Cory committed
8 9
	"runtime/debug"
	"sync"
Will Cory's avatar
Will Cory committed
10

11
	"github.com/ethereum-optimism/optimism/indexer/api/routes"
Will Cory's avatar
Will Cory committed
12
	"github.com/ethereum-optimism/optimism/indexer/config"
Will Cory's avatar
Will Cory committed
13
	"github.com/ethereum-optimism/optimism/indexer/database"
14
	"github.com/ethereum-optimism/optimism/op-service/metrics"
15
	"github.com/ethereum/go-ethereum/log"
16
	"github.com/go-chi/chi/v5"
17
	"github.com/go-chi/chi/v5/middleware"
Will Cory's avatar
Will Cory committed
18
	"github.com/prometheus/client_golang/prometheus"
19 20
)

21
const ethereumAddressRegex = `^0x[a-fA-F0-9]{40}$`
22

23 24
// Api ... Indexer API struct
// TODO : Structured error responses
25
type API struct {
Will Cory's avatar
Will Cory committed
26
	log             log.Logger
27
	router          *chi.Mux
Will Cory's avatar
Will Cory committed
28 29
	serverConfig    config.ServerConfig
	metricsConfig   config.ServerConfig
Will Cory's avatar
Will Cory committed
30
	metricsRegistry *prometheus.Registry
31 32
}

33
const (
34
	MetricsNamespace = "op_indexer_api"
35
	addressParam     = "{address:%s}"
36 37

	// Endpoint paths
38 39
	// NOTE - This can be further broken out over time as new version iterations
	// are implemented
40 41 42
	HealthPath      = "/healthz"
	DepositsPath    = "/api/v0/deposits/"
	WithdrawalsPath = "/api/v0/withdrawals/"
43 44
)

45
// chiMetricsMiddleware ... Injects a metrics recorder into request processing middleware
Will Cory's avatar
Will Cory committed
46
func chiMetricsMiddleware(rec metrics.HTTPRecorder) func(http.Handler) http.Handler {
Will Cory's avatar
Will Cory committed
47 48 49 50 51
	return func(next http.Handler) http.Handler {
		return metrics.NewHTTPRecordingMiddleware(rec, next)
	}
}

52
// NewApi ... Construct a new api instance
53
func NewApi(logger log.Logger, bv database.BridgeTransfersView, serverConfig config.ServerConfig, metricsConfig config.ServerConfig) *API {
54
	// (1) Initialize dependencies
55 56 57
	apiRouter := chi.NewRouter()
	h := routes.NewRoutes(logger, bv, apiRouter)

Will Cory's avatar
Will Cory committed
58 59 60
	mr := metrics.NewRegistry()
	promRecorder := metrics.NewPromHTTPRecorder(mr, MetricsNamespace)

61
	// (2) Inject routing middleware
Will Cory's avatar
Will Cory committed
62
	apiRouter.Use(chiMetricsMiddleware(promRecorder))
63
	apiRouter.Use(middleware.Recoverer)
64
	apiRouter.Use(middleware.Heartbeat(HealthPath))
65

66
	// (3) Set GET routes
67 68
	apiRouter.Get(fmt.Sprintf(DepositsPath+addressParam, ethereumAddressRegex), h.L1DepositsHandler)
	apiRouter.Get(fmt.Sprintf(WithdrawalsPath+addressParam, ethereumAddressRegex), h.L2WithdrawalsHandler)
69

70
	return &API{log: logger, router: apiRouter, metricsRegistry: mr, serverConfig: serverConfig, metricsConfig: metricsConfig}
Will Cory's avatar
Will Cory committed
71
}
Hamdi Allam's avatar
Hamdi Allam committed
72

73 74
// Run ... Runs the API server routines
func (a *API) Run(ctx context.Context) error {
Will Cory's avatar
Will Cory committed
75 76 77
	var wg sync.WaitGroup
	errCh := make(chan error, 2)

78 79
	// (1) Construct an inner function that will start a goroutine
	//    and handle any panics that occur on a shared error channel
Will Cory's avatar
Will Cory committed
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
	processCtx, processCancel := context.WithCancel(ctx)
	runProcess := func(start func(ctx context.Context) error) {
		wg.Add(1)
		go func() {
			defer func() {
				if err := recover(); err != nil {
					a.log.Error("halting api on panic", "err", err)
					debug.PrintStack()
					errCh <- fmt.Errorf("panic: %v", err)
				}

				processCancel()
				wg.Done()
			}()

			errCh <- start(processCtx)
		}()
	}
98

99
	// (2) Start the API and metrics servers
Will Cory's avatar
Will Cory committed
100 101
	runProcess(a.startServer)
	runProcess(a.startMetricsServer)
102

103
	// (3) Wait for all processes to complete
Will Cory's avatar
Will Cory committed
104
	wg.Wait()
105

Will Cory's avatar
Will Cory committed
106
	err := <-errCh
Hamdi Allam's avatar
Hamdi Allam committed
107
	if err != nil {
Will Cory's avatar
Will Cory committed
108
		a.log.Error("api stopped", "err", err)
Hamdi Allam's avatar
Hamdi Allam committed
109
	} else {
Will Cory's avatar
Will Cory committed
110
		a.log.Info("api stopped")
Hamdi Allam's avatar
Hamdi Allam committed
111 112 113
	}

	return err
114
}
115

116 117 118 119 120
// Port ... Returns the the port that server is listening on
func (a *API) Port() int {
	return a.serverConfig.Port
}

121
// startServer ... Starts the API server
122
func (a *API) startServer(ctx context.Context) error {
Will Cory's avatar
Will Cory committed
123
	a.log.Info("api server listening...", "port", a.serverConfig.Port)
124
	server := http.Server{Addr: fmt.Sprintf(":%d", a.serverConfig.Port), Handler: a.router}
125 126 127 128 129 130 131 132 133 134 135 136 137

	addr := fmt.Sprintf(":%d", a.serverConfig.Port)
	listener, err := net.Listen("tcp", addr)
	if err != nil {
		a.log.Error("Listen:", err)
		return err
	}
	tcpAddr, ok := listener.Addr().(*net.TCPAddr)
	if !ok {
		return fmt.Errorf("failed to get TCP address from network listener")
	}

	// Update the port in the config in case the OS chose a different port
138
	// than the one we requested (e.g. using port 0 to fetch a random open port)
139 140
	a.serverConfig.Port = tcpAddr.Port

141
	err = http.Serve(listener, server.Handler)
142
	if err != nil {
143
		a.log.Error("api server stopped with error", "err", err)
144 145 146
	} else {
		a.log.Info("api server stopped")
	}
Will Cory's avatar
Will Cory committed
147 148
	return err
}
149

150
// startMetricsServer ... Starts the metrics server
151
func (a *API) startMetricsServer(ctx context.Context) error {
Will Cory's avatar
Will Cory committed
152 153 154 155 156 157 158
	a.log.Info("starting metrics server...", "port", a.metricsConfig.Port)
	err := metrics.ListenAndServe(ctx, a.metricsRegistry, a.metricsConfig.Host, a.metricsConfig.Port)
	if err != nil {
		a.log.Error("metrics server stopped", "err", err)
	} else {
		a.log.Info("metrics server stopped")
	}
159 160
	return err
}