Repository: ethereum-optimism/optimism
Pull request #14841: op-supervisor: Add RPC metrics
Author: login: ghost
Created: 2025-03-13T15:18:21Z | Merged: 2025-03-18T22:21:33Z

- Adds server RPC metrics to the frontend.
- Adds client RPC metric _support_.
- Enables client RPC metrics in packages where metrics were readily available.

Note: I found two ways of doing this; with `RPCMetricer` and with `WithHTTPRecorder`. The former seemed more specialized and appropriate for what we're doing so I used it.

closes https://github.com/ethereum-optimism/optimism/issues/14272


Diff:
diff --git a/devnet-sdk/system/system.go b/devnet-sdk/system/system.go
index a831a58d3f3..162145831f2 100644
--- a/devnet-sdk/system/system.go
+++ b/devnet-sdk/system/system.go
@@ -106,7 +106,7 @@ func (i *interopSystem) Supervisor(ctx context.Context) (Supervisor, error) {
 	if err != nil {
 		return nil, fmt.Errorf("failed to dial supervisor RPC: %w", err)
 	}
-	supervisor := sources.NewSupervisorClient(cl)
+	supervisor := sources.NewSupervisorClient(cl, nil)
 	i.supervisor = supervisor
 	return supervisor, nil
 }
diff --git a/op-challenger/game/fault/clients.go b/op-challenger/game/fault/clients.go
index 8651917169d..0b2045c6f22 100644
--- a/op-challenger/game/fault/clients.go
+++ b/op-challenger/game/fault/clients.go
@@ -79,7 +79,7 @@ func (c *clientProvider) SuperchainClients() (super.RootProvider, *super.SyncVal
 	if err != nil {
 		return nil, nil, fmt.Errorf("failed to dial supervisor: %w", err)
 	}
-	supervisorClient := sources.NewSupervisorClient(cl)
+	supervisorClient := sources.NewSupervisorClient(cl, nil)
 	c.rootProvider = supervisorClient
 	c.toClose = append(c.toClose, supervisorClient.Close)
 	return supervisorClient, super.NewSyncValidator(supervisorClient), nil
diff --git a/op-challenger/runner/metrics.go b/op-challenger/runner/metrics.go
index 5977727caec..15e65fab596 100644
--- a/op-challenger/runner/metrics.go
+++ b/op-challenger/runner/metrics.go
@@ -18,6 +18,7 @@ type Metrics struct {
 	factory  opmetrics.Factory
 	*contractMetrics.ContractMetrics
 	*metrics.VmMetrics
+	*opmetrics.RPCClientMetrics
 
 	up                  prometheus.Gauge
 	vmLastExecutionTime *prometheus.GaugeVec
@@ -42,8 +43,9 @@ func NewMetrics(runConfigs []RunConfig) *Metrics {
 		registry: registry,
 		factory:  factory,
 
-		ContractMetrics: contractMetrics.MakeContractMetrics(Namespace, factory),
-		VmMetrics:       metrics.NewVmMetrics(Namespace, factory),
+		ContractMetrics:  contractMetrics.MakeContractMetrics(Namespace, factory),
+		VmMetrics:        metrics.NewVmMetrics(Namespace, factory),
+		RPCClientMetrics: &opmetrics.RPCClientMetrics{},
 
 		up: factory.NewGauge(prometheus.GaugeOpts{
 			Namespace: Namespace,
diff --git a/op-challenger/runner/runner.go b/op-challenger/runner/runner.go
index b717d286d83..71cb957dc9e 100644
--- a/op-challenger/runner/runner.go
+++ b/op-challenger/runner/runner.go
@@ -38,6 +38,7 @@ var (
 type Metricer interface {
 	contractMetrics.ContractMetricer
 	metrics.VmMetricer
+	opmetrics.RPCClientMetricer
 
 	RecordFailure(vmType string)
 	RecordPanic(vmType string)
@@ -101,7 +102,7 @@ func (r *Runner) Start(ctx context.Context) error {
 		if err != nil {
 			return fmt.Errorf("failed to dial rollup client: %w", err)
 		}
-		supervisorClient = sources.NewSupervisorClient(client.NewBaseRPCClient(rpcCl))
+		supervisorClient = sources.NewSupervisorClient(client.NewBaseRPCClient(rpcCl), r.m)
 	}
 
 	l1Client, err := dial.DialRPCClientWithTimeout(ctx, 1*time.Minute, r.log, r.cfg.L1EthRpc)
diff --git a/op-dispute-mon/metrics/metrics.go b/op-dispute-mon/metrics/metrics.go
index 0d2a848ffc8..51169cd92e2 100644
--- a/op-dispute-mon/metrics/metrics.go
+++ b/op-dispute-mon/metrics/metrics.go
@@ -187,6 +187,7 @@ type Metricer interface {
 
 	caching.Metrics
 	contractMetrics.ContractMetricer
+	opmetrics.RPCClientMetricer
 }
 
 // Metrics implementation must implement RegistryMetricer to allow the metrics server to work.
@@ -199,6 +200,7 @@ type Metrics struct {
 
 	*opmetrics.CacheMetrics
 	*contractMetrics.ContractMetrics
+	*opmetrics.RPCClientMetrics
 
 	monitorDuration prometheus.Histogram
 
@@ -246,9 +248,9 @@ func NewMetrics() *Metrics {
 		registry: registry,
 		factory:  factory,
 
-		CacheMetrics:    opmetrics.NewCacheMetrics(factory, Namespace, "provider_cache", "Provider cache"),
-		ContractMetrics: contractMetrics.MakeContractMetrics(Namespace, factory),
-
+		CacheMetrics:     opmetrics.NewCacheMetrics(factory, Namespace, "provider_cache", "Provider cache"),
+		ContractMetrics:  contractMetrics.MakeContractMetrics(Namespace, factory),
+		RPCClientMetrics: &opmetrics.RPCClientMetrics{},
 		info: *factory.NewGaugeVec(prometheus.GaugeOpts{
 			Namespace: Namespace,
 			Name:      "info",
diff --git a/op-dispute-mon/metrics/noop.go b/op-dispute-mon/metrics/noop.go
index 4459fdd9c1f..c58954d29f4 100644
--- a/op-dispute-mon/metrics/noop.go
+++ b/op-dispute-mon/metrics/noop.go
@@ -5,11 +5,13 @@ import (
 	"time"
 
 	contractMetrics "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts/metrics"
+	opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
 	"github.com/ethereum/go-ethereum/common"
 )
 
 type NoopMetricsImpl struct {
 	contractMetrics.NoopMetrics
+	opmetrics.NoopRPCMetrics
 }
 
 var NoopMetrics Metricer = new(NoopMetricsImpl)
diff --git a/op-dispute-mon/mon/service.go b/op-dispute-mon/mon/service.go
index 817501e3a59..8c966bba1cb 100644
--- a/op-dispute-mon/mon/service.go
+++ b/op-dispute-mon/mon/service.go
@@ -176,7 +176,7 @@ func (s *Service) initSupervisorClient(ctx context.Context, cfg *config.Config)
 	if err != nil {
 		return fmt.Errorf("failed to dial supervisor client: %w", err)
 	}
-	s.supervisorClient = sources.NewSupervisorClient(rpcclient.NewBaseRPCClient(rpcClient))
+	s.supervisorClient = sources.NewSupervisorClient(rpcclient.NewBaseRPCClient(rpcClient), s.metrics)
 	return nil
 }
 
diff --git a/op-e2e/actions/interop/dsl/interop.go b/op-e2e/actions/interop/dsl/interop.go
index dec5aff6300..b72de10fdb7 100644
--- a/op-e2e/actions/interop/dsl/interop.go
+++ b/op-e2e/actions/interop/dsl/interop.go
@@ -236,7 +236,7 @@ func NewSupervisor(t helpers.Testing, logger log.Logger, depSet depset.Dependenc
 	rpcServer := helpers.NewSimpleRPCServer()
 	supervisor.RegisterRPCs(logger, svCfg, rpcServer, b)
 	rpcServer.Start(t)
-	supervisorClient := sources.NewSupervisorClient(rpcServer.Connect(t))
+	supervisorClient := sources.NewSupervisorClient(rpcServer.Connect(t), nil)
 	return &SupervisorActor{
 		exec:             evExec,
 		backend:          b,
diff --git a/op-e2e/interop/supersystem.go b/op-e2e/interop/supersystem.go
index 840eb1c2b48..da9bdb3c08c 100644
--- a/op-e2e/interop/supersystem.go
+++ b/op-e2e/interop/supersystem.go
@@ -334,7 +334,7 @@ func (s *interopE2ESystem) SupervisorClient() *sources.SupervisorClient {
 	}
 	cl, err := client.NewRPC(context.Background(), s.logger, s.supervisor.RPC())
 	require.NoError(s.t, err, "failed to dial supervisor RPC")
-	superClient := sources.NewSupervisorClient(cl)
+	superClient := sources.NewSupervisorClient(cl, nil)
 	s.superClient = superClient
 	return superClient
 }
diff --git a/op-node/rollup/interop/config.go b/op-node/rollup/interop/config.go
index 129acbf05f9..227658ee568 100644
--- a/op-node/rollup/interop/config.go
+++ b/op-node/rollup/interop/config.go
@@ -54,6 +54,6 @@ func (cfg *Config) Setup(ctx context.Context, logger log.Logger, rollupCfg *roll
 		if err != nil {
 			return nil, fmt.Errorf("failed to create supervisor RPC: %w", err)
 		}
-		return standard.NewStandardMode(logger, sources.NewSupervisorClient(cl)), nil
+		return standard.NewStandardMode(logger, sources.NewSupervisorClient(cl, nil)), nil
 	}
 }
diff --git a/op-proposer/proposer/service.go b/op-proposer/proposer/service.go
index 8a0fbb2f70e..7043f2722e6 100644
--- a/op-proposer/proposer/service.go
+++ b/op-proposer/proposer/service.go
@@ -152,7 +152,7 @@ func (ps *ProposerService) initRPCClients(ctx context.Context, cfg *CLIConfig) e
 			if err != nil {
 				return fmt.Errorf("failed to dial supervisor RPC client (%v): %w", url, err)
 			}
-			cl := sources.NewSupervisorClient(client.NewBaseRPCClient(supervisorRpc))
+			cl := sources.NewSupervisorClient(client.NewBaseRPCClient(supervisorRpc), ps.Metrics)
 			clients = append(clients, cl)
 		}
 		ps.ProposalSource = source.NewSupervisorProposalSource(ps.Log, clients...)
diff --git a/op-service/sources/supervisor_client.go b/op-service/sources/supervisor_client.go
index 5641fca3f62..3f7b7d7c050 100644
--- a/op-service/sources/supervisor_client.go
+++ b/op-service/sources/supervisor_client.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/ethereum-optimism/optimism/op-service/client"
 	"github.com/ethereum-optimism/optimism/op-service/eth"
+	opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
 	"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
 	"github.com/ethereum/go-ethereum"
 	"github.com/ethereum/go-ethereum/common"
@@ -33,22 +34,31 @@ type SupervisorQueryAPI interface {
 }
 
 type SupervisorClient struct {
-	client client.RPC
+	client  client.RPC
+	metrics opmetrics.RPCClientMetricer
 }
 
 // This type-check keeps the Server API and Client API in sync.
 var _ SupervisorQueryAPI = (*SupervisorClient)(nil)
 var _ SupervisorAdminAPI = (*SupervisorClient)(nil)
 
-func NewSupervisorClient(client client.RPC) *SupervisorClient {
+func NewSupervisorClient(client client.RPC, metrics opmetrics.RPCClientMetricer) *SupervisorClient {
+	if metrics == nil {
+		metrics = &opmetrics.NoopRPCMetrics{}
+	}
 	return &SupervisorClient{
-		client: client,
+		client:  client,
+		metrics: metrics,
 	}
 }
 
 func (cl *SupervisorClient) Stop(ctx context.Context) error {
+	done := cl.metrics.RecordRPCClientRequest("admin_stop")
+	var err error
+	defer func() { done(err) }()
+
 	var result error
-	err := cl.client.CallContext(ctx, &result, "admin_stop")
+	err = cl.client.CallContext(ctx, &result, "admin_stop")
 	if err != nil {
 		return fmt.Errorf("failed to stop Supervisor: %w", err)
 	}
@@ -56,8 +66,12 @@ func (cl *SupervisorClient) Stop(ctx context.Context) error {
 }
 
 func (cl *SupervisorClient) Start(ctx context.Context) error {
+	done := cl.metrics.RecordRPCClientRequest("admin_start")
+	var err error
+	defer func() { done(err) }()
+
 	var result error
-	err := cl.client.CallContext(ctx, &result, "admin_start")
+	err = cl.client.CallContext(ctx, &result, "admin_start")
 	if err != nil {
 		return fmt.Errorf("failed to start Supervisor: %w", err)
 	}
@@ -65,8 +79,12 @@ func (cl *SupervisorClient) Start(ctx context.Context) error {
 }
 
 func (cl *SupervisorClient) AddL2RPC(ctx context.Context, rpc string, auth eth.Bytes32) error {
+	done := cl.metrics.RecordRPCClientRequest("admin_addL2RPC")
+	var err error
+	defer func() { done(err) }()
+
 	var result error
-	err := cl.client.CallContext(ctx, &result, "admin_addL2RPC", rpc, auth)
+	err = cl.client.CallContext(ctx, &result, "admin_addL2RPC", rpc, auth)
 	if err != nil {
 		return fmt.Errorf("failed to Add L2 to Supervisor (rpc: %s): %w", rpc, err)
 	}
@@ -75,74 +93,106 @@ func (cl *SupervisorClient) AddL2RPC(ctx context.Context, rpc string, auth eth.B
 
 func (cl *SupervisorClient) CheckAccessList(ctx context.Context, inboxEntries []common.Hash,
 	minSafety types.SafetyLevel, executingDescriptor types.ExecutingDescriptor) error {
-	return cl.client.CallContext(ctx, nil, "supervisor_checkAccessList", inboxEntries, minSafety, executingDescriptor)
+	done := cl.metrics.RecordRPCClientRequest("supervisor_checkAccessList")
+	var err error
+	defer func() { done(err) }()
+
+	err = cl.client.CallContext(ctx, nil, "supervisor_checkAccessList", inboxEntries, minSafety, executingDescriptor)
+	return err
 }
 
 func (cl *SupervisorClient) CrossDerivedToSource(ctx context.Context, chainID eth.ChainID, derived eth.BlockID) (derivedFrom eth.BlockRef, err error) {
-	var result eth.BlockRef
-	err = cl.client.CallContext(ctx, &result, "supervisor_crossDerivedToSource", chainID, derived)
-	return result, err
+	done := cl.metrics.RecordRPCClientRequest("supervisor_crossDerivedToSource")
+	defer func() { done(err) }()
+
+	err = cl.client.CallContext(ctx, &derivedFrom, "supervisor_crossDerivedToSource", chainID, derived)
+	return derivedFrom, err
 }
 
-func (cl *SupervisorClient) LocalUnsafe(ctx context.Context, chainID eth.ChainID) (eth.BlockID, error) {
-	var result eth.BlockID
-	err := cl.client.CallContext(ctx, &result, "supervisor_localUnsafe", chainID)
+func (cl *SupervisorClient) LocalUnsafe(ctx context.Context, chainID eth.ChainID) (result eth.BlockID, err error) {
+	done := cl.metrics.RecordRPCClientRequest("supervisor_localUnsafe")
+	defer func() { done(err) }()
+
+	err = cl.client.CallContext(ctx, &result, "supervisor_localUnsafe", chainID)
 	return result, err
 }
 
-func (cl *SupervisorClient) CrossSafe(ctx context.Context, chainID eth.ChainID) (types.DerivedIDPair, error) {
-	var result types.DerivedIDPair
-	err := cl.client.CallContext(ctx, &result, "supervisor_crossSafe", chainID)
+func (cl *SupervisorClient) CrossSafe(ctx context.Context, chainID eth.ChainID) (result types.DerivedIDPair, err error) {
+	done := cl.metrics.RecordRPCClientRequest("supervisor_crossSafe")
+	defer func() { done(err) }()
+
+	err = cl.client.CallContext(ctx, &result, "supervisor_crossSafe", chainID)
 	return result, err
 }
 
-func (cl *SupervisorClient) Finalized(ctx context.Context, chainID eth.ChainID) (eth.BlockID, error) {
-	var result eth.BlockID
-	err := cl.client.CallContext(ctx, &result, "supervisor_finalized", chainID)
+func (cl *SupervisorClient) Finalized(ctx context.Context, chainID eth.ChainID) (result eth.BlockID, err error) {
+	done := cl.metrics.RecordRPCClientRequest("supervisor_finalized")
+	defer func() { done(err) }()
+
+	err = cl.client.CallContext(ctx, &result, "supervisor_finalized", chainID)
 	return result, err
 }
 
-func (cl *SupervisorClient) FinalizedL1(ctx context.Context) (eth.BlockRef, error) {
-	var result eth.BlockRef
-	err := cl.client.CallContext(ctx, &result, "supervisor_finalizedL1")
+func (cl *SupervisorClient) FinalizedL1(ctx context.Context) (result eth.BlockRef, err error) {
+	done := cl.metrics.RecordRPCClientRequest("supervisor_finalizedL1")
+	defer func() { done(err) }()
+
+	err = cl.client.CallContext(ctx, &result, "supervisor_finalizedL1")
 	return result, err
 }
 
-func (cl *SupervisorClient) CrossDerivedFrom(ctx context.Context, chainID eth.ChainID, derived eth.BlockID) (eth.BlockRef, error) {
-	var result eth.BlockRef
-	err := cl.client.CallContext(ctx, &result, "supervisor_crossDerivedFrom", chainID, derived)
+func (cl *SupervisorClient) CrossDerivedFrom(ctx context.Context, chainID eth.ChainID, derived eth.BlockID) (result eth.BlockRef, err error) {
+	done := cl.metrics.RecordRPCClientRequest("supervisor_crossDerivedFrom")
+	defer func() { done(err) }()
+
+	err = cl.client.CallContext(ctx, &result, "supervisor_crossDerivedFrom", chainID, derived)
 	return result, err
 }
 
-func (cl *SupervisorClient) UpdateLocalUnsafe(ctx context.Context, chainID eth.ChainID, head eth.BlockRef) error {
-	return cl.client.CallContext(ctx, nil, "supervisor_updateLocalUnsafe", chainID, head)
+func (cl *SupervisorClient) UpdateLocalUnsafe(ctx context.Context, chainID eth.ChainID, head eth.BlockRef) (err error) {
+	done := cl.metrics.RecordRPCClientRequest("supervisor_updateLocalUnsafe")
+	defer func() { done(err) }()
+
+	err = cl.client.CallContext(ctx, nil, "supervisor_updateLocalUnsafe", chainID, head)
+	return err
 }
 
-func (cl *SupervisorClient) UpdateLocalSafe(ctx context.Context, chainID eth.ChainID, derivedFrom eth.L1BlockRef, lastDerived eth.BlockRef) error {
-	return cl.client.CallContext(ctx, nil, "supervisor_updateLocalSafe", chainID, derivedFrom, lastDerived)
+func (cl *SupervisorClient) UpdateLocalSafe(ctx context.Context, chainID eth.ChainID, derivedFrom eth.L1BlockRef, lastDerived eth.BlockRef) (err error) {
+	done := cl.metrics.RecordRPCClientRequest("supervisor_updateLocalSafe")
+	defer func() { done(err) }()
+
+	err = cl.client.CallContext(ctx, nil, "supervisor_updateLocalSafe", chainID, derivedFrom, lastDerived)
+	return err
 }
 
 // SuperRootAtTimestamp returns the super root at the specified timestamp.
 // Returns ethereum.NotFound if one of the chain's has not yet reached the block required for the requested super root.
-func (cl *SupervisorClient) SuperRootAtTimestamp(ctx context.Context, timestamp hexutil.Uint64) (eth.SuperRootResponse, error) {
-	var result eth.SuperRootResponse
-	err := cl.client.CallContext(ctx, &result, "supervisor_superRootAtTimestamp", timestamp)
+func (cl *SupervisorClient) SuperRootAtTimestamp(ctx context.Context, timestamp hexutil.Uint64) (result eth.SuperRootResponse, err error) {
+	done := cl.metrics.RecordRPCClientRequest("supervisor_superRootAtTimestamp")
+	defer func() { done(err) }()
+
+	err = cl.client.CallContext(ctx, &result, "supervisor_superRootAtTimestamp", timestamp)
 	if isNotFound(err) {
 		// Downstream users expect to get a properly typed error message for not found.
-		return result, fmt.Errorf("%w: %v", ethereum.NotFound, err.Error())
+		err = fmt.Errorf("%w: %v", ethereum.NotFound, err.Error())
+		return result, err
 	}
 	return result, err
 }
 
-func (cl *SupervisorClient) AllSafeDerivedAt(ctx context.Context, derivedFrom eth.BlockID) (map[eth.ChainID]eth.BlockID, error) {
-	var result map[eth.ChainID]eth.BlockID
-	err := cl.client.CallContext(ctx, &result, "supervisor_allSafeDerivedAt", derivedFrom)
+func (cl *SupervisorClient) AllSafeDerivedAt(ctx context.Context, derivedFrom eth.BlockID) (result map[eth.ChainID]eth.BlockID, err error) {
+	done := cl.metrics.RecordRPCClientRequest("supervisor_allSafeDerivedAt")
+	defer func() { done(err) }()
+
+	err = cl.client.CallContext(ctx, &result, "supervisor_allSafeDerivedAt", derivedFrom)
 	return result, err
 }
 
-func (cl *SupervisorClient) SyncStatus(ctx context.Context) (eth.SupervisorSyncStatus, error) {
-	var result eth.SupervisorSyncStatus
-	err := cl.client.CallContext(ctx, &result, "supervisor_syncStatus")
+func (cl *SupervisorClient) SyncStatus(ctx context.Context) (result eth.SupervisorSyncStatus, err error) {
+	done := cl.metrics.RecordRPCClientRequest("supervisor_syncStatus")
+	defer func() { done(err) }()
+
+	err = cl.client.CallContext(ctx, &result, "supervisor_syncStatus")
 	return result, err
 }
 
diff --git a/op-service/sources/supervisor_client_test.go b/op-service/sources/supervisor_client_test.go
index cf200adf605..2ba4913b291 100644
--- a/op-service/sources/supervisor_client_test.go
+++ b/op-service/sources/supervisor_client_test.go
@@ -18,7 +18,7 @@ func TestSupervisorClient_SuperRootAtTimestamp(t *testing.T) {
 		ctx := context.Background()
 		rpc := new(mockRPC)
 		defer rpc.AssertExpectations(t)
-		client := NewSupervisorClient(rpc)
+		client := NewSupervisorClient(rpc, nil)
 
 		timestamp := hexutil.Uint64(245)
 
@@ -44,7 +44,7 @@ func TestSupervisorClient_SuperRootAtTimestamp(t *testing.T) {
 		ctx := context.Background()
 		rpc := new(mockRPC)
 		defer rpc.AssertExpectations(t)
-		client := NewSupervisorClient(rpc)
+		client := NewSupervisorClient(rpc, nil)
 
 		timestamp := hexutil.Uint64(245)
 
diff --git a/op-supervisor/supervisor/frontend/frontend.go b/op-supervisor/supervisor/frontend/frontend.go
index fce86030488..17055907452 100644
--- a/op-supervisor/supervisor/frontend/frontend.go
+++ b/op-supervisor/supervisor/frontend/frontend.go
@@ -2,15 +2,21 @@ package frontend
 
 import (
 	"context"
+	"fmt"
 
 	"github.com/ethereum/go-ethereum/common"
 	"github.com/ethereum/go-ethereum/common/hexutil"
 
 	"github.com/ethereum-optimism/optimism/op-service/eth"
+	opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
 	"github.com/ethereum-optimism/optimism/op-service/sources"
 	"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
 )
 
+type FrontendMetrics interface {
+	opmetrics.RPCServerMetricer
+}
+
 type Backend interface {
 	sources.SupervisorAdminAPI
 	sources.SupervisorQueryAPI
@@ -18,70 +24,103 @@ type Backend interface {
 
 type QueryFrontend struct {
 	Supervisor sources.SupervisorQueryAPI
+	Metrics    FrontendMetrics
 }
 
 var _ sources.SupervisorQueryAPI = (*QueryFrontend)(nil)
 
 func (q *QueryFrontend) CheckAccessList(ctx context.Context, inboxEntries []common.Hash,
 	minSafety types.SafetyLevel, executingDescriptor types.ExecutingDescriptor) error {
+	defer q.maybeRecordMetric("checkAccessList")
 	return q.Supervisor.CheckAccessList(ctx, inboxEntries, minSafety, executingDescriptor)
 }
 
 func (q *QueryFrontend) LocalUnsafe(ctx context.Context, chainID eth.ChainID) (eth.BlockID, error) {
+	defer q.maybeRecordMetric("localUnsafe")
 	return q.Supervisor.LocalUnsafe(ctx, chainID)
 }
 
 func (q *QueryFrontend) CrossSafe(ctx context.Context, chainID eth.ChainID) (types.DerivedIDPair, error) {
+	defer q.maybeRecordMetric("crossSafe")
 	return q.Supervisor.CrossSafe(ctx, chainID)
 }
 
 func (q *QueryFrontend) Finalized(ctx context.Context, chainID eth.ChainID) (eth.BlockID, error) {
+	defer q.maybeRecordMetric("finalized")
 	return q.Supervisor.Finalized(ctx, chainID)
 }
 
 func (q *QueryFrontend) FinalizedL1(ctx context.Context) (eth.BlockRef, error) {
+	defer q.maybeRecordMetric("finalizedL1")
 	return q.Supervisor.FinalizedL1(ctx)
 }
 
 // CrossDerivedFrom is deprecated, but remains for backwards compatibility to callers
 // it is equivalent to CrossDerivedToSource
 func (q *QueryFrontend) CrossDerivedFrom(ctx context.Context, chainID eth.ChainID, derived eth.BlockID) (derivedFrom eth.BlockRef, err error) {
+	defer q.maybeRecordMetric("crossDerivedFrom")
 	return q.Supervisor.CrossDerivedToSource(ctx, chainID, derived)
 }
 
 func (q *QueryFrontend) CrossDerivedToSource(ctx context.Context, chainID eth.ChainID, derived eth.BlockID) (derivedFrom eth.BlockRef, err error) {
+	defer q.maybeRecordMetric("crossDerivedToSource")
 	return q.Supervisor.CrossDerivedToSource(ctx, chainID, derived)
 }
 
 func (q *QueryFrontend) SuperRootAtTimestamp(ctx context.Context, timestamp hexutil.Uint64) (eth.SuperRootResponse, error) {
+	defer q.maybeRecordMetric("superRootAtTimestamp")
 	return q.Supervisor.SuperRootAtTimestamp(ctx, timestamp)
 }
 
 func (q *QueryFrontend) AllSafeDerivedAt(ctx context.Context, derivedFrom eth.BlockID) (derived map[eth.ChainID]eth.BlockID, err error) {
+	defer q.maybeRecordMetric("allSafeDerivedAt")
 	return q.Supervisor.AllSafeDerivedAt(ctx, derivedFrom)
 }
 
 func (q *QueryFrontend) SyncStatus(ctx context.Context) (eth.SupervisorSyncStatus, error) {
+	defer q.maybeRecordMetric("syncStatus")
 	return q.Supervisor.SyncStatus(ctx)
 }
 
+func (q *QueryFrontend) maybeRecordMetric(name string) func() {
+	return maybeRecordMetric(q.Metrics, "supervisor", name)
+}
+
 type AdminFrontend struct {
 	Supervisor Backend
+	Metrics    FrontendMetrics
 }
 
 var _ sources.SupervisorAdminAPI = (*AdminFrontend)(nil)
 
 // Start starts the service, if it was previously stopped.
 func (a *AdminFrontend) Start(ctx context.Context) error {
+	defer a.maybeRecordMetric("start")
 	return a.Supervisor.Start(ctx)
 }
 
 // Stop stops the service, if it was previously started.
 func (a *AdminFrontend) Stop(ctx context.Context) error {
+	defer a.maybeRecordMetric("stop")
 	return a.Supervisor.Stop(ctx)
 }
 
 // AddL2RPC adds a new L2 chain to the supervisor backend
 func (a *AdminFrontend) AddL2RPC(ctx context.Context, rpc string, jwtSecret eth.Bytes32) error {
+	defer a.maybeRecordMetric("addL2RPC")
 	return a.Supervisor.AddL2RPC(ctx, rpc, jwtSecret)
 }
+
+func (a *AdminFrontend) maybeRecordMetric(name string) func() {
+	return maybeRecordMetric(a.Metrics, "admin", name)
+}
+
+// maybeRecordMetric emits metrics in the given namespace, only if we have an instance.
+// It returns a function to be invoked at the end of the method being recorded.
+func maybeRecordMetric(m FrontendMetrics, ns string, endpoint string) func() {
+	if m != nil {
+		method := fmt.Sprintf("%s_%s", ns, endpoint)
+		return m.RecordRPCServerRequest(method)
+	}
+	return func() {}
+}
diff --git a/op-supervisor/supervisor/service.go b/op-supervisor/supervisor/service.go
index 2f04d889183..c9b0e2d717b 100644
--- a/op-supervisor/supervisor/service.go
+++ b/op-supervisor/supervisor/service.go
@@ -152,7 +152,6 @@ func (su *SupervisorService) initRPCServer(cfg *config.Config) error {
 		cfg.RPC.ListenPort,
 		cfg.Version,
 		oprpc.WithLogger(su.log),
-		// oprpc.WithHTTPRecorder(su.metrics), // TODO(protocol-quest#286) hook up metrics to RPC server
 	)
 	RegisterRPCs(su.log, cfg, server, su.backend)
 	su.rpcServer = server
@@ -164,17 +163,21 @@ type RpcServer interface {
 }
 
 func RegisterRPCs(logger log.Logger, cfg *config.Config, server RpcServer, backend Backend) {
+	m := metrics.NoopMetrics
+	if cfg.MetricsConfig.Enabled {
+		m = metrics.NewMetrics("default")
+	}
 	if cfg.RPC.EnableAdmin {
 		logger.Info("Admin RPC enabled")
 		server.AddAPI(rpc.API{
 			Namespace:     "admin",
-			Service:       &frontend.AdminFrontend{Supervisor: backend},
+			Service:       &frontend.AdminFrontend{Supervisor: backend, Metrics: m},
 			Authenticated: true, // TODO(protocol-quest#286): enforce auth on this or not?
 		})
 	}
 	server.AddAPI(rpc.API{
 		Namespace:     "supervisor",
-		Service:       &frontend.QueryFrontend{Supervisor: backend},
+		Service:       &frontend.QueryFrontend{Supervisor: backend, Metrics: m},
 		Authenticated: false,
 	})
 }


Reviews:
user: login: axelKingsley
body: Looks good. Left one code styling suggestion, but then also: could you run this briefly in Kurtosis to make sure a live setup has all the appropriate wires? It looks like everything is in place, but a nil metric system can cause crashes and the best way to see if we missed something is to try it.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T15:37:32Z
user: login: axelKingsley
body: Approved with one additional optional style suggestion.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T20:34:53Z
user: login: tynes
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T22:09:09Z
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-19T03:44:30Z

Review comments:
diff_hunk: @@ -7,24 +7,34 @@ import (
 	"github.com/ethereum/go-ethereum/common/hexutil"
 
 	"github.com/ethereum-optimism/optimism/op-service/eth"
+	opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
 	"github.com/ethereum-optimism/optimism/op-service/sources"
 	"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
 )
 
+type FrontendMetrics interface {
+	opmetrics.RPCServerMetricer
+}
+
 type Backend interface {
 	sources.SupervisorAdminAPI
 	sources.SupervisorQueryAPI
 }
 
 type QueryFrontend struct {
 	Supervisor sources.SupervisorQueryAPI
+	Metrics    FrontendMetrics
 }
 
 var _ sources.SupervisorQueryAPI = (*QueryFrontend)(nil)
 
 // CheckMessage checks the safety-level of an individual message.
 // The payloadHash references the hash of the message-payload of the message.
 func (q *QueryFrontend) CheckMessage(ctx context.Context, identifier types.Identifier, payloadHash common.Hash, executingDescriptor types.ExecutingDescriptor) (types.SafetyLevel, error) {
+	if q.Metrics != nil {
+		done := q.Metrics.RecordRPCServerRequest("supervisor_checkMessage")
+		defer done()
+	}
path: op-supervisor/supervisor/frontend/frontend.go
user: login: axelKingsley
body: You could make this into a helper, like:

```golang
func (q *QueryFrontend)maybeRecordMetric(recordFn func()) {
  if q.Metrics != nil {
    recordFn()
  }
}
```

And then in the functions individually,
```golang
defer q.maybeRecordMetric(q.Metrics.RecordRPCServerRequest("..."))
```

By deferring the whole `if` clause it can get abstracted out.
created_at: 2025-03-14T15:30:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,40 +35,57 @@ type SupervisorQueryAPI interface {
 }
 
 type SupervisorClient struct {
-	client client.RPC
+	client  client.RPC
+	metrics opmetrics.RPCClientMetricer
 }
 
 // This type-check keeps the Server API and Client API in sync.
 var _ SupervisorQueryAPI = (*SupervisorClient)(nil)
 var _ SupervisorAdminAPI = (*SupervisorClient)(nil)
 
-func NewSupervisorClient(client client.RPC) *SupervisorClient {
+func NewSupervisorClient(client client.RPC, metrics opmetrics.RPCClientMetricer) *SupervisorClient {
+	if metrics == nil {
+		metrics = &opmetrics.NoopRPCMetrics{}
+	}
 	return &SupervisorClient{
-		client: client,
+		client:  client,
+		metrics: metrics,
 	}
 }
 
 func (cl *SupervisorClient) Stop(ctx context.Context) error {
+	done := cl.metrics.RecordRPCClientRequest("admin_stop")
+	var err error
+	defer func() { done(err) }()
path: op-service/sources/supervisor_client.go
user: login: axelKingsley
body: nit: these "done" sections could be condensed too:

```
var err error
defer cl.metrics.RecordRPCClientRequest("admin_stop")(err)
```
created_at: 2025-03-14T20:34:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,40 +35,57 @@ type SupervisorQueryAPI interface {
 }
 
 type SupervisorClient struct {
-	client client.RPC
+	client  client.RPC
+	metrics opmetrics.RPCClientMetricer
 }
 
 // This type-check keeps the Server API and Client API in sync.
 var _ SupervisorQueryAPI = (*SupervisorClient)(nil)
 var _ SupervisorAdminAPI = (*SupervisorClient)(nil)
 
-func NewSupervisorClient(client client.RPC) *SupervisorClient {
+func NewSupervisorClient(client client.RPC, metrics opmetrics.RPCClientMetricer) *SupervisorClient {
+	if metrics == nil {
+		metrics = &opmetrics.NoopRPCMetrics{}
+	}
 	return &SupervisorClient{
-		client: client,
+		client:  client,
+		metrics: metrics,
 	}
 }
 
 func (cl *SupervisorClient) Stop(ctx context.Context) error {
+	done := cl.metrics.RecordRPCClientRequest("admin_stop")
+	var err error
+	defer func() { done(err) }()
path: op-service/sources/supervisor_client.go
body: @axelKingsley I think this doesn't work because the value of `err` that is passed to the defer'd function is bound at the time we call defer, before we set `err`.
created_at: 2025-03-19T03:44:30Z
author_association: NONE

PR comments:
user: login: codecov[bot]
created_at: 2025-03-13T15:49:06Z
body: ## [Codecov](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism) Report
Attention: Patch coverage is `33.03571%` with `75 lines` in your changes missing coverage. Please review.
> Project coverage is 45.26%. Comparing base [(`6638905`)](https://app.codecov.io/gh/ethereum-optimism/optimism/commit/6638905c0cbe190b6c3fdf99dfc8f67e60cd657c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism) to head [(`c156502`)](https://app.codecov.io/gh/ethereum-optimism/optimism/commit/c156502804b66e140ce9e386f16785db8d2b28ab?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism).
> Report is 2 commits behind head on develop.

| [Files with missing lines](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism) | Patch % | Lines |
|---|---|---|
| [op-service/sources/supervisor\_client.go](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&filepath=op-service%2Fsources%2Fsupervisor_client.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism#diff-b3Atc2VydmljZS9zb3VyY2VzL3N1cGVydmlzb3JfY2xpZW50Lmdv) | 16.90% | [59 Missing :warning: ](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism) |
| [op-supervisor/supervisor/frontend/frontend.go](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&filepath=op-supervisor%2Fsupervisor%2Ffrontend%2Ffrontend.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism#diff-b3Atc3VwZXJ2aXNvci9zdXBlcnZpc29yL2Zyb250ZW5kL2Zyb250ZW5kLmdv) | 82.60% | [4 Missing :warning: ](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism) |
| [op-challenger/runner/metrics.go](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&filepath=op-challenger%2Frunner%2Fmetrics.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism#diff-b3AtY2hhbGxlbmdlci9ydW5uZXIvbWV0cmljcy5nbw==) | 0.00% | [3 Missing :warning: ](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism) |
| [op-dispute-mon/metrics/metrics.go](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&filepath=op-dispute-mon%2Fmetrics%2Fmetrics.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism#diff-b3AtZGlzcHV0ZS1tb24vbWV0cmljcy9tZXRyaWNzLmdv) | 0.00% | [3 Missing :warning: ](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism) |
| [devnet-sdk/system/system.go](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&filepath=devnet-sdk%2Fsystem%2Fsystem.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism#diff-ZGV2bmV0LXNkay9zeXN0ZW0vc3lzdGVtLmdv) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism) |
| [op-challenger/game/fault/clients.go](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&filepath=op-challenger%2Fgame%2Ffault%2Fclients.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism#diff-b3AtY2hhbGxlbmdlci9nYW1lL2ZhdWx0L2NsaWVudHMuZ28=) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism) |
| [op-challenger/runner/runner.go](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&filepath=op-challenger%2Frunner%2Frunner.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism#diff-b3AtY2hhbGxlbmdlci9ydW5uZXIvcnVubmVyLmdv) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism) |
| [op-dispute-mon/mon/service.go](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&filepath=op-dispute-mon%2Fmon%2Fservice.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism#diff-b3AtZGlzcHV0ZS1tb24vbW9uL3NlcnZpY2UuZ28=) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism) |
| [op-node/rollup/interop/config.go](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&filepath=op-node%2Frollup%2Finterop%2Fconfig.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism#diff-b3Atbm9kZS9yb2xsdXAvaW50ZXJvcC9jb25maWcuZ28=) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism) |
| [op-proposer/proposer/service.go](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&filepath=op-proposer%2Fproposer%2Fservice.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism#diff-b3AtcHJvcG9zZXIvcHJvcG9zZXIvc2VydmljZS5nbw==) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           develop   #14841      +/-   ##
===========================================
- Coverage    46.33%   45.26%   -1.07%     
===========================================
  Files         1126     1067      -59     
  Lines        97617    92968    -4649     
===========================================
- Hits         45226    42078    -3148     
+ Misses       49111    47778    -1333     
+ Partials      3280     3112     -168     
```

| [Flag](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism) | Coverage Δ | |
|---|---|---|
| [cannon-go-tests-32](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism) | `?` | |
| [cannon-go-tests-64](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism) | `?` | |
| [contracts-bedrock-tests](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism) | `94.30% <ø> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Files with missing lines](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism) | Coverage Δ | |
|---|---|---|
| [op-dispute-mon/metrics/noop.go](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&filepath=op-dispute-mon%2Fmetrics%2Fnoop.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism#diff-b3AtZGlzcHV0ZS1tb24vbWV0cmljcy9ub29wLmdv) | `5.00% <ø> (ø)` | |
| [op-supervisor/supervisor/service.go](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&filepath=op-supervisor%2Fsupervisor%2Fservice.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism#diff-b3Atc3VwZXJ2aXNvci9zdXBlcnZpc29yL3NlcnZpY2UuZ28=) | `55.74% <100.00%> (+0.77%)` | :arrow_up: |
| [devnet-sdk/system/system.go](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&filepath=devnet-sdk%2Fsystem%2Fsystem.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism#diff-ZGV2bmV0LXNkay9zeXN0ZW0vc3lzdGVtLmdv) | `42.37% <0.00%> (ø)` | |
| [op-challenger/game/fault/clients.go](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&filepath=op-challenger%2Fgame%2Ffault%2Fclients.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism#diff-b3AtY2hhbGxlbmdlci9nYW1lL2ZhdWx0L2NsaWVudHMuZ28=) | `0.00% <0.00%> (ø)` | |
| [op-challenger/runner/runner.go](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&filepath=op-challenger%2Frunner%2Frunner.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism#diff-b3AtY2hhbGxlbmdlci9ydW5uZXIvcnVubmVyLmdv) | `0.00% <0.00%> (ø)` | |
| [op-dispute-mon/mon/service.go](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&filepath=op-dispute-mon%2Fmon%2Fservice.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism#diff-b3AtZGlzcHV0ZS1tb24vbW9uL3NlcnZpY2UuZ28=) | `0.00% <0.00%> (ø)` | |
| [op-node/rollup/interop/config.go](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&filepath=op-node%2Frollup%2Finterop%2Fconfig.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism#diff-b3Atbm9kZS9yb2xsdXAvaW50ZXJvcC9jb25maWcuZ28=) | `38.09% <0.00%> (ø)` | |
| [op-proposer/proposer/service.go](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&filepath=op-proposer%2Fproposer%2Fservice.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism#diff-b3AtcHJvcG9zZXIvcHJvcG9zZXIvc2VydmljZS5nbw==) | `0.00% <0.00%> (ø)` | |
| [op-challenger/runner/metrics.go](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&filepath=op-challenger%2Frunner%2Fmetrics.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism#diff-b3AtY2hhbGxlbmdlci9ydW5uZXIvbWV0cmljcy5nbw==) | `0.00% <0.00%> (ø)` | |
| [op-dispute-mon/metrics/metrics.go](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree&filepath=op-dispute-mon%2Fmetrics%2Fmetrics.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism#diff-b3AtZGlzcHV0ZS1tb24vbWV0cmljcy9tZXRyaWNzLmdv) | `13.35% <0.00%> (ø)` | |
| ... and [2 more](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism) | |

... and [70 files with indirect coverage changes](https://app.codecov.io/gh/ethereum-optimism/optimism/pull/14841/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethereum-optimism)

</details>
<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- 📦 [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: ghost
created_at: 2025-03-13T17:36:38Z
body: Note: Need to add client-side metrics to op-geth
author_association: NONE
user: login: tynes
created_at: 2025-03-15T00:11:34Z
body: Nice! Looks like this needs a rebase
author_association: CONTRIBUTOR