api.go 5.5 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

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
15

16 17 18
	"github.com/prometheus/client_golang/prometheus"

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

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

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

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

	// Endpoint paths
35
	DocsPath        = "/docs"
36 37 38
	HealthPath      = "/healthz"
	DepositsPath    = "/api/v0/deposits/"
	WithdrawalsPath = "/api/v0/withdrawals/"
39 40

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

43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
// 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
}

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

67
// NewApi ... Construct a new api instance
68 69 70 71 72 73 74
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
}
75

76 77 78 79 80 81 82
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)
	}
83
	a.initRouter(cfg.HTTPServer)
84 85 86 87 88
	if err := a.startServer(cfg.HTTPServer); err != nil {
		return fmt.Errorf("failed to start API server: %w", err)
	}
	return nil
}
89

90 91 92 93
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
94
}
Hamdi Allam's avatar
Hamdi Allam committed
95

96 97 98 99 100 101 102 103 104 105 106
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
107
	}
108 109 110 111 112 113 114 115 116
	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
}
117

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

122 123 124 125 126 127 128
// 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()
}
129

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

140
func (a *APIService) initRouter(apiConfig config.ServerConfig) {
141 142 143
	v := new(service.Validator)

	svc := service.New(v, a.bv, a.log)
144
	apiRouter := chi.NewRouter()
145
	h := routes.NewRoutes(a.log, apiRouter, svc)
146

147
	apiRouter.Use(middleware.Logger)
148
	apiRouter.Use(middleware.Timeout(time.Duration(apiConfig.WriteTimeout) * time.Second))
149 150
	apiRouter.Use(middleware.Recoverer)
	apiRouter.Use(middleware.Heartbeat(HealthPath))
151
	apiRouter.Use(chiMetricsMiddleware(metrics.NewPromHTTPRecorder(a.metricsRegistry, MetricsNamespace)))
152 153 154

	apiRouter.Get(fmt.Sprintf(DepositsPath+addressParam, ethereumAddressRegex), h.L1DepositsHandler)
	apiRouter.Get(fmt.Sprintf(WithdrawalsPath+addressParam, ethereumAddressRegex), h.L2WithdrawalsHandler)
155
	apiRouter.Get(SupplyPath, h.SupplyView)
156
	apiRouter.Get(DocsPath, h.DocsHandler)
157
	a.router = apiRouter
158 159
}

160
// startServer ... Starts the API server
161 162
func (a *APIService) startServer(serverConfig config.ServerConfig) error {
	a.log.Debug("API server listening...", "port", serverConfig.Port)
163

164
	addr := net.JoinHostPort(serverConfig.Host, strconv.Itoa(serverConfig.Port))
165 166 167 168
	srv, err := httputil.StartHTTPServer(addr, a.router)
	if err != nil {
		return fmt.Errorf("failed to start API server: %w", err)
	}
169

170 171
	a.log.Info("API server started", "addr", srv.Addr().String())
	a.apiServer = srv
172
	return nil
Will Cory's avatar
Will Cory committed
173
}
174

175
// startMetricsServer ... Starts the metrics server
176 177 178
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
179
	if err != nil {
180
		return fmt.Errorf("failed to start metrics server: %w", err)
Will Cory's avatar
Will Cory committed
181
	}
182 183 184
	a.log.Info("Metrics server started", "addr", srv.Addr().String())
	a.metricsServer = srv
	return nil
185
}