diff --git a/metrics/client.go b/metrics/client.go index 44c3775..313913c 100644 --- a/metrics/client.go +++ b/metrics/client.go @@ -40,7 +40,7 @@ type RPCClientMetrics interface { // RecordRequest records latency for an RPC call (observed in nanoseconds for Prometheus and Beholder). // Failures use success="false"; derive error rate from rpc_call_latency_count{success="false"} (or equivalent). // rpcURL is sanitized before export (userinfo and query removed; path hashed). - RecordRequest(ctx context.Context, rpcURL string, isSendOnly bool, callName string, latency time.Duration, err error) + RecordRequest(ctx context.Context, rpcDomain string, isSendOnly bool, callName string, latency time.Duration, err error) } var _ RPCClientMetrics = (*rpcClientMetrics)(nil) @@ -74,11 +74,11 @@ func NewRPCClientMetrics(cfg RPCClientMetricsConfig) (RPCClientMetrics, error) { }, nil } -func (m *rpcClientMetrics) RecordRequest(ctx context.Context, rpcURL string, isSendOnly bool, callName string, latency time.Duration, err error) { +func (m *rpcClientMetrics) RecordRequest(ctx context.Context, rpcDomain string, isSendOnly bool, callName string, latency time.Duration, err error) { successStr := strconv.FormatBool(err != nil) sendStr := strconv.FormatBool(isSendOnly) latencyNs := float64(latency) - safeRPCURL := SanitizeRPCURL(rpcURL) + safeRPCURL := SanitizeRPCURL(rpcDomain) RPCCallLatency.WithLabelValues( m.chainFamily, m.chainID, safeRPCURL, sendStr, successStr, callName, @@ -87,10 +87,10 @@ func (m *rpcClientMetrics) RecordRequest(ctx context.Context, rpcURL string, isS m.latencyHis.Record(ctx, latencyNs/float64(time.Millisecond), metric.WithAttributes( attribute.String("chainFamily", m.chainFamily), attribute.String("chainID", m.chainID), - attribute.String("rpcUrl", safeRPCURL), + attribute.String("rpcDomain", safeRPCURL), attribute.String("isSendOnly", sendStr), attribute.String("success", successStr), - attribute.String("rpcCallName", callName), + attribute.String("callName", callName), )) } diff --git a/metrics/sanitize_rpc_url.go b/metrics/sanitize_rpc_url.go index 6083901..02af3c4 100644 --- a/metrics/sanitize_rpc_url.go +++ b/metrics/sanitize_rpc_url.go @@ -9,6 +9,14 @@ import ( // SanitizeRPCURL either strips user:passwd or replaces path and params with their sha1-hex, excluding leading / if present func SanitizeRPCURL(raw string) string { + // url.Parse requires a scheme to correctly populate Host. + // When none is present, prepend a temporary one and strip it afterwards. + // TrimPrefix below is always safe: no real scheme starts with fakeScheme. + const fakeScheme = "x://" + if !strings.Contains(raw, "://") { + raw = fakeScheme + raw + } + u, err := url.Parse(raw) if err != nil { return "invalid_rpc_url" @@ -17,7 +25,7 @@ func SanitizeRPCURL(raw string) string { if u.User != nil { // Strip credentials and leave everything else intact. u.User = nil - return u.String() + return strings.TrimPrefix(u.String(), fakeScheme) } // Build the sensitive portion: path (without leading /) plus optional query. @@ -32,12 +40,12 @@ func SanitizeRPCURL(raw string) string { if sensitive == "" { // Nothing to redact. - return u.String() + return strings.TrimPrefix(u.String(), fakeScheme) } //nolint:gosec h := sha1.Sum([]byte(sensitive)) u.Path = "/" + fmt.Sprintf("%x", h) u.RawQuery = "" - return u.String() + return strings.TrimPrefix(u.String(), fakeScheme) } diff --git a/metrics/sanitize_rpc_url_test.go b/metrics/sanitize_rpc_url_test.go index b37bab5..56f38e4 100644 --- a/metrics/sanitize_rpc_url_test.go +++ b/metrics/sanitize_rpc_url_test.go @@ -6,59 +6,70 @@ import ( "github.com/stretchr/testify/assert" ) -func TestSanitizeRPCURL_RedactsSecrets(t *testing.T) { - cases := []struct { - input string - want string - }{ - // simple path - {"https://bsc-mainnet.core.chainstack.com/MjcwMDk3ZGFhMDA5NjJjMDM1", "https://bsc-mainnet.core.chainstack.com/b0b99e8b33b401b05251f91aec08b6a9581c86dd"}, - // less simple path - {"http://172.16.156.14:8000/MmZmNTJmOWRiNzg0NTgxNDYyNzJjMTYzNDlmNGJ/iYWEwOTVmYWE0OQ/bsc/mainnet/", "http://172.16.156.14:8000/bd161d616d46b90a248de0f0a3ebc2daf2b8bb20"}, - // path with no / is excluded from sha - {"https://anyblocks-01.mainnet.bnb.bdnodes.net?auth=MDcwMTgzODk3NzIyMjU4YzY2MTQzNGMyNTU2OWE2NGEzYjhlODM0NA", "https://anyblocks-01.mainnet.bnb.bdnodes.net/7c87697e63d8f9c049183bb4f8c171af40715b2b"}, - // path with leading / is included in sha - {"https://anyblocks-02.mainnet.bnb.bdnodes.net/somepath/?auth=2Dc8bNAqCC0X74zZfi_4ra6XzuBY8lmXcTE1ic9EO5o", "https://anyblocks-02.mainnet.bnb.bdnodes.net/22c028ea2d53fd106e2bb93bc61d838ed6b01c19"}, - // strip creds keep path - {"https://myLittleNop:YjY5MjAwOGJkMzBjNW@broadcast-mirror.fiews.io/?chain_id=56", "https://broadcast-mirror.fiews.io/?chain_id=56"}, - // even if no creds, sacrifice path for uniformity - {"https://eu-bsc.rpc.linkriver.internal/rpc", "https://eu-bsc.rpc.linkriver.internal/e64b40f2bd5c8a9560773d16476a86ede7e7c1ba"}, - // keeps protocol too - {"wss://bsc-mainnet-proxy.internal.linkpool.io/ws", "wss://bsc-mainnet-proxy.internal.linkpool.io/1457b75dc8c5500c0f1d4503cf801b60deb045a4"}, - } +var toSanitizeCases = []struct { + input string + want string +}{ + // simple path + {"bsc-mainnet.core.chainstack.com/MjcwMDk3ZGFhMDA5NjJjMDM1", "bsc-mainnet.core.chainstack.com/b0b99e8b33b401b05251f91aec08b6a9581c86dd"}, + // less simple path + {"172.16.156.14:8000/MmZmNTJmOWRiNzg0NTgxNDYyNzJjMTYzNDlmNGJ/iYWEwOTVmYWE0OQ/bsc/mainnet/", "172.16.156.14:8000/bd161d616d46b90a248de0f0a3ebc2daf2b8bb20"}, + // path with no / is excluded from sha + {"anyblocks-01.mainnet.bnb.bdnodes.net?auth=MDcwMTgzODk3NzIyMjU4YzY2MTQzNGMyNTU2OWE2NGEzYjhlODM0NA", "anyblocks-01.mainnet.bnb.bdnodes.net/7c87697e63d8f9c049183bb4f8c171af40715b2b"}, + // path with leading / is included in sha + {"anyblocks-02.mainnet.bnb.bdnodes.net/somepath/?auth=2Dc8bNAqCC0X74zZfi_4ra6XzuBY8lmXcTE1ic9EO5o", "anyblocks-02.mainnet.bnb.bdnodes.net/22c028ea2d53fd106e2bb93bc61d838ed6b01c19"}, + // strip creds keep path + {"myLittleNop:YjY5MjAwOGJkMzBjNW@broadcast-mirror.fiews.io/?chain_id=56", "broadcast-mirror.fiews.io/?chain_id=56"}, + // even if no creds, sacrifice path for uniformity + {"eu-bsc.rpc.linkriver.internal/rpc", "eu-bsc.rpc.linkriver.internal/e64b40f2bd5c8a9560773d16476a86ede7e7c1ba"}, + // keeps protocol too + {"bsc-mainnet-proxy.internal.linkpool.io/ws", "bsc-mainnet-proxy.internal.linkpool.io/1457b75dc8c5500c0f1d4503cf801b60deb045a4"}, +} - for _, tc := range cases { - t.Run(tc.input, func(t *testing.T) { - assert.Equal(t, tc.want, SanitizeRPCURL(tc.input)) - }) - } +var alreadySanitizedCases = []string{ + "10.0.1.191:8545", + "144.178.241.22:8545", + "222.106.187.14:12001", + "at2-bsc-main03.blockchain.fiews.net:8545", + "berlioz.stakesystems.io:8745", + "blockchains-1.shultzpro.com:8545", + "dfw3-bsc-main01.blockchain.fiews.net:8545", + "sylvester.stakesystems.io:8745", + "bsc-dataseed.binance.org/", + "chainlink-bsc.rpc.blxrbdn.com", + "puissant-builder.48.club", + "10.0.1.191:8546", + "144.76.108.206:8546", + "172.16.152.140:8546", + "bsc-rpc-2.piertwo.prod:8546", + "bsc.rpc.cinternal.com", + "sylvester.stakesystems.io:8746", + "bsc-rpc.o1.wtf", } -func TestSanitizeRPCURL_AlreadySanitized(t *testing.T) { - urls := []string{ - "http://10.0.1.191:8545", - "http://144.178.241.22:8545", - "http://222.106.187.14:12001", - "http://at2-bsc-main03.blockchain.fiews.net:8545", - "http://berlioz.stakesystems.io:8745", - "http://blockchains-1.shultzpro.com:8545", - "http://dfw3-bsc-main01.blockchain.fiews.net:8545", - "http://sylvester.stakesystems.io:8745", - "https://bsc-dataseed.binance.org/", - "https://chainlink-bsc.rpc.blxrbdn.com", - "https://puissant-builder.48.club", - "ws://10.0.1.191:8546", - "ws://144.76.108.206:8546", - "ws://172.16.152.140:8546", - "ws://bsc-rpc-2.piertwo.prod:8546", - "ws://bsc.rpc.cinternal.com", - "ws://sylvester.stakesystems.io:8746", - "wss://bsc-rpc.o1.wtf", +var protocolParts = []string{ + "wss://", + "https://", + "http://", + "", +} + +func TestSanitizeRPCURL_RedactsSecrets(t *testing.T) { + for _, prefix := range protocolParts { + for _, tc := range toSanitizeCases { + t.Run(tc.input, func(t *testing.T) { + assert.Equal(t, prefix+tc.want, SanitizeRPCURL(prefix+tc.input)) + }) + } } +} - for _, u := range urls { - t.Run(u, func(t *testing.T) { - assert.Equal(t, u, SanitizeRPCURL(u)) - }) +func TestSanitizeRPCURL_AlreadySanitized(t *testing.T) { + for _, prefix := range protocolParts { + for _, u := range alreadySanitizedCases { + t.Run(u, func(t *testing.T) { + assert.Equal(t, prefix+u, SanitizeRPCURL(prefix+u)) + }) + } } }