From e74c06e87a823426015d642b0c6ab1758a95a953 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Tue, 23 Dec 2025 18:34:08 +0100 Subject: [PATCH] extra logging --- .../commands/wallet/onchain_balance.go | 13 -- .../service/gateway/rpcclient/clients.go | 170 ++++++++++++++++++ api/pkg/api/routers/gsresponse/response.go | 7 +- 3 files changed, 171 insertions(+), 19 deletions(-) create mode 100644 api/gateway/chain/internal/service/gateway/rpcclient/clients.go diff --git a/api/gateway/chain/internal/service/gateway/commands/wallet/onchain_balance.go b/api/gateway/chain/internal/service/gateway/commands/wallet/onchain_balance.go index cb0dd48..87a538c 100644 --- a/api/gateway/chain/internal/service/gateway/commands/wallet/onchain_balance.go +++ b/api/gateway/chain/internal/service/gateway/commands/wallet/onchain_balance.go @@ -3,7 +3,6 @@ package wallet import ( "context" "math/big" - "net/url" "strings" "time" @@ -30,7 +29,6 @@ func onChainWalletBalance(ctx context.Context, deps Deps, wallet *model.ManagedW zap.String("token_symbol", strings.ToUpper(strings.TrimSpace(wallet.TokenSymbol))), zap.String("contract", strings.ToLower(strings.TrimSpace(wallet.ContractAddress))), zap.String("wallet_address", strings.ToLower(strings.TrimSpace(wallet.DepositAddress))), - zap.String("rpc_endpoint", safeRPCLabel(rpcURL)), } if rpcURL == "" { @@ -154,14 +152,3 @@ const erc20ABIJSON = ` "type": "function" } ]` - -func safeRPCLabel(raw string) string { - parsed, err := url.Parse(strings.TrimSpace(raw)) - if err != nil || parsed.Host == "" { - return "" - } - parsed.User = nil - parsed.RawQuery = "" - parsed.Fragment = "" - return parsed.String() -} diff --git a/api/gateway/chain/internal/service/gateway/rpcclient/clients.go b/api/gateway/chain/internal/service/gateway/rpcclient/clients.go new file mode 100644 index 0000000..a434442 --- /dev/null +++ b/api/gateway/chain/internal/service/gateway/rpcclient/clients.go @@ -0,0 +1,170 @@ +package rpcclient + +import ( + "context" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rpc" + "github.com/tech/sendico/gateway/chain/internal/service/gateway/shared" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "go.uber.org/zap" +) + +// Clients holds pre-initialised RPC clients keyed by network name. +type Clients struct { + logger mlogger.Logger + clients map[string]*ethclient.Client +} + +// Prepare dials all configured networks up front and returns a ready-to-use client set. +func Prepare(ctx context.Context, logger mlogger.Logger, networks []shared.Network) (*Clients, error) { + if logger == nil { + return nil, merrors.Internal("rpc clients: logger is required") + } + clientLogger := logger.Named("rpc_client") + result := &Clients{ + logger: clientLogger, + clients: make(map[string]*ethclient.Client), + } + + for _, network := range networks { + name := strings.ToLower(strings.TrimSpace(network.Name)) + rpcURL := strings.TrimSpace(network.RPCURL) + if name == "" { + clientLogger.Warn("skipping network with empty name during rpc client preparation") + continue + } + if rpcURL == "" { + result.Close() + err := merrors.InvalidArgument(fmt.Sprintf("rpc url not configured for network %s", name)) + clientLogger.Warn("rpc url missing", zap.String("network", name)) + return nil, err + } + + fields := []zap.Field{ + zap.String("network", name), + } + clientLogger.Info("initialising rpc client", fields...) + + dialCtx, cancel := context.WithTimeout(ctx, 15*time.Second) + httpClient := &http.Client{ + Transport: &loggingRoundTripper{ + logger: clientLogger, + network: name, + base: http.DefaultTransport, + }, + } + rpcCli, err := rpc.DialOptions(dialCtx, rpcURL, rpc.WithHTTPClient(httpClient)) + cancel() + if err != nil { + result.Close() + clientLogger.Warn("failed to dial rpc endpoint", append(fields, zap.Error(err))...) + return nil, merrors.Internal(fmt.Sprintf("rpc dial failed for %s: %s", name, err.Error())) + } + client := ethclient.NewClient(rpcCli) + + result.clients[name] = client + clientLogger.Info("rpc client ready", fields...) + } + + if len(result.clients) == 0 { + return nil, merrors.InvalidArgument("no rpc clients initialised") + } + + return result, nil +} + +// Client returns a prepared client for the given network name. +func (c *Clients) Client(network string) (*ethclient.Client, error) { + if c == nil { + return nil, merrors.Internal("rpc clients not initialised") + } + name := strings.ToLower(strings.TrimSpace(network)) + client, ok := c.clients[name] + if !ok { + return nil, merrors.InvalidArgument(fmt.Sprintf("rpc client not configured for network %s", name)) + } + return client, nil +} + +// Close tears down all RPC clients, logging each close. +func (c *Clients) Close() { + if c == nil { + return + } + for name, client := range c.clients { + client.Close() + if c.logger != nil { + c.logger.Info("rpc client closed", zap.String("network", name)) + } + } +} + +type loggingRoundTripper struct { + logger mlogger.Logger + network string + endpoint string + base http.RoundTripper +} + +func (l *loggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + if l.base == nil { + l.base = http.DefaultTransport + } + + var reqBody []byte + if req.Body != nil { + raw, _ := io.ReadAll(req.Body) + reqBody = raw + req.Body = io.NopCloser(strings.NewReader(string(raw))) + } + + fields := []zap.Field{ + zap.String("network", l.network), + zap.String("rpc_endpoint", l.endpoint), + } + if len(reqBody) > 0 { + fields = append(fields, zap.String("rpc_request", truncate(string(reqBody), 2048))) + } + l.logger.Debug("rpc request", fields...) + + resp, err := l.base.RoundTrip(req) + if err != nil { + l.logger.Warn("rpc http request failed", append(fields, zap.Error(err))...) + return nil, err + } + + bodyBytes, _ := io.ReadAll(resp.Body) + resp.Body.Close() + resp.Body = io.NopCloser(strings.NewReader(string(bodyBytes))) + + respFields := append(fields, + zap.Int("status_code", resp.StatusCode), + ) + if len(bodyBytes) > 0 { + respFields = append(respFields, zap.String("rpc_response", truncate(string(bodyBytes), 2048))) + } + if resp.StatusCode >= 400 { + l.logger.Warn("rpc response error", respFields...) + } else { + l.logger.Debug("rpc response", respFields...) + } + + return resp, nil +} + +func truncate(s string, max int) string { + if len(s) <= max { + return s + } + if max <= 3 { + return s[:max] + } + return s[:max-3] + "..." +} diff --git a/api/pkg/api/routers/gsresponse/response.go b/api/pkg/api/routers/gsresponse/response.go index 5cb377a..917ff3a 100644 --- a/api/pkg/api/routers/gsresponse/response.go +++ b/api/pkg/api/routers/gsresponse/response.go @@ -47,12 +47,7 @@ func Error[T any](logger mlogger.Logger, service mservice.Type, code codes.Code, if err != nil { fields = append(fields, zap.Error(err)) } - logFn := logger.Warn - switch code { - case codes.Internal, codes.DataLoss, codes.Unavailable: - logFn = logger.Error - } - logFn("gRPC request failed", fields...) + logger.Warn("gRPC request failed", fields...) msg := message(err) switch {