api.go 5.41 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"
9
	"strconv"
10
	"sync/atomic"
11
	"time"
12 13 14 15 16 17

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
	"github.com/prometheus/client_golang/prometheus"

	"github.com/ethereum/go-ethereum/log"
Will Cory's avatar
Will Cory committed
18

19
	"github.com/ethereum-optimism/optimism/indexer/api/routes"
Will Cory's avatar
Will Cory committed
20
	"github.com/ethereum-optimism/optimism/indexer/config"
Will Cory's avatar
Will Cory committed
21
	"github.com/ethereum-optimism/optimism/indexer/database"
22
	"github.com/ethereum-optimism/optimism/op-service/httputil"
23
	"github.com/ethereum-optimism/optimism/op-service/metrics"
24 25
)

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

28
const (
29
	MetricsNamespace = "op_indexer_api"
30
	addressParam     = "{address:%s}"
31 32

	// Endpoint paths
33 34
	// NOTE - This can be further broken out over time as new version iterations
	// are implemented
35 36 37
	HealthPath      = "/healthz"
	DepositsPath    = "/api/v0/deposits/"
	WithdrawalsPath = "/api/v0/withdrawals/"
38 39

	SupplyPath = "/api/v0/supply"
40 41
)

42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
// Api ... Indexer API struct
// TODO : Structured error responses
type APIService struct {
	log    log.Logger
	router *chi.Mux

	bv      database.BridgeTransfersView
	dbClose func() error

	metricsRegistry *prometheus.Registry

	apiServer     *httputil.HTTPServer
	metricsServer *httputil.HTTPServer

	stopped atomic.Bool
}

59
// chiMetricsMiddleware ... Injects a metrics recorder into request processing middleware
Will Cory's avatar
Will Cory committed
60
func chiMetricsMiddleware(rec metrics.HTTPRecorder) func(http.Handler) http.Handler {
Will Cory's avatar
Will Cory committed
61 62 63 64 65
	return func(next http.Handler) http.Handler {
		return metrics.NewHTTPRecordingMiddleware(rec, next)
	}
}

66
// NewApi ... Construct a new api instance
67 68 69 70 71 72 73
func NewApi(ctx context.Context, log log.Logger, cfg *Config) (*APIService, error) {
	out := &APIService{log: log, metricsRegistry: metrics.NewRegistry()}
	if err := out.initFromConfig(ctx, cfg); err != nil {
		return nil, errors.Join(err, out.Stop(ctx)) // close any resources we may have opened already
	}
	return out, nil
}
74

75 76 77 78 79 80 81
func (a *APIService) initFromConfig(ctx context.Context, cfg *Config) error {
	if err := a.initDB(ctx, cfg.DB); err != nil {
		return fmt.Errorf("failed to init DB: %w", err)
	}
	if err := a.startMetricsServer(cfg.MetricsServer); err != nil {
		return fmt.Errorf("failed to start metrics server: %w", err)
	}
82
	a.initRouter(cfg.HTTPServer)
83 84 85 86 87
	if err := a.startServer(cfg.HTTPServer); err != nil {
		return fmt.Errorf("failed to start API server: %w", err)
	}
	return nil
}
88

89 90 91 92
func (a *APIService) Start(ctx context.Context) error {
	// Completed all setup-up jobs at init-time already,
	// and the API service does not have any other special starting routines or background-jobs to start.
	return nil
Will Cory's avatar
Will Cory committed
93
}
Hamdi Allam's avatar
Hamdi Allam committed
94

95 96 97 98 99 100 101 102 103 104 105
func (a *APIService) Stop(ctx context.Context) error {
	var result error
	if a.apiServer != nil {
		if err := a.apiServer.Stop(ctx); err != nil {
			result = errors.Join(result, fmt.Errorf("failed to stop API server: %w", err))
		}
	}
	if a.metricsServer != nil {
		if err := a.metricsServer.Stop(ctx); err != nil {
			result = errors.Join(result, fmt.Errorf("failed to stop metrics server: %w", err))
		}
Will Cory's avatar
Will Cory committed
106
	}
107 108 109 110 111 112 113 114 115
	if a.dbClose != nil {
		if err := a.dbClose(); err != nil {
			result = errors.Join(result, fmt.Errorf("failed to close DB: %w", err))
		}
	}
	a.stopped.Store(true)
	a.log.Info("API service shutdown complete")
	return result
}
116

117 118 119
func (a *APIService) Stopped() bool {
	return a.stopped.Load()
}
120

121 122 123 124 125 126 127
// Addr ... returns the address that the HTTP server is listening on (excl. http:// prefix, just the host and port)
func (a *APIService) Addr() string {
	if a.apiServer == nil {
		return ""
	}
	return a.apiServer.Addr().String()
}
128

129 130
func (a *APIService) initDB(ctx context.Context, connector DBConnector) error {
	db, err := connector.OpenDB(ctx, a.log)
Hamdi Allam's avatar
Hamdi Allam committed
131
	if err != nil {
132
		return fmt.Errorf("failed to connect to databse: %w", err)
Hamdi Allam's avatar
Hamdi Allam committed
133
	}
134 135 136
	a.dbClose = db.Closer
	a.bv = db.BridgeTransfers
	return nil
137
}
138

139
func (a *APIService) initRouter(apiConfig config.ServerConfig) {
140 141 142 143 144 145
	apiRouter := chi.NewRouter()
	h := routes.NewRoutes(a.log, a.bv, apiRouter)

	promRecorder := metrics.NewPromHTTPRecorder(a.metricsRegistry, MetricsNamespace)

	apiRouter.Use(chiMetricsMiddleware(promRecorder))
146
	apiRouter.Use(middleware.Timeout(time.Duration(apiConfig.WriteTimeout) * time.Second))
147 148 149 150 151
	apiRouter.Use(middleware.Recoverer)
	apiRouter.Use(middleware.Heartbeat(HealthPath))

	apiRouter.Get(fmt.Sprintf(DepositsPath+addressParam, ethereumAddressRegex), h.L1DepositsHandler)
	apiRouter.Get(fmt.Sprintf(WithdrawalsPath+addressParam, ethereumAddressRegex), h.L2WithdrawalsHandler)
152
	apiRouter.Get(SupplyPath, h.SupplyView)
153
	a.router = apiRouter
154 155
}

156
// startServer ... Starts the API server
157 158 159
func (a *APIService) startServer(serverConfig config.ServerConfig) error {
	a.log.Debug("API server listening...", "port", serverConfig.Port)
	addr := net.JoinHostPort(serverConfig.Host, strconv.Itoa(serverConfig.Port))
160 161 162 163
	srv, err := httputil.StartHTTPServer(addr, a.router)
	if err != nil {
		return fmt.Errorf("failed to start API server: %w", err)
	}
164 165
	a.log.Info("API server started", "addr", srv.Addr().String())
	a.apiServer = srv
166
	return nil
Will Cory's avatar
Will Cory committed
167
}
168

169
// startMetricsServer ... Starts the metrics server
170 171 172
func (a *APIService) startMetricsServer(metricsConfig config.ServerConfig) error {
	a.log.Debug("starting metrics server...", "port", metricsConfig.Port)
	srv, err := metrics.StartServer(a.metricsRegistry, metricsConfig.Host, metricsConfig.Port)
Will Cory's avatar
Will Cory committed
173
	if err != nil {
174
		return fmt.Errorf("failed to start metrics server: %w", err)
Will Cory's avatar
Will Cory committed
175
	}
176 177 178
	a.log.Info("Metrics server started", "addr", srv.Addr().String())
	a.metricsServer = srv
	return nil
179
}