Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions metrics/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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),
))
}

Expand Down
14 changes: 11 additions & 3 deletions metrics/sanitize_rpc_url.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
Expand All @@ -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)
}
109 changes: 60 additions & 49 deletions metrics/sanitize_rpc_url_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
})
}
}
}
Loading