diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml deleted file mode 100644 index a9b6981..0000000 --- a/.github/workflows/deploy.yaml +++ /dev/null @@ -1,77 +0,0 @@ -name: deploy - -on: - workflow_dispatch: - inputs: - commit: - description: Commit SHA to deploy. Default to main branch's commit. - environment: - type: choice - options: - - rpc-gateway-arbitrum - - rpc-gateway-avalanche - - rpc-gateway-bsc - - rpc-gateway-celo - - rpc-gateway-ethereum - - rpc-gateway-ethereum-staging - - rpc-gateway-fantom - - rpc-gateway-optimism - - rpc-gateway-polygon - required: true - -env: - IMAGE_NAME: 883408475785.dkr.ecr.us-east-1.amazonaws.com/rpc-gateway - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Validate Input - id: validate_input - uses: actions/github-script@v7 - with: - script: | - const { environment, commit } = context.payload.inputs - core.setOutput('environment', environment) - core.setOutput('commit', commit ? commit : context.sha) - - uses: jwalton/gh-find-current-pr@v1 - id: findPr - with: - state: closed - - name: Checkout 0x-main-infra repo - uses: actions/checkout@v4 - with: - token: ${{ secrets.PAT }} - repository: 0xProject/0x-main-infra - - name: Update GitOps repo - id: update_gitops_repo - run: | - cd 'clusters/main-cluster/kubernetes/${{ steps.validate_input.outputs.environment }}' - # Read current image tag - current_image_tag=$(yq e '.images[] | select(.name | contains("rpc-gateway")) | .newTag' kustomization.yaml) - - # Update image tags - kustomize edit set image ${IMAGE_NAME}:${{ steps.validate_input.outputs.commit }} - - # Output for next steps - echo "##[set-output name=current_image_tag;]${current_image_tag}" - - name: Create Pull Request - uses: peter-evans/create-pull-request@v6 - with: - token: ${{ secrets.PAT }} - author: ${{ github.event.sender.login }} <${{ github.event.sender.login }}@users.noreply.github.com> - commit-message: 'rpc-gateway deploy to ${{ steps.validate_input.outputs.environment }}: ${{ steps.validate_input.outputs.commit }}' - title: '[rpc-gateway][deploy][${{ steps.validate_input.outputs.environment }}] Revision ${{ steps.validate_input.outputs.commit }}' - branch: 'rpc-gateway/deploy/${{ steps.validate_input.outputs.environment }}' - body: | - ### Deployment Details - Triggered by @${{ github.event.sender.login }} - PR: https://github.com/${{ github.repository }}/pull/${{ steps.findPr.outputs.number }} - - **Current version:** https://github.com/${{ github.repository }}/commit/${{ steps.update_gitops_repo.outputs.current_image_tag }} - **Target version:** https://github.com/${{ github.repository }}/commit/${{ steps.validate_input.outputs.commit }} - - Comparison: https://github.com/${{ github.repository }}/compare/${{ steps.update_gitops_repo.outputs.current_image_tag }}...${{ steps.validate_input.outputs.commit }} - - name: Check outputs - run: | - echo "Pull Request Number - ${{ env.PULL_REQUEST_NUMBER }}" diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml deleted file mode 100644 index d7c0754..0000000 --- a/.github/workflows/docker.yaml +++ /dev/null @@ -1,53 +0,0 @@ -name: docker - -on: - pull_request: - branches: - - main - push: - branches: - - main - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: hadolint/hadolint-action@v3.1.0 - with: - recursive: true - ignore: DL3018,DL3019 - - build: - runs-on: ubuntu-latest - permissions: - id-token: write - contents: read - steps: - - uses: actions/checkout@v4 - - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: ${{ secrets.AWSROLE }} - aws-region: ${{ secrets.AWSREGION }} - if: ${{ github.actor != 'dependabot[bot]' }} - - uses: aws-actions/amazon-ecr-login@v2 - if: ${{ github.actor != 'dependabot[bot]' }} - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 - - uses: docker/build-push-action@v5 - with: - push: ${{ github.ref == 'refs/heads/main' }} - platforms: linux/amd64,linux/arm64 - tags: ${{ secrets.REGISTRY }}/rpc-gateway:${{ github.sha }} - if: ${{ github.actor != 'dependabot[bot]' }} - - uses: docker/build-push-action@v5 - with: - push: false - platforms: linux/amd64,linux/arm64 - tags: ${{ secrets.REGISTRY }}/rpc-gateway:${{ github.sha }} - if: ${{ github.actor == 'dependabot[bot]' }} diff --git a/.gitignore b/.gitignore index 1b4e896..a75541c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,12 @@ -/rpc-gateway +/app/rpc-gateway /main /tags /vendor + + +.idea + +config.yml +config_* + +prometheus \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index eb88fa9..bf18b7f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -66,7 +66,7 @@ linters-settings: main: allow: - "$gostd" - - github.com/0xProject/rpc-gateway + - github.com/sygmaprotocol/rpc-gateway - github.com/Shopify/toxiproxy - github.com/ethereum/go-ethereum - github.com/gorilla/mux diff --git a/README.md b/README.md index a4fb202..c7a6fa4 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,14 @@ -> [!CAUTION] -> The rpc-gateway is in development mode, and you should not consider it -> stable yet. +# RPC Gateway -## RPC Gateway +The rpc-gateway is a failover proxy designed for node providers. It ensures high availability and reliability by automatically rerouting requests to a backup node provider when health checks indicate the primary provider is down. This process ensures uninterrupted service even in the event of node provider failures. -The rpc-gateway is a failover proxy for node providers. When health checks -fail, the rpc-gateway automatically routes requests to a backup node provider. +## Caution + +> :warning: The rpc-gateway is currently in development mode. It is not considered stable and should be used with caution in production environments. + +## Overview + +The rpc-gateway operates by continuously performing health checks on configured node providers. If the primary node provider fails these checks, the gateway will automatically attempt to route requests to the next available provider based on a predefined failover sequence. ```mermaid sequenceDiagram @@ -27,39 +30,56 @@ RPC Gateway-->>Alice: {"result":[...]} ## Development -Make sure the test pass +To contribute to the development of rpc-gateway, ensure that you have Go installed and the project set up locally. Start by running tests to ensure everything is working as expected. + ```console go test -v ./... ``` -To run the app locally +For local development and testing, you can run the application with: + ```console -DEBUG=true go run . --config example_config.yml +DEBUG=true go run . --config config.yml ``` ## Configuration +A main YAML configuration (`config.yml`) specifies the metrics server port and multiple gateways, each with its own `.yml` configuration file: + ```yaml metrics: - port: "9090" # port for prometheus metrics, served on /metrics and / + port: 9090 # Port for Prometheus metrics, served on /metrics and / +gateways: + - config-file: "config_holesky.yml" + name: "Holesky gateway" + - config-file: "config_sepolia.yml" + name: "Sepolia gateway" +``` + +Each `.yml` configuration file for the gateways can specify detailed settings for proxy behavior, health checks, and target node providers. Here is an example of what these individual gateway configuration files can contain: + +```yaml proxy: - port: "3000" # port for RPC gateway - upstreamTimeout: "1s" # when is a request considered timed out + port: "3000" # Port for RPC gateway + upstreamTimeout: "1s" # When is a request considered timed out healthChecks: - interval: "5s" # how often to do healthchecks - timeout: "1s" # when should the timeout occur and considered unhealthy - failureThreshold: 2 # how many failed checks until marked as unhealthy - successThreshold: 1 # how many successes to be marked as healthy again + interval: "5s" # How often to perform health checks + timeout: "1s" # Timeout duration for health checks + failureThreshold: 2 # Failed checks until a target is marked unhealthy + successThreshold: 1 # Successes required to mark a target healthy again -targets: # the order here determines the failover order +targets: # Failover order is determined by the list order - name: "Cloudflare" connection: - http: # ws is supported by default, it will be a sticky connection. + http: url: "https://cloudflare-eth.com" - name: "Alchemy" connection: - http: # ws is supported by default, it will be a sticky connection. + http: url: "https://alchemy.com/rpc/" ``` + +Any of these configuration files can be also loaded from URL if one is provided as path. +--- diff --git a/example_config.yml b/example_config.yml index 22dc448..9804e3e 100644 --- a/example_config.yml +++ b/example_config.yml @@ -3,23 +3,8 @@ metrics: port: 9090 # port for prometheus metrics, served on /metrics and / -proxy: - port: 3000 # port for RPC gateway - upstreamTimeout: "1s" # when is a request considered timed out - -healthChecks: - interval: "5s" # how often to do healthchecks - timeout: "1s" # when should the timeout occur and considered unhealthy - failureThreshold: 2 # how many failed checks until marked as unhealthy - successThreshold: 1 # how many successes to be marked as healthy again - -targets: - - name: "Ankr" - connection: - http: # ws is supported by default, it will be a sticky connection. - url: "https://rpc.ankr.com/eth" - # compression: true # Specify if the target supports request compression - - name: "Cloudflare" - connection: - http: - url: "https://cloudflare-eth.com" +gateways: + - configFile: "config_holesky.yml" + name: "Holesky gateway" + - configFile: "config_sepolia.yml" + name: "Sepolia gateway" \ No newline at end of file diff --git a/example_network_config.yml b/example_network_config.yml new file mode 100644 index 0000000..71334ab --- /dev/null +++ b/example_network_config.yml @@ -0,0 +1,20 @@ + +proxy: + port: "3000" # port for RPC gateway + upstreamTimeout: "1s" # when is a request considered timed out + +healthChecks: + interval: "5s" # how often to do healthchecks + timeout: "1s" # when should the timeout occur and considered unhealthy + failureThreshold: 2 # how many failed checks until marked as unhealthy + successThreshold: 1 # how many successes to be marked as healthy again + +targets: # the order here determines the failover order + - name: "Cloudflare" + connection: + http: # ws is supported by default, it will be a sticky connection. + url: "https://cloudflare-eth.com" + - name: "Alchemy" + connection: + http: # ws is supported by default, it will be a sticky connection. + url: "https://alchemy.com/rpc/" \ No newline at end of file diff --git a/go.mod b/go.mod index f010d0c..16afa0c 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/0xProject/rpc-gateway +module github.com/sygmaprotocol/rpc-gateway go 1.21 diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index 46d8a7d..2dda96f 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -25,7 +25,7 @@ func (s *Server) Stop() error { func NewServer(config Config) *Server { r := chi.NewRouter() - r.Use(middleware.Heartbeat("/healthz")) + r.Use(middleware.Heartbeat("/health")) r.Handle("/metrics", promhttp.Handler()) return &Server{ diff --git a/internal/proxy/config.go b/internal/proxy/config.go index f4c21f3..b376100 100644 --- a/internal/proxy/config.go +++ b/internal/proxy/config.go @@ -12,7 +12,7 @@ type HealthCheckConfig struct { } type ProxyConfig struct { // nolint:revive - Port string `yaml:"port"` + Path string `yaml:"path"` UpstreamTimeout time.Duration `yaml:"upstreamTimeout"` } @@ -22,4 +22,5 @@ type Config struct { Targets []NodeProviderConfig HealthChecks HealthCheckConfig HealthcheckManager *HealthCheckManager + Name string } diff --git a/internal/proxy/healthcheckmanager.go b/internal/proxy/healthcheckmanager.go index 5b67158..0f92ae1 100644 --- a/internal/proxy/healthcheckmanager.go +++ b/internal/proxy/healthcheckmanager.go @@ -28,12 +28,12 @@ type HealthCheckManager struct { metricRPCProviderGasLimit *prometheus.GaugeVec } -func NewHealthCheckManager(config HealthCheckManagerConfig) (*HealthCheckManager, error) { +func NewHealthCheckManager(config HealthCheckManagerConfig, name string) (*HealthCheckManager, error) { hcm := &HealthCheckManager{ logger: config.Logger, metricRPCProviderInfo: promauto.NewGaugeVec( prometheus.GaugeOpts{ - Name: "zeroex_rpc_gateway_provider_info", + Name: "zeroex_rpc_gateway_provider_info_" + name, Help: "Gas limit of a given provider", }, []string{ "index", @@ -41,7 +41,7 @@ func NewHealthCheckManager(config HealthCheckManagerConfig) (*HealthCheckManager }), metricRPCProviderStatus: promauto.NewGaugeVec( prometheus.GaugeOpts{ - Name: "zeroex_rpc_gateway_provider_status", + Name: "zeroex_rpc_gateway_provider_status_" + name, Help: "Current status of a given provider by type. Type can be either healthy or tainted.", }, []string{ "provider", @@ -49,14 +49,14 @@ func NewHealthCheckManager(config HealthCheckManagerConfig) (*HealthCheckManager }), metricRPCProviderBlockNumber: promauto.NewGaugeVec( prometheus.GaugeOpts{ - Name: "zeroex_rpc_gateway_provider_block_number", + Name: "zeroex_rpc_gateway_provider_block_number_" + name, Help: "Block number of a given provider", }, []string{ "provider", }), metricRPCProviderGasLimit: promauto.NewGaugeVec( prometheus.GaugeOpts{ - Name: "zeroex_rpc_gateway_provider_gasLimit_number", + Name: "zeroex_rpc_gateway_provider_gasLimit_number_" + name, Help: "Gas limit of a given provider", }, []string{ "provider", diff --git a/internal/proxy/nodeprovider.go b/internal/proxy/nodeprovider.go index 53c38e9..c95bafd 100644 --- a/internal/proxy/nodeprovider.go +++ b/internal/proxy/nodeprovider.go @@ -5,8 +5,8 @@ import ( "net/http/httputil" "strings" - "github.com/0xProject/rpc-gateway/internal/middleware" "github.com/go-http-utils/headers" + "github.com/sygmaprotocol/rpc-gateway/internal/middleware" ) type NodeProviderConnectionHTTPConfig struct { diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index cabf870..19dbd44 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -26,7 +26,7 @@ func NewProxy(config Config) (*Proxy, error) { timeout: config.Proxy.UpstreamTimeout, metricRequestDuration: promauto.NewHistogramVec( prometheus.HistogramOpts{ - Name: "zeroex_rpc_gateway_request_duration_seconds", + Name: "zeroex_rpc_gateway_request_duration_seconds_" + config.Name, Help: "Histogram of response time for Gateway in seconds", Buckets: []float64{ .025, @@ -50,7 +50,7 @@ func NewProxy(config Config) (*Proxy, error) { }), metricRequestErrors: promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "zeroex_rpc_gateway_request_errors_handled_total", + Name: "zeroex_rpc_gateway_request_errors_handled_total_" + config.Name, Help: "The total number of request errors handled by gateway", }, []string{ "provider", diff --git a/internal/proxy/proxy_test.go b/internal/proxy/proxy_test.go index 320c647..910d5b7 100644 --- a/internal/proxy/proxy_test.go +++ b/internal/proxy/proxy_test.go @@ -29,6 +29,7 @@ func createConfig() Config { SuccessThreshold: 0, }, Targets: []NodeProviderConfig{}, + Name: "test", } } @@ -70,7 +71,7 @@ func TestHttpFailoverProxyRerouteRequests(t *testing.T) { Targets: rpcGatewayConfig.Targets, Config: rpcGatewayConfig.HealthChecks, Logger: slog.New(slog.NewTextHandler(os.Stderr, nil)), - }) + }, "test") assert.NoError(t, err) assert.NotNil(t, healthcheckManager) @@ -128,7 +129,7 @@ func TestHttpFailoverProxyDecompressRequest(t *testing.T) { Targets: rpcGatewayConfig.Targets, Config: rpcGatewayConfig.HealthChecks, Logger: slog.New(slog.NewTextHandler(os.Stderr, nil)), - }) + }, "test") assert.NotNil(t, healthcheckManager) assert.NoError(t, err) @@ -189,7 +190,7 @@ func TestHttpFailoverProxyWithCompressionSupportedTarget(t *testing.T) { Targets: rpcGatewayConfig.Targets, Config: rpcGatewayConfig.HealthChecks, Logger: slog.New(slog.NewTextHandler(os.Stderr, nil)), - }) + }, "test") assert.NotNil(t, healthcheckManager) assert.NoError(t, err) @@ -260,7 +261,7 @@ func TestHTTPFailoverProxyWhenCannotConnectToPrimaryProvider(t *testing.T) { Targets: rpcGatewayConfig.Targets, Config: rpcGatewayConfig.HealthChecks, Logger: slog.New(slog.NewTextHandler(os.Stderr, nil)), - }) + }, "test") assert.NotNil(t, healthcheckManager) assert.NoError(t, err) diff --git a/internal/rpcgateway/config.go b/internal/rpcgateway/config.go index c271578..d892743 100644 --- a/internal/rpcgateway/config.go +++ b/internal/rpcgateway/config.go @@ -1,11 +1,12 @@ package rpcgateway import ( - "github.com/0xProject/rpc-gateway/internal/metrics" - "github.com/0xProject/rpc-gateway/internal/proxy" + "github.com/sygmaprotocol/rpc-gateway/internal/metrics" + "github.com/sygmaprotocol/rpc-gateway/internal/proxy" ) type RPCGatewayConfig struct { //nolint:revive + Name string `yaml:"name"` Metrics metrics.Config `yaml:"metrics"` Proxy proxy.ProxyConfig `yaml:"proxy"` HealthChecks proxy.HealthCheckConfig `yaml:"healthChecks"` diff --git a/internal/rpcgateway/rpcgateway.go b/internal/rpcgateway/rpcgateway.go index fd6e34f..29ae0bd 100644 --- a/internal/rpcgateway/rpcgateway.go +++ b/internal/rpcgateway/rpcgateway.go @@ -4,30 +4,20 @@ import ( "context" "fmt" "log/slog" - "net/http" "os" - "time" - "github.com/0xProject/rpc-gateway/internal/metrics" - "github.com/0xProject/rpc-gateway/internal/proxy" + "github.com/sygmaprotocol/rpc-gateway/internal/util" + "github.com/carlmjohnson/flowmatic" "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" - "github.com/go-chi/httplog/v2" "github.com/pkg/errors" - "gopkg.in/yaml.v2" + "github.com/sygmaprotocol/rpc-gateway/internal/proxy" ) type RPCGateway struct { - config RPCGatewayConfig - proxy *proxy.Proxy - hcm *proxy.HealthCheckManager - server *http.Server - metrics *metrics.Server -} - -func (r *RPCGateway) ServeHTTP(w http.ResponseWriter, req *http.Request) { - r.server.Handler.ServeHTTP(w, req) + config RPCGatewayConfig + proxy *proxy.Proxy + hcm *proxy.HealthCheckManager } func (r *RPCGateway) Start(c context.Context) error { @@ -35,12 +25,6 @@ func (r *RPCGateway) Start(c context.Context) error { func() error { return errors.Wrap(r.hcm.Start(c), "failed to start health check manager") }, - func() error { - return errors.Wrap(r.server.ListenAndServe(), "failed to start rpc-gateway") - }, - func() error { - return errors.Wrap(r.metrics.Start(), "failed to start metrics server") - }, ) } @@ -49,27 +33,15 @@ func (r *RPCGateway) Stop(c context.Context) error { func() error { return errors.Wrap(r.hcm.Stop(c), "failed to stop health check manager") }, - func() error { - return errors.Wrap(r.server.Close(), "failed to stop rpc-gateway") - }, - func() error { - return errors.Wrap(r.metrics.Stop(), "failed to stop metrics server") - }, ) } -func NewRPCGateway(config RPCGatewayConfig) (*RPCGateway, error) { +func NewRPCGateway(config RPCGatewayConfig, router *chi.Mux) (*RPCGateway, error) { logLevel := slog.LevelWarn if os.Getenv("DEBUG") == "true" { logLevel = slog.LevelDebug } - logger := httplog.NewLogger("rpc-gateway", httplog.Options{ - JSON: true, - RequestHeaders: true, - LogLevel: logLevel, - }) - hcm, err := proxy.NewHealthCheckManager( proxy.HealthCheckManagerConfig{ Targets: config.Targets, @@ -78,7 +50,7 @@ func NewRPCGateway(config RPCGatewayConfig) (*RPCGateway, error) { slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ Level: logLevel, })), - }) + }, config.Name) if err != nil { return nil, errors.Wrap(err, "healthcheckmanager failed") } @@ -89,55 +61,32 @@ func NewRPCGateway(config RPCGatewayConfig) (*RPCGateway, error) { Targets: config.Targets, HealthChecks: config.HealthChecks, HealthcheckManager: hcm, + Name: config.Name, }, ) if err != nil { return nil, errors.Wrap(err, "proxy failed") } - r := chi.NewRouter() - r.Use(httplog.RequestLogger(logger)) - - // Recoverer is a middleware that recovers from panics, logs the panic (and - // a backtrace), and returns a HTTP 500 (Internal Server Error) status if - // possible. Recoverer prints a request ID if one is provided. - // - r.Use(middleware.Recoverer) - - r.Handle("/", proxy) + router.Handle(fmt.Sprintf("/%s", config.Proxy.Path), proxy) return &RPCGateway{ config: config, proxy: proxy, hcm: hcm, - metrics: metrics.NewServer( - metrics.Config{ - Port: config.Metrics.Port, - }, - ), - server: &http.Server{ - Addr: fmt.Sprintf(":%s", config.Proxy.Port), - Handler: r, - WriteTimeout: time.Second * 15, - ReadTimeout: time.Second * 15, - ReadHeaderTimeout: time.Second * 5, - }, }, nil } // NewRPCGatewayFromConfigFile creates an instance of RPCGateway from provided // configuration file. -func NewRPCGatewayFromConfigFile(s string) (*RPCGateway, error) { - data, err := os.ReadFile(s) +func NewRPCGatewayFromConfigFile(fileOrURL string, router *chi.Mux) (*RPCGateway, error) { + config, err := util.LoadYamlFile[RPCGatewayConfig](fileOrURL) if err != nil { - return nil, err + return nil, errors.Wrap(err, "failed to load config") } - var config RPCGatewayConfig - - if err := yaml.Unmarshal(data, &config); err != nil { - return nil, err - } + fmt.Println("Starting RPC Gateway for " + config.Name + " on path: /" + config.Proxy.Path) - return NewRPCGateway(config) + // Pass the metrics router as an argument to NewRPCGateway. + return NewRPCGateway(*config, router) } diff --git a/internal/util/util.go b/internal/util/util.go new file mode 100644 index 0000000..0e7e6af --- /dev/null +++ b/internal/util/util.go @@ -0,0 +1,69 @@ +package util + +import ( + "errors" + "io" + "net/http" + "net/url" + "os" + + "gopkg.in/yaml.v2" +) + +// LoadYamlFile is refactored to use a generic type T. +// T must be a type that can be unmarshaled from JSON. +func LoadYamlFile[T any](pathOrURL string) (*T, error) { + var data []byte + var err error + + if isValidURL(pathOrURL) { + data, err = loadFileFromURL(pathOrURL) + if err != nil { + return nil, err + } + } else { + data, err = os.ReadFile(pathOrURL) + if err != nil { + return nil, err + } + } + + var config T + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, err + } + + return &config, nil +} + +func loadFileFromURL(pathOrURL string) ([]byte, error) { + req, err := http.NewRequest(http.MethodGet, pathOrURL, nil) + if err != nil { + return nil, err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, errors.New("failed to fetch config from URL") + } + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return data, nil +} + +func isValidURL(toTest string) bool { + u, err := url.Parse(toTest) + if err != nil { + return false + } + + return u.Scheme != "" && u.Host != "" +} diff --git a/main.go b/main.go index dbe2af8..9c5e369 100644 --- a/main.go +++ b/main.go @@ -3,16 +3,41 @@ package main import ( "context" "fmt" + "log/slog" + "net/http" "os" "os/signal" + "sync" "syscall" + "time" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/httplog/v2" + + "github.com/sygmaprotocol/rpc-gateway/internal/metrics" + "github.com/sygmaprotocol/rpc-gateway/internal/util" - "github.com/0xProject/rpc-gateway/internal/rpcgateway" - "github.com/carlmjohnson/flowmatic" "github.com/pkg/errors" + "github.com/sygmaprotocol/rpc-gateway/internal/rpcgateway" "github.com/urfave/cli/v2" ) +type MetricsConfig struct { + Port int `yaml:"port"` +} + +type Config struct { + Metrics MetricsConfig `yaml:"metrics"` + Port string `yaml:"port"` + Gateways []GatewayConfig `yaml:"gateways"` +} + +type GatewayConfig struct { + ConfigFile string `yaml:"configFile"` + Name string `yaml:"name"` +} + func main() { c, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() @@ -22,27 +47,76 @@ func main() { Usage: "The failover proxy for node providers.", Flags: []cli.Flag{ &cli.StringFlag{ - Name: "config", - Usage: "The configuration file path.", - Required: true, + Name: "config", + Usage: "The YAML configuration file path with gateway configurations.", + Value: "config.yml", // Default configuration file name }, }, Action: func(cc *cli.Context) error { - service, err := rpcgateway.NewRPCGatewayFromConfigFile(cc.String("config")) + configPath := cc.String("config") + config, err := util.LoadYamlFile[Config](configPath) + if err != nil { + return errors.Wrap(err, "failed to load config") + } + + logLevel := slog.LevelWarn + if os.Getenv("DEBUG") == "true" { + logLevel = slog.LevelDebug + } + + logger := httplog.NewLogger("rpc-gateway", httplog.Options{ + JSON: true, + RequestHeaders: true, + LogLevel: logLevel, + }) + + metricsServer := metrics.NewServer(metrics.Config{Port: uint(config.Metrics.Port)}) + go func() { + err = metricsServer.Start() + defer func(metricsServer *metrics.Server) { + err := metricsServer.Stop() + if err != nil { + fmt.Fprintf(os.Stderr, "error stopping metrics server: %v\n", err) + } + }(metricsServer) + if err != nil { + fmt.Fprintf(os.Stderr, "error starting metrics server: %v\n", err) + } + }() + + r := chi.NewRouter() + r.Use(httplog.RequestLogger(logger)) + r.Use(middleware.Recoverer) + server := &http.Server{ + Addr: fmt.Sprintf(":%s", config.Port), + Handler: r, + WriteTimeout: time.Second * 15, + ReadTimeout: time.Second * 15, + ReadHeaderTimeout: time.Second * 5, + } + defer server.Close() + + var wg sync.WaitGroup + for _, gatewayConfig := range config.Gateways { + wg.Add(1) + go func(gwConfig GatewayConfig) { + defer wg.Done() + err := startGateway(c, gwConfig, r) + if err != nil { + fmt.Fprintf(os.Stderr, "error starting gateway '%s': %v\n", gwConfig.Name, err) + } + }(gatewayConfig) + } + + fmt.Println("Starting RPC Gateway server on port: " + config.Port) + err = server.ListenAndServe() if err != nil { - return errors.Wrap(err, "rpc-gateway failed") + return err } - return flowmatic.Do( - func() error { - return errors.Wrap(service.Start(c), "cannot start a service") - }, - func() error { - <-c.Done() + wg.Wait() - return errors.Wrap(service.Stop(c), "cannot stop a service") - }, - ) + return nil }, } @@ -50,3 +124,19 @@ func main() { fmt.Fprintf(os.Stderr, "error: %v", err) } } + +func startGateway(ctx context.Context, config GatewayConfig, router *chi.Mux) error { + service, err := rpcgateway.NewRPCGatewayFromConfigFile(config.ConfigFile, router) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("%s rpc-gateway failed", config.Name)) + } + + err = service.Start(ctx) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("cannot start %s rpc-gateway", config.Name)) + } + + <-ctx.Done() + + return errors.Wrap(service.Stop(ctx), fmt.Sprintf("cannot stop %s rpc-gateway", config.Name)) +}