Skip to content

Commit 71a99d2

Browse files
committed
Add metrics to deliverorderer
Signed-off-by: Liran Funaro <liran.funaro@gmail.com>
1 parent c92c0da commit 71a99d2

24 files changed

Lines changed: 1139 additions & 331 deletions

.bob/rules/02-testing-guidelines.md

Lines changed: 272 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ This document provides specific guidelines for writing tests in the fabric-x-com
4040
```go
4141
func TestMyFunction(t *testing.T) {
4242
t.Parallel()
43-
43+
4444
t.Run("success cases", func(t *testing.T) { // ❌ Unnecessary nesting
4545
t.Parallel()
4646
for _, tc := range []struct{...}{...} {
@@ -49,7 +49,7 @@ func TestMyFunction(t *testing.T) {
4949
})
5050
}
5151
})
52-
52+
5353
t.Run("failure cases", func(t *testing.T) { // ❌ Unnecessary nesting
5454
t.Parallel()
5555
for _, tc := range []struct{...}{...} {
@@ -65,7 +65,7 @@ func TestMyFunction(t *testing.T) {
6565
```go
6666
func TestMyFunction(t *testing.T) {
6767
t.Parallel()
68-
68+
6969
// Success cases
7070
for _, tc := range []struct {
7171
name string
@@ -89,7 +89,7 @@ func TestMyFunction(t *testing.T) {
8989
require.Equal(t, tc.expected, result)
9090
})
9191
}
92-
92+
9393
// Failure cases
9494
for _, tc := range []struct {
9595
name string
@@ -206,7 +206,7 @@ require.PanicsWithValue(t, "exact error message", func() {
206206
```go
207207
func TestBucketConfig_Buckets(t *testing.T) {
208208
t.Parallel()
209-
209+
210210
// Success cases
211211
for _, tc := range []struct {
212212
name string
@@ -236,7 +236,7 @@ func TestBucketConfig_Buckets(t *testing.T) {
236236
require.Equal(t, tc.expected, result)
237237
})
238238
}
239-
239+
240240
// Failure cases
241241
for _, tc := range []struct {
242242
name string
@@ -260,6 +260,271 @@ func TestBucketConfig_Buckets(t *testing.T) {
260260
}
261261
```
262262

263+
## Metrics Testing
264+
265+
The project provides specialized helper functions in [`utils/test/metrics.go`](../../utils/test/metrics.go) for testing Prometheus metrics. Use these helpers consistently across all tests.
266+
267+
### Available Metrics Testing Functions
268+
269+
#### Direct Metric Value Assertions
270+
271+
**CORRECT** - Use [`test.RequireIntMetricValue()`](../../utils/test/metrics.go:99) for immediate assertions:
272+
```go
273+
// Assert metric equals expected value immediately
274+
test.RequireIntMetricValue(t, 5, myMetric)
275+
test.RequireIntMetricValue(t, 0, myMetric.WithLabelValues("label"))
276+
277+
// Verify initial state
278+
test.RequireIntMetricValue(t, 0, metrics.pendingRequests)
279+
```
280+
281+
#### Eventual Metric Value Assertions
282+
283+
**CORRECT** - Use [`test.EventuallyIntMetric()`](../../utils/test/metrics.go:105) when metrics update asynchronously:
284+
```go
285+
// Wait for metric to reach expected value
286+
test.EventuallyIntMetric(
287+
t,
288+
expectedValue,
289+
myMetric,
290+
5*time.Second, // waitFor duration
291+
100*time.Millisecond, // tick interval
292+
)
293+
294+
// With optional message
295+
test.EventuallyIntMetric(
296+
t,
297+
10,
298+
metrics.transactionsSentTotal,
299+
5*time.Second,
300+
10*time.Millisecond,
301+
"transactions should be sent",
302+
)
303+
```
304+
305+
#### Getting Metric Values
306+
307+
Use [`test.GetIntMetricValue()`](../../utils/test/metrics.go:92) or [`test.GetMetricValue()`](../../utils/test/metrics.go:69) when you need the value for custom assertions:
308+
309+
```go
310+
// Get integer metric value (rounded)
311+
preValue := test.GetIntMetricValue(t, metrics.transactionReceivedTotal)
312+
313+
// Perform operations...
314+
315+
// Assert using the captured value
316+
test.EventuallyIntMetric(t, preValue+10, metrics.transactionReceivedTotal,
317+
5*time.Second, 100*time.Millisecond)
318+
319+
// Get float metric value (for histograms, summaries)
320+
latency := test.GetMetricValue(t, metrics.blockMappingInRelaySeconds)
321+
require.Greater(t, latency, float64(0))
322+
323+
// Use with require.EventuallyWithT for complex conditions
324+
require.EventuallyWithT(t, func(ct *assert.CollectT) {
325+
require.Equal(ct, 0, test.GetIntMetricValue(t, metrics.queueSize))
326+
require.Equal(ct, 5, test.GetIntMetricValue(t, metrics.activeWorkers))
327+
}, 3*time.Second, 500*time.Millisecond)
328+
```
329+
330+
#### HTTP Metrics Endpoint Testing
331+
332+
For integration tests that check metrics via HTTP:
333+
334+
```go
335+
// Check that specific metrics exist in the output
336+
test.CheckMetrics(t, metricsURL, tlsConfig,
337+
"loadgen_block_sent_total",
338+
"loadgen_transaction_committed_total",
339+
)
340+
341+
// Get specific metric value from HTTP endpoint
342+
count := test.GetMetricValueFromURL(t, metricsURL, "metric_name", tlsConfig)
343+
require.Greater(t, count, 500)
344+
```
345+
346+
### Metrics Testing Best Practices
347+
348+
1. **Use Appropriate Helper**: Choose the right helper based on timing requirements:
349+
- [`test.RequireIntMetricValue()`](../../utils/test/metrics.go:99) for synchronous operations where the metric should already have the expected value
350+
- [`test.EventuallyIntMetric()`](../../utils/test/metrics.go:105) for asynchronous operations where the metric will reach the expected value after some time
351+
- [`test.GetIntMetricValue()`](../../utils/test/metrics.go:92) for capturing baseline values or when using with `require.Eventually()` for complex conditions
352+
- [`test.GetMetricValue()`](../../utils/test/metrics.go:69) for float metrics (histograms, summaries) or when you need the raw float value
353+
354+
2. **Capture Baseline Values**: When testing incremental changes, capture the metric value before the operation:
355+
```go
356+
preValue := test.GetIntMetricValue(t, metrics.transactionReceivedTotal)
357+
358+
// perform operation that adds 10 transactions
359+
sendTransactions(10)
360+
361+
// Verify the increment
362+
test.EventuallyIntMetric(t, preValue+10, metrics.transactionReceivedTotal,
363+
5*time.Second, 100*time.Millisecond)
364+
```
365+
366+
3. **Test Labeled Metrics**: Use [`WithLabelValues()`](https://pkg.go.dev/github.com/prometheus/client_golang/prometheus#CounterVec.WithLabelValues) to test specific label combinations:
367+
```go
368+
test.RequireIntMetricValue(t, 10,
369+
metrics.transactionCommittedTotal.WithLabelValues("COMMITTED"))
370+
test.RequireIntMetricValue(t, 2,
371+
metrics.transactionCommittedTotal.WithLabelValues("MALFORMED"))
372+
```
373+
374+
4. **Use Appropriate Wait Times**: For [`test.EventuallyIntMetric()`](../../utils/test/metrics.go:105):
375+
- Unit tests: 1-5 seconds wait, 10-100ms tick
376+
- Integration tests: 5-60 seconds wait, 100ms-1s tick
377+
- Examples from codebase:
378+
- Fast operations: `5*time.Second, 10*time.Millisecond`
379+
- Normal operations: `5*time.Second, 100*time.Millisecond`
380+
- Slow operations: `2*time.Second, 200*time.Millisecond`
381+
382+
5. **Test Initial State**: Always verify metrics start at expected initial values:
383+
```go
384+
test.RequireIntMetricValue(t, 0, metrics.pendingRequests)
385+
test.RequireIntMetricValue(t, 0, metrics.activeStreams)
386+
```
387+
388+
6. **Verify Metric Decrements**: When testing gauges that can decrease:
389+
```go
390+
test.RequireIntMetricValue(t, 5, metrics.queueSize)
391+
// process items
392+
test.EventuallyIntMetric(t, 0, metrics.queueSize, 2*time.Second, 100*time.Millisecond)
393+
```
394+
395+
7. **Use `require.Never()` for Bounds Testing**: When verifying metrics don't exceed limits:
396+
```go
397+
require.Never(t, func() bool {
398+
return test.GetIntMetricValue(t, metrics.counter) > maxExpected
399+
}, 3*time.Second, 1*time.Second)
400+
```
401+
402+
8. **Use `require.EventuallyWithT()` for Multi-Metric Conditions**: When checking multiple metrics, always use `require.EventuallyWithT()` instead of `require.Eventually()` to ensure clear visibility of which metric failed:
403+
```go
404+
// ✅ CORRECT - Shows which metric failed
405+
require.EventuallyWithT(t, func(ct *assert.CollectT) {
406+
require.Equal(ct, 1, test.GetIntMetricValue(t, metrics.inputQueue))
407+
require.Equal(ct, 1, test.GetIntMetricValue(t, metrics.outputQueue))
408+
require.Equal(ct, 0, test.GetIntMetricValue(t, metrics.processingQueue))
409+
}, 3*time.Second, 500*time.Millisecond)
410+
411+
// ❌ INCORRECT - Doesn't show which condition failed
412+
require.Eventually(t, func() bool {
413+
return test.GetIntMetricValue(t, metrics.inputQueue) == 1 &&
414+
test.GetIntMetricValue(t, metrics.outputQueue) == 1 &&
415+
test.GetIntMetricValue(t, metrics.processingQueue) == 0
416+
}, 3*time.Second, 500*time.Millisecond)
417+
```
418+
419+
9. **Use `require.EventuallyWithT()` for Single Metrics with Context**: When you need detailed assertion messages for a single metric:
420+
```go
421+
require.EventuallyWithT(t, func(ct *assert.CollectT) {
422+
actual := test.GetIntMetricValue(t, metrics.counter)
423+
require.Equal(ct, expected, actual, "counter should reach expected value")
424+
}, 5*time.Second, 100*time.Millisecond)
425+
```
426+
427+
10. **Test Timing Metrics**: For histogram and summary metrics, verify they are positive:
428+
```go
429+
latency := test.GetMetricValue(t, metrics.requestDurationSeconds)
430+
require.Greater(t, latency, float64(0))
431+
```
432+
433+
### Common Patterns
434+
435+
#### Pattern 1: Incremental Counter Testing
436+
```go
437+
// Capture baseline
438+
preValue := test.GetIntMetricValue(t, metrics.transactionReceivedTotal)
439+
440+
// Perform operation
441+
sendTransactions(5)
442+
443+
// Verify increment
444+
test.EventuallyIntMetric(t, preValue+5, metrics.transactionReceivedTotal,
445+
5*time.Second, 100*time.Millisecond)
446+
```
447+
448+
#### Pattern 2: Multiple Labeled Metrics
449+
```go
450+
test.RequireIntMetricValue(t, 30, metrics.notifierPendingTxIDs)
451+
test.RequireIntMetricValue(t, 6, metrics.notifierUniquePendingTxIDs)
452+
test.RequireIntMetricValue(t, 10, metrics.notifierTxIDsStatusDeliveries)
453+
test.RequireIntMetricValue(t, 0, metrics.notifierTxIDsTimeoutDeliveries)
454+
```
455+
456+
#### Pattern 3: Queue Size Monitoring
457+
```go
458+
// Verify queue fills up
459+
test.EventuallyIntMetric(t, 3, metrics.waitingTransactionsQueueSize,
460+
5*time.Second, 10*time.Millisecond)
461+
462+
// Process items
463+
processQueue()
464+
465+
// Verify queue empties
466+
test.EventuallyIntMetric(t, 0, metrics.waitingTransactionsQueueSize,
467+
5*time.Second, 10*time.Millisecond)
468+
```
469+
470+
#### Pattern 4: Complex Multi-Metric Conditions
471+
```go
472+
// Use EventuallyWithT for clear failure messages
473+
require.EventuallyWithT(t, func(ct *assert.CollectT) {
474+
require.Equal(ct, 10, test.GetIntMetricValue(t, metrics.gdgTxProcessedTotal))
475+
require.Equal(ct, 8, test.GetIntMetricValue(t, metrics.gdgValidatedTxProcessedTotal))
476+
}, 2*time.Second, 200*time.Millisecond)
477+
```
478+
479+
#### Pattern 5: Integration Test HTTP Metrics
480+
```go
481+
require.EventuallyWithT(t, func(ct *assert.CollectT) {
482+
count := test.GetMetricValueFromURL(
483+
t, metricsURL, "loadgen_transaction_committed_total", tlsConfig,
484+
)
485+
require.Greater(ct, count, 500)
486+
}, 300*time.Second, 1*time.Second)
487+
```
488+
489+
### Example: Complete Metrics Test
490+
491+
```go
492+
func TestRelayMetrics(t *testing.T) {
493+
t.Parallel()
494+
495+
env := setupTestEnvironment(t)
496+
m := env.metrics
497+
498+
// Verify initial state
499+
test.RequireIntMetricValue(t, 0, m.transactionsSentTotal)
500+
test.RequireIntMetricValue(t, 0, m.waitingTransactionsQueueSize)
501+
502+
// Submit transactions
503+
txCount := 3
504+
submitTransactions(txCount)
505+
506+
// Verify metrics update asynchronously
507+
test.EventuallyIntMetric(t, txCount, m.transactionsSentTotal,
508+
5*time.Second, 10*time.Millisecond)
509+
test.EventuallyIntMetric(t, txCount, m.waitingTransactionsQueueSize,
510+
5*time.Second, 10*time.Millisecond)
511+
512+
// Process transactions
513+
processTransactions()
514+
515+
// Verify labeled metrics
516+
test.RequireIntMetricValue(t, txCount,
517+
m.transactionsStatusReceivedTotal.WithLabelValues("COMMITTED"))
518+
519+
// Verify queue empties
520+
test.EventuallyIntMetric(t, 0, m.waitingTransactionsQueueSize,
521+
5*time.Second, 10*time.Millisecond)
522+
523+
// Verify timing metrics are positive
524+
require.Greater(t, test.GetMetricValue(t, m.processingSeconds), float64(0))
525+
}
526+
```
527+
263528
## Summary
264529

265530
- **No nested test groups** - Keep table-driven tests flat
@@ -269,3 +534,4 @@ func TestBucketConfig_Buckets(t *testing.T) {
269534
- **Descriptive test names**
270535
- **Helper functions at the end with `t.Helper()`**
271536
- **Never use `panic()` in tests**
537+
- **Use metrics testing helpers** - [`test.RequireIntMetricValue()`](../../utils/test/metrics.go:99), [`test.EventuallyIntMetric()`](../../utils/test/metrics.go:105), [`test.GetIntMetricValue()`](../../utils/test/metrics.go:92)

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ test/**/*.log
4343

4444
.DS_Store
4545
venv
46+
**/__pycache__
4647

4748
# MkDocs build output
4849
site/

0 commit comments

Comments
 (0)