Freshness diagnostics API, UI trust notes, mission control/stats updates, and deploy scripts.
Made-with: Cursor
This commit is contained in:
@@ -5,6 +5,8 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/explorer/backend/api/freshness"
|
||||
)
|
||||
|
||||
func relaySnapshotStatus(relay map[string]interface{}) string {
|
||||
@@ -129,6 +131,81 @@ func (s *Server) BuildBridgeStatusData(ctx context.Context) map[string]interface
|
||||
if ov := readOptionalVerifyJSON(); ov != nil {
|
||||
data["operator_verify"] = ov
|
||||
}
|
||||
if s.freshnessLoader != nil {
|
||||
if snapshot, completeness, sampling, err := s.freshnessLoader(ctx); err == nil && snapshot != nil {
|
||||
subsystems := map[string]interface{}{
|
||||
"rpc_head": map[string]interface{}{
|
||||
"status": chainStatusFromProbe(p138),
|
||||
"updated_at": valueOrNil(snapshot.ChainHead.Timestamp),
|
||||
"age_seconds": valueOrNil(snapshot.ChainHead.AgeSeconds),
|
||||
"source": snapshot.ChainHead.Source,
|
||||
"confidence": snapshot.ChainHead.Confidence,
|
||||
"provenance": snapshot.ChainHead.Provenance,
|
||||
"completeness": snapshot.ChainHead.Completeness,
|
||||
},
|
||||
"tx_index": map[string]interface{}{
|
||||
"status": completenessStatus(completeness.TransactionsFeed),
|
||||
"updated_at": valueOrNil(snapshot.LatestIndexedTransaction.Timestamp),
|
||||
"age_seconds": valueOrNil(snapshot.LatestIndexedTransaction.AgeSeconds),
|
||||
"source": snapshot.LatestIndexedTransaction.Source,
|
||||
"confidence": snapshot.LatestIndexedTransaction.Confidence,
|
||||
"provenance": snapshot.LatestIndexedTransaction.Provenance,
|
||||
"completeness": completeness.TransactionsFeed,
|
||||
},
|
||||
"stats_summary": map[string]interface{}{
|
||||
"status": completenessStatus(completeness.BlocksFeed),
|
||||
"updated_at": valueOrNil(sampling.StatsGeneratedAt),
|
||||
"age_seconds": ageSinceRFC3339(sampling.StatsGeneratedAt),
|
||||
"source": freshness.SourceReported,
|
||||
"confidence": freshness.ConfidenceMedium,
|
||||
"provenance": freshness.ProvenanceComposite,
|
||||
"completeness": completeness.BlocksFeed,
|
||||
},
|
||||
}
|
||||
if len(sampling.Issues) > 0 {
|
||||
subsystems["freshness_queries"] = map[string]interface{}{
|
||||
"status": "degraded",
|
||||
"updated_at": valueOrNil(sampling.StatsGeneratedAt),
|
||||
"age_seconds": ageSinceRFC3339(sampling.StatsGeneratedAt),
|
||||
"source": freshness.SourceDerived,
|
||||
"confidence": freshness.ConfidenceMedium,
|
||||
"provenance": freshness.ProvenanceComposite,
|
||||
"completeness": freshness.CompletenessPartial,
|
||||
"issues": sampling.Issues,
|
||||
}
|
||||
}
|
||||
modeKind := "live"
|
||||
modeReason := any(nil)
|
||||
modeScope := any(nil)
|
||||
if relays, ok := data["ccip_relays"].(map[string]interface{}); ok && len(relays) > 0 {
|
||||
modeKind = "snapshot"
|
||||
modeReason = "live_homepage_stream_not_attached"
|
||||
modeScope = "relay_monitoring_homepage_card_only"
|
||||
subsystems["bridge_relay_monitoring"] = map[string]interface{}{
|
||||
"status": overall,
|
||||
"updated_at": now,
|
||||
"age_seconds": int64(0),
|
||||
"source": freshness.SourceReported,
|
||||
"confidence": freshness.ConfidenceHigh,
|
||||
"provenance": freshness.ProvenanceMissionFeed,
|
||||
"completeness": freshness.CompletenessComplete,
|
||||
}
|
||||
}
|
||||
data["freshness"] = snapshot
|
||||
data["subsystems"] = subsystems
|
||||
data["sampling"] = sampling
|
||||
data["mode"] = map[string]interface{}{
|
||||
"kind": modeKind,
|
||||
"updated_at": now,
|
||||
"age_seconds": int64(0),
|
||||
"reason": modeReason,
|
||||
"scope": modeScope,
|
||||
"source": freshness.SourceReported,
|
||||
"confidence": freshness.ConfidenceHigh,
|
||||
"provenance": freshness.ProvenanceMissionFeed,
|
||||
}
|
||||
}
|
||||
}
|
||||
if relays := FetchCCIPRelayHealths(ctx); relays != nil {
|
||||
data["ccip_relays"] = relays
|
||||
if ccip := primaryRelayHealth(relays); ccip != nil {
|
||||
@@ -142,5 +219,58 @@ func (s *Server) BuildBridgeStatusData(ctx context.Context) map[string]interface
|
||||
}
|
||||
}
|
||||
}
|
||||
if mode, ok := data["mode"].(map[string]interface{}); ok {
|
||||
if relays, ok := data["ccip_relays"].(map[string]interface{}); ok && len(relays) > 0 {
|
||||
mode["kind"] = "snapshot"
|
||||
mode["reason"] = "live_homepage_stream_not_attached"
|
||||
mode["scope"] = "relay_monitoring_homepage_card_only"
|
||||
if subsystems, ok := data["subsystems"].(map[string]interface{}); ok {
|
||||
subsystems["bridge_relay_monitoring"] = map[string]interface{}{
|
||||
"status": data["status"],
|
||||
"updated_at": now,
|
||||
"age_seconds": int64(0),
|
||||
"source": freshness.SourceReported,
|
||||
"confidence": freshness.ConfidenceHigh,
|
||||
"provenance": freshness.ProvenanceMissionFeed,
|
||||
"completeness": freshness.CompletenessComplete,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func valueOrNil[T any](value *T) any {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func ageSinceRFC3339(value *string) any {
|
||||
if value == nil || *value == "" {
|
||||
return nil
|
||||
}
|
||||
parsed, err := time.Parse(time.RFC3339, *value)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
age := int64(time.Since(parsed).Seconds())
|
||||
if age < 0 {
|
||||
age = 0
|
||||
}
|
||||
return age
|
||||
}
|
||||
|
||||
func completenessStatus(value freshness.Completeness) string {
|
||||
switch value {
|
||||
case freshness.CompletenessComplete:
|
||||
return "operational"
|
||||
case freshness.CompletenessPartial:
|
||||
return "partial"
|
||||
case freshness.CompletenessStale:
|
||||
return "stale"
|
||||
default:
|
||||
return "unavailable"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/explorer/backend/api/freshness"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -145,7 +146,50 @@ func TestBuildBridgeStatusDataIncludesCCIPRelay(t *testing.T) {
|
||||
t.Setenv("CCIP_RELAY_HEALTH_URLS", "")
|
||||
t.Setenv("MISSION_CONTROL_CCIP_JSON", "")
|
||||
|
||||
s := &Server{}
|
||||
s := &Server{
|
||||
freshnessLoader: func(context.Context) (*freshness.Snapshot, *freshness.SummaryCompleteness, *freshness.Sampling, error) {
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
head := int64(16)
|
||||
txBlock := int64(12)
|
||||
distance := int64(4)
|
||||
return &freshness.Snapshot{
|
||||
ChainHead: freshness.Reference{
|
||||
BlockNumber: &head,
|
||||
Timestamp: &now,
|
||||
AgeSeconds: func() *int64 { v := int64(1); return &v }(),
|
||||
Source: freshness.SourceReported,
|
||||
Confidence: freshness.ConfidenceHigh,
|
||||
Provenance: freshness.ProvenanceRPC,
|
||||
Completeness: freshness.CompletenessComplete,
|
||||
},
|
||||
LatestIndexedTransaction: freshness.Reference{
|
||||
BlockNumber: &txBlock,
|
||||
Timestamp: &now,
|
||||
AgeSeconds: func() *int64 { v := int64(120); return &v }(),
|
||||
Source: freshness.SourceReported,
|
||||
Confidence: freshness.ConfidenceHigh,
|
||||
Provenance: freshness.ProvenanceTxIndex,
|
||||
Completeness: freshness.CompletenessPartial,
|
||||
},
|
||||
LatestNonEmptyBlock: freshness.Reference{
|
||||
BlockNumber: &txBlock,
|
||||
Timestamp: &now,
|
||||
AgeSeconds: func() *int64 { v := int64(120); return &v }(),
|
||||
DistanceFromHead: &distance,
|
||||
Source: freshness.SourceReported,
|
||||
Confidence: freshness.ConfidenceHigh,
|
||||
Provenance: freshness.ProvenanceTxIndex,
|
||||
Completeness: freshness.CompletenessPartial,
|
||||
},
|
||||
},
|
||||
&freshness.SummaryCompleteness{
|
||||
TransactionsFeed: freshness.CompletenessPartial,
|
||||
BlocksFeed: freshness.CompletenessComplete,
|
||||
},
|
||||
&freshness.Sampling{StatsGeneratedAt: &now},
|
||||
nil
|
||||
},
|
||||
}
|
||||
got := s.BuildBridgeStatusData(context.Background())
|
||||
ccip, ok := got["ccip_relay"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
@@ -156,6 +200,9 @@ func TestBuildBridgeStatusDataIncludesCCIPRelay(t *testing.T) {
|
||||
probe, ok := ccip["url_probe"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
require.Equal(t, true, probe["ok"])
|
||||
require.Contains(t, got, "freshness")
|
||||
require.Contains(t, got, "subsystems")
|
||||
require.Contains(t, got, "mode")
|
||||
}
|
||||
|
||||
func TestBuildBridgeStatusDataDegradesWhenNamedRelayFails(t *testing.T) {
|
||||
@@ -197,7 +244,11 @@ func TestBuildBridgeStatusDataDegradesWhenNamedRelayFails(t *testing.T) {
|
||||
t.Setenv("MISSION_CONTROL_CCIP_JSON", "")
|
||||
t.Setenv("CCIP_RELAY_HEALTH_URLS", "mainnet="+mainnet.URL+"/healthz,bsc="+bad.URL+"/healthz")
|
||||
|
||||
s := &Server{}
|
||||
s := &Server{
|
||||
freshnessLoader: func(context.Context) (*freshness.Snapshot, *freshness.SummaryCompleteness, *freshness.Sampling, error) {
|
||||
return nil, nil, nil, nil
|
||||
},
|
||||
}
|
||||
got := s.BuildBridgeStatusData(context.Background())
|
||||
require.Equal(t, "degraded", got["status"])
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/explorer/backend/api/freshness"
|
||||
"github.com/explorer/backend/libs/go-rpc-gateway"
|
||||
)
|
||||
|
||||
@@ -19,13 +20,18 @@ var track1HashPattern = regexp.MustCompile(`^0x[a-fA-F0-9]{64}$`)
|
||||
|
||||
// Server handles Track 1 endpoints (uses RPC gateway from lib)
|
||||
type Server struct {
|
||||
rpcGateway *gateway.RPCGateway
|
||||
rpcGateway *gateway.RPCGateway
|
||||
freshnessLoader func(ctx context.Context) (*freshness.Snapshot, *freshness.SummaryCompleteness, *freshness.Sampling, error)
|
||||
}
|
||||
|
||||
// NewServer creates a new Track 1 server
|
||||
func NewServer(rpcGateway *gateway.RPCGateway) *Server {
|
||||
func NewServer(
|
||||
rpcGateway *gateway.RPCGateway,
|
||||
freshnessLoader func(ctx context.Context) (*freshness.Snapshot, *freshness.SummaryCompleteness, *freshness.Sampling, error),
|
||||
) *Server {
|
||||
return &Server{
|
||||
rpcGateway: rpcGateway,
|
||||
rpcGateway: rpcGateway,
|
||||
freshnessLoader: freshnessLoader,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user