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

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

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

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

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

36
const (
37
	MetricsNamespace = "op_indexer_api"
38
	addressParam     = "{address:%s}"
39 40

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

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

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

Will Cory's avatar
Will Cory committed
61 62 63
	mr := metrics.NewRegistry()
	promRecorder := metrics.NewPromHTTPRecorder(mr, MetricsNamespace)

64
	// (2) Inject routing middleware
Will Cory's avatar
Will Cory committed
65
	apiRouter.Use(chiMetricsMiddleware(promRecorder))
66
	apiRouter.Use(middleware.Recoverer)
67
	apiRouter.Use(middleware.Heartbeat(HealthPath))
68

69
	// (3) Set GET routes
70 71
	apiRouter.Get(fmt.Sprintf(DepositsPath+addressParam, ethereumAddressRegex), h.L1DepositsHandler)
	apiRouter.Get(fmt.Sprintf(WithdrawalsPath+addressParam, ethereumAddressRegex), h.L2WithdrawalsHandler)
72

73
	return &API{log: logger, router: apiRouter, metricsRegistry: mr, serverConfig: serverConfig, metricsConfig: metricsConfig}
Will Cory's avatar
Will Cory committed
74
}
Hamdi Allam's avatar
Hamdi Allam committed
75

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

81 82
	// (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
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
	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)
		}()
	}
101

102
	// (2) Start the API and metrics servers
Will Cory's avatar
Will Cory committed
103 104
	runProcess(a.startServer)
	runProcess(a.startMetricsServer)
105

106
	// (3) Wait for all processes to complete
Will Cory's avatar
Will Cory committed
107
	wg.Wait()
108

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

	return err
117
}
118

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

124
// startServer ... Starts the API server
125
func (a *API) startServer(ctx context.Context) error {
126 127 128 129 130 131
	a.log.Debug("api server listening...", "port", a.serverConfig.Port)
	addr := net.JoinHostPort(a.serverConfig.Host, strconv.Itoa(a.serverConfig.Port))
	srv, err := httputil.StartHTTPServer(addr, a.router)
	if err != nil {
		return fmt.Errorf("failed to start API server: %w", err)
	}
132

133
	host, portStr, err := net.SplitHostPort(srv.Addr().String())
134
	if err != nil {
135
		return errors.Join(err, srv.Close())
136
	}
137 138 139
	port, err := strconv.Atoi(portStr)
	if err != nil {
		return errors.Join(err, srv.Close())
140 141 142
	}

	// Update the port in the config in case the OS chose a different port
143
	// than the one we requested (e.g. using port 0 to fetch a random open port)
144 145
	a.serverConfig.Host = host
	a.serverConfig.Port = port
146

147 148 149
	<-ctx.Done()
	if err := srv.Stop(context.Background()); err != nil {
		return fmt.Errorf("failed to shutdown api server: %w", err)
150
	}
151
	return nil
Will Cory's avatar
Will Cory committed
152
}
153

154
// startMetricsServer ... Starts the metrics server
155
func (a *API) startMetricsServer(ctx context.Context) error {
156
	a.log.Debug("starting metrics server...", "port", a.metricsConfig.Port)
157
	srv, err := metrics.StartServer(a.metricsRegistry, a.metricsConfig.Host, a.metricsConfig.Port)
Will Cory's avatar
Will Cory committed
158
	if err != nil {
159
		return fmt.Errorf("failed to start metrics server: %w", err)
Will Cory's avatar
Will Cory committed
160
	}
161 162
	<-ctx.Done()
	defer a.log.Info("metrics server stopped")
163
	return srv.Stop(context.Background())
164
}