diff --git a/exp/lighthorizon/actions/root.go b/exp/lighthorizon/actions/root.go new file mode 100644 index 0000000000..3dfa4341a0 --- /dev/null +++ b/exp/lighthorizon/actions/root.go @@ -0,0 +1,29 @@ +package actions + +import ( + "encoding/json" + "net/http" + + "github.com/stellar/go/support/log" + supportProblem "github.com/stellar/go/support/render/problem" +) + +type RootResponse struct { + Version string `json:"version"` + LedgerSource string `json:"ledger_source"` + IndexSource string `json:"index_source"` + LatestLedger uint32 `json:"latest_indexed_ledger"` +} + +func Root(config RootResponse) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/hal+json; charset=utf-8") + encoder := json.NewEncoder(w) + encoder.SetIndent("", " ") + err := encoder.Encode(config) + if err != nil { + log.Error(err) + sendErrorResponse(r.Context(), w, supportProblem.ServerError) + } + } +} diff --git a/exp/lighthorizon/actions/static/api_docs.yml b/exp/lighthorizon/actions/static/api_docs.yml index 707a67eacb..281cf2b605 100644 --- a/exp/lighthorizon/actions/static/api_docs.yml +++ b/exp/lighthorizon/actions/static/api_docs.yml @@ -1,9 +1,10 @@ -openapi: 3.0.3 +openapi: 3.1.0 info: - title: Horizon Light API + title: Horizon Lite API version: 0.0.1 description: |- - The Light API is a published web service on port parameter specified when running Horizon Light `--port=`. + The Horizon Lite API is a published web service on port 8080. It's considered + extremely experimental and only provides a minimal subset of endpoints. servers: - url: http://localhost:8080/ paths: diff --git a/exp/lighthorizon/build/k8s/lighthorizon_web.yml b/exp/lighthorizon/build/k8s/lighthorizon_web.yml index 3f75c2375c..b4f4f501bf 100644 --- a/exp/lighthorizon/build/k8s/lighthorizon_web.yml +++ b/exp/lighthorizon/build/k8s/lighthorizon_web.yml @@ -128,5 +128,3 @@ spec: - hosts: - lighthorizon-pubnet.prototypes.kube001.services.stellar-ops.com secretName: lighthorizon-pubnet-web-cert - - diff --git a/exp/lighthorizon/build/web/Dockerfile b/exp/lighthorizon/build/web/Dockerfile index 780a6b8a45..abdffc2964 100644 --- a/exp/lighthorizon/build/web/Dockerfile +++ b/exp/lighthorizon/build/web/Dockerfile @@ -14,9 +14,7 @@ RUN apt-get clean COPY --from=builder /go/bin/lighthorizon ./ -ENTRYPOINT ./lighthorizon \ - -source "$TXMETA_SOURCE" \ - -indexes "$INDEXES_SOURCE" \ - -network-passphrase "$NETWORK_PASSPHRASE" \ - -ledger-cache "$CACHE_PATH" - +ENTRYPOINT ./lighthorizon serve \ + --network-passphrase "$NETWORK_PASSPHRASE" \ + --ledger-cache "$CACHE_PATH" \ + "$TXMETA_SOURCE" "$INDEXES_SOURCE" \ diff --git a/exp/lighthorizon/http.go b/exp/lighthorizon/http.go index b08e97ce34..e61ad4c716 100644 --- a/exp/lighthorizon/http.go +++ b/exp/lighthorizon/http.go @@ -62,7 +62,11 @@ func lightHorizonHTTPHandler(registry *prometheus.Registry, lightHorizon service r.MethodFunc(http.MethodGet, "/operations", actions.NewOpsByAccountHandler(lightHorizon)) }) - router.MethodFunc(http.MethodGet, "/", actions.ApiDocs()) + router.MethodFunc(http.MethodGet, "/", actions.Root(actions.RootResponse{ + Version: HorizonLiteVersion, + // by default, no other fields are known yet + })) + router.MethodFunc(http.MethodGet, "/api", actions.ApiDocs()) router.Method(http.MethodGet, "/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{})) problem.RegisterHost("") diff --git a/exp/lighthorizon/http_test.go b/exp/lighthorizon/http_test.go index 1939e8c757..f59e2719d5 100644 --- a/exp/lighthorizon/http_test.go +++ b/exp/lighthorizon/http_test.go @@ -2,7 +2,7 @@ package main import ( "encoding/json" - "io/ioutil" + "io" "net/http" "net/http/httptest" "testing" @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/stellar/go/exp/lighthorizon/actions" "github.com/stellar/go/exp/lighthorizon/services" "github.com/stellar/go/support/render/problem" ) @@ -20,22 +21,12 @@ func TestUnknownUrl(t *testing.T) { request, err := http.NewRequest("GET", "/unknown", nil) require.NoError(t, err) - mockOperationService := &services.MockOperationService{} - mockTransactionService := &services.MockTransactionService{} - registry := prometheus.NewRegistry() - - lh := services.LightHorizon{ - Operations: mockOperationService, - Transactions: mockTransactionService, - } - - handler := lightHorizonHTTPHandler(registry, lh) - handler.ServeHTTP(recorder, request) + prepareTestHttpHandler().ServeHTTP(recorder, request) resp := recorder.Result() assert.Equal(t, http.StatusNotFound, resp.StatusCode) - raw, err := ioutil.ReadAll(resp.Body) + raw, err := io.ReadAll(resp.Body) assert.NoError(t, err) var problem problem.P @@ -44,3 +35,30 @@ func TestUnknownUrl(t *testing.T) { assert.Equal(t, "Resource Missing", problem.Title) assert.Equal(t, "not_found", problem.Type) } + +func TestRootResponse(t *testing.T) { + recorder := httptest.NewRecorder() + request, err := http.NewRequest("GET", "/", nil) + require.NoError(t, err) + + prepareTestHttpHandler().ServeHTTP(recorder, request) + + var root actions.RootResponse + raw, err := io.ReadAll(recorder.Result().Body) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(raw, &root)) + require.Equal(t, HorizonLiteVersion, root.Version) +} + +func prepareTestHttpHandler() http.Handler { + mockOperationService := &services.MockOperationService{} + mockTransactionService := &services.MockTransactionService{} + registry := prometheus.NewRegistry() + + lh := services.LightHorizon{ + Operations: mockOperationService, + Transactions: mockTransactionService, + } + + return lightHorizonHTTPHandler(registry, lh) +} diff --git a/exp/lighthorizon/main.go b/exp/lighthorizon/main.go index 379e314527..d6c982e1f4 100644 --- a/exp/lighthorizon/main.go +++ b/exp/lighthorizon/main.go @@ -1,81 +1,137 @@ package main import ( - "flag" "net/http" + "github.com/go-chi/chi" "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/stellar/go/exp/lighthorizon/actions" "github.com/stellar/go/exp/lighthorizon/archive" "github.com/stellar/go/exp/lighthorizon/index" "github.com/stellar/go/exp/lighthorizon/services" + "github.com/stellar/go/exp/lighthorizon/tools" "github.com/stellar/go/network" "github.com/stellar/go/support/log" ) const ( - defaultCacheSize = (60 * 60 * 24) / 6 // 1 day of ledgers @ 6s each + HorizonLiteVersion = "0.0.1-alpha" + defaultCacheSize = (60 * 60 * 24) / 6 // 1 day of ledgers @ 6s each ) func main() { - sourceUrl := flag.String("source", "gcs://horizon-archive-poc", "history archive url to read txmeta files") - indexesUrl := flag.String("indexes", "file://indexes", "url of the indexes") - networkPassphrase := flag.String("network-passphrase", network.PublicNetworkPassphrase, "network passphrase") - cacheDir := flag.String("ledger-cache", "", `path to cache frequently-used ledgers; -if left empty, uses a temporary directory`) - cacheSize := flag.Int("ledger-cache-size", defaultCacheSize, - "number of ledgers to store in the cache") - logLevelParam := flag.String("log-level", "info", - "logging level, info, debug, warn, error, panic, fatal, trace, default is info") - flag.Parse() - - L := log.WithField("service", "horizon-lite") - logLevel, err := logrus.ParseLevel(*logLevelParam) - if err != nil { - log.Warnf("Failed to parse -log-level '%s', defaulting to 'info'.", *logLevelParam) - logLevel = log.InfoLevel - } - L.SetLevel(logLevel) - L.Info("Starting lighthorizon!") - - registry := prometheus.NewRegistry() - indexStore, err := index.ConnectWithConfig(index.StoreConfig{ - URL: *indexesUrl, - Log: L.WithField("subservice", "index"), - Metrics: registry, - }) - if err != nil { - panic(err) - } + log.SetLevel(logrus.InfoLevel) // default for subcommands - ingestArchive, err := archive.NewIngestArchive(archive.ArchiveConfig{ - SourceUrl: *sourceUrl, - NetworkPassphrase: *networkPassphrase, - CacheDir: *cacheDir, - CacheSize: *cacheSize, - }) - if err != nil { - panic(err) + cmd := &cobra.Command{ + Use: "lighthorizon ", + Long: "Horizon Lite command suite", + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Usage() // require a subcommand + }, } - defer ingestArchive.Close() - Config := services.Config{ - Archive: ingestArchive, - Passphrase: *networkPassphrase, - IndexStore: indexStore, - Metrics: services.NewMetrics(registry), - } + serve := &cobra.Command{ + Use: "serve ", + Long: `Starts the Horizon Lite server, binding it to port 8080 on all +local interfaces of the host. You can refer to the OpenAPI documentation located +at the /api endpoint to see what endpoints are supported. - lightHorizon := services.LightHorizon{ - Transactions: &services.TransactionRepository{ - Config: Config, - }, - Operations: &services.OperationRepository{ - Config: Config, +The should be a URL to meta archives from which to read unpacked +ledger files, while the should be a URL containing indices that +break down accounts by active ledgers.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 2 { + cmd.Usage() + return + } + + sourceUrl, indexStoreUrl := args[0], args[1] + + networkPassphrase, _ := cmd.Flags().GetString("network-passphrase") + switch networkPassphrase { + case "testnet": + networkPassphrase = network.TestNetworkPassphrase + case "pubnet": + networkPassphrase = network.PublicNetworkPassphrase + } + + cacheDir, _ := cmd.Flags().GetString("ledger-cache") + cacheSize, _ := cmd.Flags().GetUint("ledger-cache-size") + logLevelParam, _ := cmd.Flags().GetString("log-level") + + L := log.WithField("service", "horizon-lite") + logLevel, err := logrus.ParseLevel(logLevelParam) + if err != nil { + log.Warnf("Failed to parse log level '%s', defaulting to 'info'.", logLevelParam) + logLevel = log.InfoLevel + } + L.SetLevel(logLevel) + L.Info("Starting lighthorizon!") + + registry := prometheus.NewRegistry() + indexStore, err := index.ConnectWithConfig(index.StoreConfig{ + URL: indexStoreUrl, + Log: L.WithField("service", "index"), + Metrics: registry, + }) + if err != nil { + log.Fatal(err) + return + } + + ingester, err := archive.NewIngestArchive(archive.ArchiveConfig{ + SourceUrl: sourceUrl, + NetworkPassphrase: networkPassphrase, + CacheDir: cacheDir, + CacheSize: int(cacheSize), + }) + if err != nil { + log.Fatal(err) + return + } + + Config := services.Config{ + Archive: ingester, + Passphrase: networkPassphrase, + IndexStore: indexStore, + Metrics: services.NewMetrics(registry), + } + + lightHorizon := services.LightHorizon{ + Transactions: &services.TransactionRepository{ + Config: Config, + }, + Operations: &services.OperationRepository{ + Config: Config, + }, + } + + // Inject our config into the root response. + router := lightHorizonHTTPHandler(registry, lightHorizon).(*chi.Mux) + router.MethodFunc(http.MethodGet, "/", actions.Root(actions.RootResponse{ + Version: HorizonLiteVersion, + LedgerSource: sourceUrl, + IndexSource: indexStoreUrl, + })) + + log.Fatal(http.ListenAndServe(":8080", router)) }, } - log.Fatal(http.ListenAndServe(":8080", lightHorizonHTTPHandler(registry, lightHorizon))) + serve.Flags().String("log-level", "info", + "logging level: 'info', 'debug', 'warn', 'error', 'panic', 'fatal', or 'trace'") + serve.Flags().String("network-passphrase", "pubnet", "network passphrase") + serve.Flags().String("ledger-cache", "", "path to cache frequently-used ledgers; "+ + "if left empty, uses a temporary directory") + serve.Flags().Uint("ledger-cache-size", defaultCacheSize, + "number of ledgers to store in the cache") + + cmd.AddCommand(serve) + tools.AddCacheCommands(cmd) + tools.AddIndexCommands(cmd) + cmd.Execute() } diff --git a/exp/lighthorizon/tools/cache.go b/exp/lighthorizon/tools/cache.go index 29f24f408c..2913499bac 100644 --- a/exp/lighthorizon/tools/cache.go +++ b/exp/lighthorizon/tools/cache.go @@ -1,4 +1,4 @@ -package main +package tools import ( "context" @@ -21,7 +21,7 @@ const ( defaultCacheCount = (60 * 60 * 24) / 5 // ~24hrs worth of ledgers ) -func addCacheCommands(parent *cobra.Command) *cobra.Command { +func AddCacheCommands(parent *cobra.Command) *cobra.Command { cmd := &cobra.Command{ Use: "cache", Long: "Manages the on-disk cache of ledgers.", diff --git a/exp/lighthorizon/tools/explorer.go b/exp/lighthorizon/tools/explorer.go index 1978236a20..d05175cb1b 100644 --- a/exp/lighthorizon/tools/explorer.go +++ b/exp/lighthorizon/tools/explorer.go @@ -1,4 +1,4 @@ -package main +package tools import ( "context" @@ -24,7 +24,7 @@ var ( checkpointMgr = historyarchive.NewCheckpointManager(0) ) -func addIndexCommands(parent *cobra.Command) *cobra.Command { +func AddIndexCommands(parent *cobra.Command) *cobra.Command { cmd := &cobra.Command{ Use: "index", Long: "Lets you view details about an index source.", diff --git a/exp/lighthorizon/tools/main.go b/exp/lighthorizon/tools/main.go deleted file mode 100644 index d57021becb..0000000000 --- a/exp/lighthorizon/tools/main.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import ( - "github.com/spf13/cobra" - - "github.com/stellar/go/support/log" -) - -func main() { - log.SetLevel(log.InfoLevel) - - cmd := &cobra.Command{ - Use: "tools", - Long: "Please specify a subcommand to use this toolset.", - Example: "", - RunE: func(cmd *cobra.Command, args []string) error { - // require a subcommand - this is just a "category" - return cmd.Help() - }, - } - - cmd = addCacheCommands(cmd) - cmd = addIndexCommands(cmd) - cmd.Execute() -}