Skip to content

Commit e22986b

Browse files
authored
feat: implement Besu block log parser and remove CLIQUE API (#161)
## Summary - Implement block log parser for Besu client — parses `SlowBlock` log lines by stripping ANSI escape codes, matching the pipe-delimited prefix, and extracting the embedded JSON payload - Remove `CLIQUE` from Besu's default RPC API list (not needed for benchmarking) ## Test plan - [x] Unit tests pass for all cases: valid lines, ANSI-escaped lines, non-SlowBlock lines, empty/random input, invalid JSON - [x] Verified against real Besu run (`1774018356_6d9d88a8_besu`) — 25/26 tests captured (1 test too fast for SlowBlock threshold)
1 parent 8c42b0a commit e22986b

3 files changed

Lines changed: 169 additions & 7 deletions

File tree

pkg/blocklog/besu.go

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,46 @@ package blocklog
22

33
import (
44
"encoding/json"
5+
"regexp"
56

67
"github.com/ethpandaops/benchmarkoor/pkg/client"
78
)
89

9-
// besuParser is a stub parser for Besu client logs.
10-
// Returns nil, false until the log format is known.
10+
// besuLogPattern matches Besu SlowBlock log lines (after ANSI stripping).
11+
// Format: <timestamp> | <thread> | WARN | SlowBlock | {JSON}
12+
var besuLogPattern = regexp.MustCompile(
13+
`^.+\|\s*(?:WARN|INFO)\s*\|\s*SlowBlock\s*\|\s*(\{.+\})\s*$`,
14+
)
15+
16+
// besuParser parses JSON payloads from Besu client SlowBlock logs.
1117
type besuParser struct{}
1218

13-
// NewBesuParser creates a new Besu log parser (stub).
19+
// NewBesuParser creates a new Besu log parser.
1420
func NewBesuParser() Parser {
1521
return &besuParser{}
1622
}
1723

1824
// Ensure interface compliance.
1925
var _ Parser = (*besuParser)(nil)
2026

21-
// ParseLine is a stub that always returns nil, false.
22-
func (p *besuParser) ParseLine(_ string) (json.RawMessage, bool) {
23-
return nil, false
27+
// ParseLine extracts JSON from a Besu SlowBlock log line.
28+
func (p *besuParser) ParseLine(line string) (json.RawMessage, bool) {
29+
// Strip ANSI escape codes — Besu logs include color/style sequences.
30+
line = ansiPattern.ReplaceAllString(line, "")
31+
32+
matches := besuLogPattern.FindStringSubmatch(line)
33+
if len(matches) < 2 {
34+
return nil, false
35+
}
36+
37+
jsonStr := matches[1]
38+
39+
// Validate that it's valid JSON.
40+
if !json.Valid([]byte(jsonStr)) {
41+
return nil, false
42+
}
43+
44+
return json.RawMessage(jsonStr), true
2445
}
2546

2647
// ClientType returns the client type.

pkg/blocklog/besu_test.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package blocklog
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestBesuParser_ParseLine(t *testing.T) {
12+
parser := NewBesuParser()
13+
14+
tests := []struct {
15+
name string
16+
line string
17+
wantOK bool
18+
checkJSON func(t *testing.T, data map[string]any)
19+
}{
20+
{
21+
name: "valid SlowBlock line with all fields",
22+
line: `2026-03-20 14:48:33.568+0000 | vert.x-worker-thread-0 | WARN | SlowBlock | {"level":"warn","msg":"Slow block","block":{"number":1,"hash":"0x5c6519e89d3b01dc9846d2b67a07202efd45fcd35d380beada32f7be406fd22d","gas_used":100000000,"tx_count":6},"timing":{"execution_ms":970.803696,"state_read_ms":1.365365,"state_hash_ms":1.78546,"commit_ms":0.93028,"total_ms":973.519436},"throughput":{"mgas_per_sec":103.01},"state_reads":{"accounts":9,"storage_slots":11,"code":5,"code_bytes":25672},"state_writes":{"accounts":20,"storage_slots":20,"code":0,"code_bytes":0,"eip7702_delegations_set":0,"eip7702_delegations_cleared":0},"cache":{"account":{"hits":44,"misses":9,"hit_rate":83.02},"storage":{"hits":10,"misses":11,"hit_rate":47.62},"code":{"hits":5,"misses":0,"hit_rate":100.0}},"unique":{"accounts":3,"storage_slots":0,"contracts":2},"evm":{"sload":0,"sstore":0,"calls":372615,"creates":0}}`,
23+
wantOK: true,
24+
checkJSON: func(t *testing.T, data map[string]any) {
25+
t.Helper()
26+
27+
assert.Equal(t, "warn", data["level"])
28+
assert.Equal(t, "Slow block", data["msg"])
29+
30+
block := data["block"].(map[string]any)
31+
assert.Equal(t, float64(1), block["number"])
32+
assert.Equal(t, "0x5c6519e89d3b01dc9846d2b67a07202efd45fcd35d380beada32f7be406fd22d", block["hash"])
33+
assert.Equal(t, float64(100000000), block["gas_used"])
34+
assert.Equal(t, float64(6), block["tx_count"])
35+
36+
timing := data["timing"].(map[string]any)
37+
assert.Equal(t, 970.803696, timing["execution_ms"])
38+
assert.Equal(t, 1.365365, timing["state_read_ms"])
39+
assert.Equal(t, 1.78546, timing["state_hash_ms"])
40+
assert.Equal(t, 0.93028, timing["commit_ms"])
41+
assert.Equal(t, 973.519436, timing["total_ms"])
42+
43+
throughput := data["throughput"].(map[string]any)
44+
assert.Equal(t, 103.01, throughput["mgas_per_sec"])
45+
46+
stateReads := data["state_reads"].(map[string]any)
47+
assert.Equal(t, float64(9), stateReads["accounts"])
48+
assert.Equal(t, float64(11), stateReads["storage_slots"])
49+
assert.Equal(t, float64(5), stateReads["code"])
50+
assert.Equal(t, float64(25672), stateReads["code_bytes"])
51+
52+
stateWrites := data["state_writes"].(map[string]any)
53+
assert.Equal(t, float64(20), stateWrites["accounts"])
54+
assert.Equal(t, float64(20), stateWrites["storage_slots"])
55+
assert.Equal(t, float64(0), stateWrites["code"])
56+
57+
cache := data["cache"].(map[string]any)
58+
account := cache["account"].(map[string]any)
59+
assert.Equal(t, float64(44), account["hits"])
60+
assert.Equal(t, float64(9), account["misses"])
61+
assert.Equal(t, 83.02, account["hit_rate"])
62+
63+
storage := cache["storage"].(map[string]any)
64+
assert.Equal(t, float64(10), storage["hits"])
65+
assert.Equal(t, float64(11), storage["misses"])
66+
assert.Equal(t, 47.62, storage["hit_rate"])
67+
68+
code := cache["code"].(map[string]any)
69+
assert.Equal(t, float64(5), code["hits"])
70+
assert.Equal(t, float64(0), code["misses"])
71+
assert.Equal(t, 100.0, code["hit_rate"])
72+
},
73+
},
74+
{
75+
name: "line with ANSI escape codes",
76+
line: "\x1b[m\x1b[2m2026-03-20 14:48:33.568+0000\x1b[m\x1b[2m | \x1b[m\x1b[2mvert.x-worker-thread-0\x1b[m\x1b[2m | \x1b[m\x1b[33mWARN \x1b[m\x1b[2m | \x1b[m\x1b[2mSlowBlock\x1b[m\x1b[2m | \x1b[m\x1b[33m{\"level\":\"warn\",\"msg\":\"Slow block\",\"block\":{\"number\":1,\"hash\":\"0xabc\",\"gas_used\":100000000,\"tx_count\":6},\"timing\":{\"execution_ms\":970.8,\"state_read_ms\":1.3,\"state_hash_ms\":1.7,\"commit_ms\":0.9,\"total_ms\":973.5},\"throughput\":{\"mgas_per_sec\":103.01}}\x1b[m",
77+
wantOK: true,
78+
checkJSON: func(t *testing.T, data map[string]any) {
79+
t.Helper()
80+
81+
assert.Equal(t, "warn", data["level"])
82+
assert.Equal(t, "Slow block", data["msg"])
83+
84+
block := data["block"].(map[string]any)
85+
assert.Equal(t, float64(1), block["number"])
86+
assert.Equal(t, "0xabc", block["hash"])
87+
88+
timing := data["timing"].(map[string]any)
89+
assert.Equal(t, 970.8, timing["execution_ms"])
90+
91+
throughput := data["throughput"].(map[string]any)
92+
assert.Equal(t, 103.01, throughput["mgas_per_sec"])
93+
},
94+
},
95+
{
96+
name: "non-SlowBlock besu log line",
97+
line: `2026-03-20 14:48:33.568+0000 | vert.x-worker-thread-0 | INFO | BlockManager | Block imported #1`,
98+
wantOK: false,
99+
},
100+
{
101+
name: "empty line",
102+
line: "",
103+
wantOK: false,
104+
},
105+
{
106+
name: "random text",
107+
line: "some random log output that does not match",
108+
wantOK: false,
109+
},
110+
{
111+
name: "invalid JSON after SlowBlock prefix",
112+
line: `2026-03-20 14:48:33.568+0000 | vert.x-worker-thread-0 | WARN | SlowBlock | {not valid json}`,
113+
wantOK: false,
114+
},
115+
}
116+
117+
for _, tt := range tests {
118+
t.Run(tt.name, func(t *testing.T) {
119+
result, ok := parser.ParseLine(tt.line)
120+
121+
assert.Equal(t, tt.wantOK, ok)
122+
123+
if tt.wantOK {
124+
require.NotNil(t, result)
125+
126+
var parsed map[string]any
127+
err := json.Unmarshal(result, &parsed)
128+
require.NoError(t, err)
129+
130+
tt.checkJSON(t, parsed)
131+
} else {
132+
assert.Nil(t, result)
133+
}
134+
})
135+
}
136+
}
137+
138+
func TestBesuParser_ClientType(t *testing.T) {
139+
parser := NewBesuParser()
140+
assert.Equal(t, "besu", string(parser.ClientType()))
141+
}

pkg/client/besu.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func (s *besuSpec) DefaultCommand() []string {
3232
"--rpc-http-enabled=true",
3333
"--rpc-http-host=0.0.0.0",
3434
"--rpc-http-port=8545",
35-
"--rpc-http-api=ETH,NET,CLIQUE,DEBUG,MINER,NET,PERM,ADMIN,TXPOOL,WEB3",
35+
"--rpc-http-api=ETH,NET,DEBUG,MINER,NET,PERM,ADMIN,TXPOOL,WEB3",
3636
"--rpc-http-cors-origins=*",
3737
"--Xhttp-timeout-seconds=660",
3838
"--host-allowlist=*",

0 commit comments

Comments
 (0)