diff --git a/assets/valid.toml b/assets/valid.toml deleted file mode 100644 index 76a8a63..0000000 --- a/assets/valid.toml +++ /dev/null @@ -1,10 +0,0 @@ -[[chains]] -name = "sentinel" -lcd-endpoint = "https://api.sentinel.quokkastake.io" -coingecko-currency = "sentinel" -base-denom = "udvpn" -denom = "dvpn" -bech-wallet-prefix = "sent" -validators = [ - { address = "sentvaloper1rw9wtyhsus7jvx55v3qv5nzun054ma6kz4237k" } -] diff --git a/pkg/app.go b/pkg/app.go index d3056e4..8dd526d 100644 --- a/pkg/app.go +++ b/pkg/app.go @@ -1,6 +1,7 @@ package pkg import ( + "context" fetchersPkg "main/pkg/fetchers" "main/pkg/fs" generatorsPkg "main/pkg/generators" @@ -30,6 +31,7 @@ type App struct { Tracer trace.Tracer Config *config.Config Logger *zerolog.Logger + Server *http.Server RPCs map[string]*tendermint.RPCWithConsumers @@ -48,11 +50,11 @@ type App struct { func NewApp(configPath string, filesystem fs.FS, version string) *App { appConfig, err := config.GetConfig(configPath, filesystem) if err != nil { - loggerPkg.GetDefaultLogger().Fatal().Err(err).Msg("Could not load config") + loggerPkg.GetDefaultLogger().Panic().Err(err).Msg("Could not load config") } if err = appConfig.Validate(); err != nil { - loggerPkg.GetDefaultLogger().Fatal().Err(err).Msg("Provided config is invalid!") + loggerPkg.GetDefaultLogger().Panic().Err(err).Msg("Provided config is invalid!") } logger := loggerPkg.GetLogger(appConfig.LogConfig) @@ -66,11 +68,7 @@ func NewApp(configPath string, filesystem fs.FS, version string) *App { entry.Msg(warning.Message) } - tracer, err := tracing.InitTracer(appConfig.TracingConfig, version) - if err != nil { - loggerPkg.GetDefaultLogger().Fatal().Err(err).Msg("Error setting up tracing") - } - + tracer := tracing.InitTracer(appConfig.TracingConfig, version) coingecko := coingeckoPkg.NewCoingecko(appConfig, logger, tracer) rpcs := make(map[string]*tendermint.RPCWithConsumers, len(appConfig.Chains)) @@ -124,6 +122,8 @@ func NewApp(configPath string, filesystem fs.FS, version string) *App { generatorsPkg.NewValidatorCommissionRateGenerator(appConfig.Chains, logger), } + server := &http.Server{Addr: appConfig.ListenAddress, Handler: nil} + return &App{ Logger: logger, Config: appConfig, @@ -131,20 +131,30 @@ func NewApp(configPath string, filesystem fs.FS, version string) *App { RPCs: rpcs, Fetchers: fetchers, Generators: generators, + Server: server, } } func (a *App) Start() { otelHandler := otelhttp.NewHandler(http.HandlerFunc(a.Handler), "prometheus") - http.Handle("/metrics", otelHandler) + handler := http.NewServeMux() + handler.Handle("/metrics", otelHandler) + handler.HandleFunc("/healthcheck", a.Healthcheck) + a.Server.Handler = handler a.Logger.Info().Str("addr", a.Config.ListenAddress).Msg("Listening") - err := http.ListenAndServe(a.Config.ListenAddress, nil) + + err := a.Server.ListenAndServe() if err != nil { - a.Logger.Fatal().Err(err).Msg("Could not start application") + a.Logger.Panic().Err(err).Msg("Could not start application") } } +func (a *App) Stop() { + a.Logger.Info().Str("addr", a.Config.ListenAddress).Msg("Shutting down server...") + _ = a.Server.Shutdown(context.Background()) +} + func (a *App) Handler(w http.ResponseWriter, r *http.Request) { requestID := uuid.New().String() @@ -209,3 +219,7 @@ func (a *App) Handler(w http.ResponseWriter, r *http.Request) { Float64("request-time", time.Since(requestStart).Seconds()). Msg("Request processed") } + +func (a *App) Healthcheck(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("ok")) +} diff --git a/pkg/app_test.go b/pkg/app_test.go new file mode 100644 index 0000000..c2f6419 --- /dev/null +++ b/pkg/app_test.go @@ -0,0 +1,232 @@ +package pkg + +import ( + "io" + "main/assets" + "main/pkg/fs" + "net/http" + "testing" + "time" + + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +//nolint:paralleltest // disabled +func TestAppLoadConfigError(t *testing.T) { + defer func() { + if r := recover(); r == nil { + require.Fail(t, "Expected to have a panic here!") + } + }() + + filesystem := &fs.TestFS{} + + app := NewApp("not-found-config.toml", filesystem, "1.2.3") + app.Start() +} + +//nolint:paralleltest // disabled +func TestAppLoadConfigInvalid(t *testing.T) { + defer func() { + if r := recover(); r == nil { + require.Fail(t, "Expected to have a panic here!") + } + }() + + filesystem := &fs.TestFS{} + + app := NewApp("config-invalid.toml", filesystem, "1.2.3") + app.Start() +} + +//nolint:paralleltest // disabled +func TestAppFailToStart(t *testing.T) { + defer func() { + if r := recover(); r == nil { + require.Fail(t, "Expected to have a panic here!") + } + }() + + filesystem := &fs.TestFS{} + + app := NewApp("config-invalid-listen-address.toml", filesystem, "1.2.3") + app.Start() +} + +//nolint:paralleltest // disabled +func TestAppFailToStop(t *testing.T) { + filesystem := &fs.TestFS{} + + app := NewApp("config-valid.toml", filesystem, "1.2.3") + app.Stop() + assert.True(t, true) +} + +//nolint:paralleltest // disabled +func TestAppLoadConfigOk(t *testing.T) { + filesystem := &fs.TestFS{} + + app := NewApp("config-valid.toml", filesystem, "1.2.3") + go app.Start() + + for { + request, err := http.Get("http://localhost:9560/healthcheck") + _ = request.Body.Close() + if err == nil { + break + } + + time.Sleep(time.Millisecond * 100) + } + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder( + "GET", + "https://api.cosmos.quokkastake.io/cosmos/staking/v1beta1/validators?pagination.count_total=true&pagination.limit=1000", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("validators.json")), + ) + + httpmock.RegisterResponder( + "GET", + "https://api.cosmos.quokkastake.io/cosmos/slashing/v1beta1/signing_infos/cosmosvalcons1rt4g447zhv6jcqwdl447y88guwm0eevnrelgzc", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("signing-info.json")), + ) + + httpmock.RegisterResponder( + "GET", + "https://api.neutron.quokkastake.io/cosmos/slashing/v1beta1/signing_infos/neutronvalcons1w426hkttrwrve9mj77ld67lzgx5u9m8plhmwc6", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("signing-info.json")), + ) + + httpmock.RegisterResponder( + "GET", + "https://api.cosmos.quokkastake.io/cosmos/staking/v1beta1/params", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("staking-params.json")), + ) + + httpmock.RegisterResponder( + "GET", + "https://api.cosmos.quokkastake.io/cosmos/slashing/v1beta1/params", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("slashing-params.json")), + ) + + httpmock.RegisterResponder( + "GET", + "https://api.neutron.quokkastake.io/cosmos/slashing/v1beta1/params", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("slashing-params.json")), + ) + + httpmock.RegisterResponder( + "GET", + "https://api.cosmos.quokkastake.io/cosmos/staking/v1beta1/validators/cosmosvaloper1xqz9pemz5e5zycaa89kys5aw6m8rhgsvw4328e/unbonding_delegations?pagination.count_total=true&pagination.limit=1", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("unbonds.json")), + ) + + httpmock.RegisterResponder( + "GET", + "https://api.cosmos.quokkastake.io/cosmos/staking/v1beta1/validators/cosmosvaloper1xqz9pemz5e5zycaa89kys5aw6m8rhgsvw4328e/delegations/cosmos1xqz9pemz5e5zycaa89kys5aw6m8rhgsvtp9lt2", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("self-delegation.json")), + ) + + httpmock.RegisterResponder( + "GET", + "https://api.cosmos.quokkastake.io/cosmos/distribution/v1beta1/validators/cosmosvaloper1xqz9pemz5e5zycaa89kys5aw6m8rhgsvw4328e/commission", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("commission.json")), + ) + + httpmock.RegisterResponder( + "GET", + "https://api.cosmos.quokkastake.io/interchain_security/ccv/provider/consumer_validators/neutron-1", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("consumer-validators.json")), + ) + + httpmock.RegisterResponder( + "GET", + "https://api.cosmos.quokkastake.io/cosmos/distribution/v1beta1/delegators/cosmos1xqz9pemz5e5zycaa89kys5aw6m8rhgsvtp9lt2/rewards/cosmosvaloper1xqz9pemz5e5zycaa89kys5aw6m8rhgsvw4328e", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("rewards.json")), + ) + + httpmock.RegisterResponder( + "GET", + "https://api.cosmos.quokkastake.io/cosmos/staking/v1beta1/validators/cosmosvaloper1xqz9pemz5e5zycaa89kys5aw6m8rhgsvw4328e/delegations?pagination.count_total=true&pagination.limit=1", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("delegations.json")), + ) + + httpmock.RegisterResponder( + "GET", + "https://api.cosmos.quokkastake.io/cosmos/bank/v1beta1/balances/cosmos1xqz9pemz5e5zycaa89kys5aw6m8rhgsvtp9lt2", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("balances.json")), + ) + + httpmock.RegisterResponder( + "GET", + "https://api.cosmos.quokkastake.io/cosmos/base/tendermint/v1beta1/node_info", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("node-info.json")), + ) + + httpmock.RegisterResponder( + "GET", + "https://api.neutron.quokkastake.io/cosmos/base/tendermint/v1beta1/node_info", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("node-info.json")), + ) + + httpmock.RegisterResponder( + "GET", + "https://api.neutron.quokkastake.io/cosmos/params/v1beta1/params?subspace=ccvconsumer&key=SoftOptOutThreshold", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("soft-opt-out-threshold.json")), + ) + + httpmock.RegisterResponder( + "GET", + "https://api.neutron.quokkastake.io/cosmos/bank/v1beta1/balances/neutron1xqz9pemz5e5zycaa89kys5aw6m8rhgsv07va3d", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("balances.json")), + ) + + httpmock.RegisterResponder( + "GET", + "https://api.cosmos.quokkastake.io/interchain_security/ccv/provider/validator_consumer_addr?chain_id=neutron-1&provider_address=cosmosvalcons1rt4g447zhv6jcqwdl447y88guwm0eevnrelgzc", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("assigned-key.json")), + ) + + httpmock.RegisterResponder( + "GET", + "https://api.cosmos.quokkastake.io/interchain_security/ccv/provider/consumer_chains_per_validator/cosmosvalcons1rt4g447zhv6jcqwdl447y88guwm0eevnrelgzc", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("validator-consumers.json")), + ) + + httpmock.RegisterResponder( + "GET", + "https://api.cosmos.quokkastake.io/interchain_security/ccv/provider/consumer_commission_rate/neutron-1/cosmosvalcons1rt4g447zhv6jcqwdl447y88guwm0eevnrelgzc", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("consumer-commission.json")), + ) + + httpmock.RegisterResponder( + "GET", + "https://api.cosmos.quokkastake.io/interchain_security/ccv/provider/consumer_chains", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("consumer-info.json")), + ) + + httpmock.RegisterResponder( + "GET", + "https://api.coingecko.com/api/v3/simple/price?ids=cosmos,neutron,stride,osmosis,terra-luna-2,stargaze,juno-network,sommelier,injective-protocol,cosmos,evmos,umee,comdex,neutron&vs_currencies=usd", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("coingecko.json")), + ) + + httpmock.RegisterResponder("GET", "http://localhost:9560/healthcheck", httpmock.InitialTransport.RoundTrip) + httpmock.RegisterResponder("GET", "http://localhost:9560/metrics", httpmock.InitialTransport.RoundTrip) + + response, err := http.Get("http://localhost:9560/metrics") + require.NoError(t, err) + require.NotEmpty(t, response) + + body, err := io.ReadAll(response.Body) + require.NoError(t, err) + require.NotEmpty(t, body) + + err = response.Body.Close() + require.NoError(t, err) +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 1223726..c06a8a2 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -132,7 +132,7 @@ func TestLoadConfigValid(t *testing.T) { t.Parallel() filesystem := &fs.TestFS{} - config, err := GetConfig("valid.toml", filesystem) + config, err := GetConfig("config-valid.toml", filesystem) require.NoError(t, err) require.NotNil(t, config) } diff --git a/pkg/querier_metrics_test.go b/pkg/queries_metrics_test.go similarity index 100% rename from pkg/querier_metrics_test.go rename to pkg/queries_metrics_test.go diff --git a/pkg/tracing/tracer.go b/pkg/tracing/tracer.go index fc2a20a..c1bd508 100644 --- a/pkg/tracing/tracer.go +++ b/pkg/tracing/tracer.go @@ -3,7 +3,6 @@ package tracing import ( "context" "encoding/base64" - "fmt" configPkg "main/pkg/config" "go.opentelemetry.io/otel" @@ -12,7 +11,7 @@ import ( "go.opentelemetry.io/otel/trace" ) -func getExporter(config configPkg.TracingConfig) (tracesdk.SpanExporter, error) { +func getExporter(config configPkg.TracingConfig) tracesdk.SpanExporter { if config.Enabled.Bool { opts := []otlptracehttp.Option{ otlptracehttp.WithEndpoint(config.OpenTelemetryHTTPHost), @@ -30,25 +29,22 @@ func getExporter(config configPkg.TracingConfig) (tracesdk.SpanExporter, error) })) } - return otlptracehttp.New( + exporter, _ := otlptracehttp.New( context.Background(), opts..., ) + return exporter } - return NewNoopExporter(), nil + return NewNoopExporter() } -func InitTracer(config configPkg.TracingConfig, version string) (trace.Tracer, error) { - exporter, err := getExporter(config) - if err != nil { - return nil, fmt.Errorf("error creating exporter: %w", err) - } - +func InitTracer(config configPkg.TracingConfig, version string) trace.Tracer { + exporter := getExporter(config) tp := NewTraceProvider(exporter, version) otel.SetTracerProvider(tp) - return tp.Tracer("main"), nil + return tp.Tracer("main") } func InitNoopTracer() trace.Tracer { diff --git a/pkg/tracing/tracer_test.go b/pkg/tracing/tracer_test.go index b201cbe..3acc642 100644 --- a/pkg/tracing/tracer_test.go +++ b/pkg/tracing/tracer_test.go @@ -12,9 +12,7 @@ func TestTracerGetExporterNoop(t *testing.T) { t.Parallel() config := configPkg.TracingConfig{} - exporter, err := getExporter(config) - - require.NoError(t, err) + exporter := getExporter(config) require.NotNil(t, exporter) } @@ -22,9 +20,7 @@ func TestTracerGetExporterHttpBasic(t *testing.T) { t.Parallel() config := configPkg.TracingConfig{Enabled: null.BoolFrom(true)} - exporter, err := getExporter(config) - - require.NoError(t, err) + exporter := getExporter(config) require.NotNil(t, exporter) } @@ -38,9 +34,7 @@ func TestTracerGetExporterHttpComplex(t *testing.T) { OpenTelemetryHTTPPassword: "test", OpenTelemetryHTTPInsecure: null.BoolFrom(true), } - exporter, err := getExporter(config) - - require.NoError(t, err) + exporter := getExporter(config) require.NotNil(t, exporter) } @@ -54,9 +48,7 @@ func TestTracerGetTracerOk(t *testing.T) { OpenTelemetryHTTPPassword: "test", OpenTelemetryHTTPInsecure: null.BoolFrom(true), } - tracer, err := InitTracer(config, "v1.2.3") - - require.NoError(t, err) + tracer := InitTracer(config, "v1.2.3") require.NotNil(t, tracer) }