Skip to content

Commit ba2f4b5

Browse files
authored
Meta Client: Add prometheus and beholder metrics (#263)
* add prometheus and beholder metrics * remove prom metrics and simplify otel metrics * simplify beholder event data * remove beholder event * update metrics to add some labels * add bids histogram and error type counting metrics * move startTime for more accurate endpoint latency measurement * remove nil check for bids received record
1 parent 623f48b commit ba2f4b5

File tree

5 files changed

+165
-5
lines changed

5 files changed

+165
-5
lines changed

pkg/txm/clientwrappers/dualbroadcast/meta_client.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,16 +132,23 @@ type MetaClient struct {
132132
ks MetaClientKeystore
133133
customURL *url.URL
134134
chainID *big.Int
135+
metrics *MetaMetrics
135136
}
136137

137-
func NewMetaClient(lggr logger.Logger, c MetaClientRPC, ks MetaClientKeystore, customURL *url.URL, chainID *big.Int) *MetaClient {
138+
func NewMetaClient(lggr logger.Logger, c MetaClientRPC, ks MetaClientKeystore, customURL *url.URL, chainID *big.Int) (*MetaClient, error) {
139+
metrics, err := NewMetaMetrics(chainID.String())
140+
if err != nil {
141+
return nil, fmt.Errorf("failed to create Meta metrics: %w", err)
142+
}
143+
138144
return &MetaClient{
139145
lggr: logger.Sugared(logger.Named(lggr, "Txm.Txm.MetaClient")),
140146
c: c,
141147
ks: ks,
142148
customURL: customURL,
143149
chainID: chainID,
144-
}
150+
metrics: metrics,
151+
}, nil
145152
}
146153

147154
func (a *MetaClient) NonceAt(ctx context.Context, address common.Address, blockNumber *big.Int) (uint64, error) {
@@ -161,10 +168,12 @@ func (a *MetaClient) SendTransaction(ctx context.Context, tx *types.Transaction,
161168
if meta != nil && meta.DualBroadcast != nil && *meta.DualBroadcast && !tx.IsPurgeable && meta.DualBroadcastParams != nil && meta.FwdrDestAddress != nil {
162169
meta, err := a.SendRequest(ctx, tx, attempt, *meta.DualBroadcastParams, tx.ToAddress)
163170
if err != nil {
171+
a.metrics.RecordSendRequestError(ctx)
164172
return fmt.Errorf("error sending request for transactionID(%d): %w", tx.ID, err)
165173
}
166174
if meta != nil {
167175
if err := a.SendOperation(ctx, tx, attempt, *meta); err != nil {
176+
a.metrics.RecordSendOperationError(ctx)
168177
return fmt.Errorf("failed to send operation for transactionID(%d): %w", tx.ID, err)
169178
}
170179
return nil
@@ -315,12 +324,21 @@ func (a *MetaClient) SendRequest(parentCtx context.Context, tx *types.Transactio
315324
}
316325
req.Header.Add("Content-Type", "application/json")
317326

327+
// Start timing for endpoint latency measurement
328+
startTime := time.Now()
318329
resp, err := http.DefaultClient.Do(req)
330+
latency := time.Since(startTime)
331+
332+
// Record latency
333+
a.metrics.RecordLatency(ctx, latency)
319334
if err != nil {
320335
return nil, fmt.Errorf("failed to send POST request: %w", err)
321336
}
322337
defer resp.Body.Close()
323338

339+
// Record status code
340+
a.metrics.RecordStatusCode(ctx, resp.StatusCode)
341+
324342
if resp.StatusCode != http.StatusOK {
325343
return nil, fmt.Errorf("request %v failed with status: %d", req, resp.StatusCode)
326344
}
@@ -338,6 +356,7 @@ func (a *MetaClient) SendRequest(parentCtx context.Context, tx *types.Transactio
338356

339357
if response.Error.ErrorMessage != "" {
340358
if strings.Contains(response.Error.ErrorMessage, "no solver operations received") {
359+
a.metrics.RecordBidsReceived(ctx, 0)
341360
return nil, nil
342361
}
343362
return nil, errors.New(response.Error.ErrorMessage)
@@ -347,6 +366,9 @@ func (a *MetaClient) SendRequest(parentCtx context.Context, tx *types.Transactio
347366
return nil, nil
348367
}
349368

369+
// Record bid count (number of solver operations received)
370+
a.metrics.RecordBidsReceived(ctx, len(response.Result.SOS))
371+
350372
if r, err := json.MarshalIndent(response.Result, "", " "); err == nil {
351373
a.lggr.Info("Response: ", string(r))
352374
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package dualbroadcast
2+
3+
import (
4+
"context"
5+
"strconv"
6+
"time"
7+
8+
"go.opentelemetry.io/otel/attribute"
9+
"go.opentelemetry.io/otel/metric"
10+
11+
"github.com/smartcontractkit/chainlink-common/pkg/beholder"
12+
)
13+
14+
// MetaMetrics handles all Meta-related metrics via OTEL
15+
type MetaMetrics struct {
16+
chainID string
17+
statusCodeCounter metric.Int64Counter
18+
latencyHistogram metric.Int64Histogram
19+
bidHistogram metric.Int64Histogram
20+
errorCounter metric.Int64Counter
21+
}
22+
23+
// NewMetaMetrics creates a new MetaMetrics instance
24+
func NewMetaMetrics(chainID string) (*MetaMetrics, error) {
25+
statusCodeCounter, err := beholder.GetMeter().Int64Counter("meta_endpoint_status_codes")
26+
if err != nil {
27+
return nil, err
28+
}
29+
30+
latencyHistogram, err := beholder.GetMeter().Int64Histogram("meta_endpoint_latency")
31+
if err != nil {
32+
return nil, err
33+
}
34+
35+
bidHistogram, err := beholder.GetMeter().Int64Histogram("meta_bids_per_transaction")
36+
if err != nil {
37+
return nil, err
38+
}
39+
40+
errorCounter, err := beholder.GetMeter().Int64Counter("meta_errors")
41+
if err != nil {
42+
return nil, err
43+
}
44+
45+
return &MetaMetrics{
46+
chainID: chainID,
47+
statusCodeCounter: statusCodeCounter,
48+
latencyHistogram: latencyHistogram,
49+
bidHistogram: bidHistogram,
50+
errorCounter: errorCounter,
51+
}, nil
52+
}
53+
54+
// RecordStatusCode records the HTTP status code from Meta endpoint
55+
func (m *MetaMetrics) RecordStatusCode(ctx context.Context, statusCode int) {
56+
m.statusCodeCounter.Add(ctx, 1,
57+
metric.WithAttributes(
58+
attribute.String("chainID", m.chainID),
59+
attribute.String("statusCode", strconv.Itoa(statusCode)),
60+
),
61+
)
62+
}
63+
64+
// RecordLatency records the latency of Meta endpoint requests
65+
func (m *MetaMetrics) RecordLatency(ctx context.Context, duration time.Duration) {
66+
m.latencyHistogram.Record(ctx, duration.Milliseconds(),
67+
metric.WithAttributes(
68+
attribute.String("chainID", m.chainID),
69+
),
70+
)
71+
}
72+
73+
// RecordBidsReceived records the distribution of bids per transaction
74+
func (m *MetaMetrics) RecordBidsReceived(ctx context.Context, bidCount int) {
75+
m.bidHistogram.Record(ctx, int64(bidCount),
76+
metric.WithAttributes(
77+
attribute.String("chainID", m.chainID),
78+
),
79+
)
80+
}
81+
82+
// RecordSendRequestError records errors from SendRequest method
83+
func (m *MetaMetrics) RecordSendRequestError(ctx context.Context) {
84+
m.errorCounter.Add(ctx, 1,
85+
metric.WithAttributes(
86+
attribute.String("chainID", m.chainID),
87+
attribute.String("errorType", "send_request"),
88+
),
89+
)
90+
}
91+
92+
// RecordSendOperationError records errors from SendOperation method
93+
func (m *MetaMetrics) RecordSendOperationError(ctx context.Context) {
94+
m.errorCounter.Add(ctx, 1,
95+
metric.WithAttributes(
96+
attribute.String("chainID", m.chainID),
97+
attribute.String("errorType", "send_operation"),
98+
),
99+
)
100+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package dualbroadcast
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestMetaMetrics(t *testing.T) {
12+
chainID := "1"
13+
14+
t.Run("NewMetaMetrics", func(t *testing.T) {
15+
metrics, err := NewMetaMetrics(chainID)
16+
require.NoError(t, err)
17+
assert.NotNil(t, metrics)
18+
assert.Equal(t, chainID, metrics.chainID)
19+
})
20+
21+
t.Run("RecordBasicMetrics", func(t *testing.T) {
22+
metrics, err := NewMetaMetrics(chainID)
23+
require.NoError(t, err)
24+
25+
ctx := t.Context()
26+
27+
// Test that these don't panic - all metrics methods
28+
metrics.RecordStatusCode(ctx, 200)
29+
metrics.RecordLatency(ctx, time.Millisecond*100)
30+
metrics.RecordBidsReceived(ctx, 5)
31+
metrics.RecordSendRequestError(ctx)
32+
metrics.RecordSendOperationError(ctx)
33+
})
34+
}

pkg/txm/clientwrappers/dualbroadcast/selector.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ import (
1212
"github.com/smartcontractkit/chainlink-evm/pkg/txm"
1313
)
1414

15-
func SelectClient(lggr logger.Logger, client client.Client, keyStore keys.ChainStore, url *url.URL, chainID *big.Int) txm.Client {
15+
func SelectClient(lggr logger.Logger, client client.Client, keyStore keys.ChainStore, url *url.URL, chainID *big.Int) (txm.Client, error) {
1616
urlString := url.String()
1717
switch {
1818
case strings.Contains(urlString, "flashbots"):
19-
return NewFlashbotsClient(client, keyStore, url)
19+
return NewFlashbotsClient(client, keyStore, url), nil
2020
default:
2121
return NewMetaClient(lggr, client, keyStore, url, chainID)
2222
}

pkg/txmgr/builder.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,11 @@ func NewTxmV2(
148148
}
149149
var c txm.Client
150150
if txmV2Config.DualBroadcast() != nil && *txmV2Config.DualBroadcast() && txmV2Config.CustomURL() != nil {
151-
c = dualbroadcast.SelectClient(lggr, client, keyStore, txmV2Config.CustomURL(), chainID)
151+
var err error
152+
c, err = dualbroadcast.SelectClient(lggr, client, keyStore, txmV2Config.CustomURL(), chainID)
153+
if err != nil {
154+
return nil, fmt.Errorf("failed to create dual broadcast client: %w", err)
155+
}
152156
} else {
153157
c = clientwrappers.NewChainClient(client)
154158
}

0 commit comments

Comments
 (0)