- Introduced a new Diagnostics struct to capture transaction visibility state and activity state. - Updated BuildSnapshot function to return diagnostics alongside snapshot, completeness, and sampling. - Enhanced test cases to validate the new diagnostics data. - Updated frontend components to utilize the new diagnostics information for improved user feedback on freshness context. This change improves the observability of transaction activity and enhances the user experience by providing clearer insights into the freshness of data.
280 lines
8.2 KiB
Go
280 lines
8.2 KiB
Go
package track1
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/explorer/backend/api/freshness"
|
|
)
|
|
|
|
func relaySnapshotStatus(relay map[string]interface{}) string {
|
|
if relay == nil {
|
|
return ""
|
|
}
|
|
if probe, ok := relay["url_probe"].(map[string]interface{}); ok {
|
|
if okValue, exists := probe["ok"].(bool); exists && !okValue {
|
|
return "down"
|
|
}
|
|
if body, ok := probe["body"].(map[string]interface{}); ok {
|
|
if status, ok := body["status"].(string); ok {
|
|
return strings.ToLower(strings.TrimSpace(status))
|
|
}
|
|
}
|
|
}
|
|
if _, ok := relay["file_snapshot_error"].(string); ok {
|
|
return "down"
|
|
}
|
|
if snapshot, ok := relay["file_snapshot"].(map[string]interface{}); ok {
|
|
if status, ok := snapshot["status"].(string); ok {
|
|
return strings.ToLower(strings.TrimSpace(status))
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func relayNeedsAttention(relay map[string]interface{}) bool {
|
|
status := relaySnapshotStatus(relay)
|
|
switch status {
|
|
case "degraded", "stale", "stopped", "down":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// BuildBridgeStatusData builds the inner `data` object for bridge/status and SSE payloads.
|
|
func (s *Server) BuildBridgeStatusData(ctx context.Context) map[string]interface{} {
|
|
rpc138 := strings.TrimSpace(os.Getenv("RPC_URL"))
|
|
if rpc138 == "" {
|
|
rpc138 = "http://localhost:8545"
|
|
}
|
|
|
|
var probes []RPCProbeResult
|
|
p138 := ProbeEVMJSONRPC(ctx, "chain-138", "138", rpc138)
|
|
probes = append(probes, p138)
|
|
|
|
if eth := strings.TrimSpace(os.Getenv("ETH_MAINNET_RPC_URL")); eth != "" {
|
|
probes = append(probes, ProbeEVMJSONRPC(ctx, "ethereum-mainnet", "1", eth))
|
|
}
|
|
|
|
for _, row := range ParseExtraRPCProbes() {
|
|
name, u, ck := row[0], row[1], row[2]
|
|
probes = append(probes, ProbeEVMJSONRPC(ctx, name, ck, u))
|
|
}
|
|
|
|
overall := "operational"
|
|
if !p138.OK {
|
|
overall = "degraded"
|
|
} else {
|
|
for _, p := range probes {
|
|
if !p.OK {
|
|
overall = "degraded"
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
now := time.Now().UTC().Format(time.RFC3339)
|
|
chains := map[string]interface{}{
|
|
"138": map[string]interface{}{
|
|
"name": "Defi Oracle Meta Mainnet",
|
|
"status": chainStatusFromProbe(p138),
|
|
"last_sync": now,
|
|
"latency_ms": p138.LatencyMs,
|
|
"head_age_sec": p138.HeadAgeSeconds,
|
|
"block_number": p138.BlockNumberDec,
|
|
"endpoint": p138.Endpoint,
|
|
"probe_error": p138.Error,
|
|
},
|
|
}
|
|
|
|
for _, p := range probes {
|
|
if p.ChainKey != "1" && p.Name != "ethereum-mainnet" {
|
|
continue
|
|
}
|
|
chains["1"] = map[string]interface{}{
|
|
"name": "Ethereum Mainnet",
|
|
"status": chainStatusFromProbe(p),
|
|
"last_sync": now,
|
|
"latency_ms": p.LatencyMs,
|
|
"head_age_sec": p.HeadAgeSeconds,
|
|
"block_number": p.BlockNumberDec,
|
|
"endpoint": p.Endpoint,
|
|
"probe_error": p.Error,
|
|
}
|
|
break
|
|
}
|
|
|
|
probeJSON := make([]map[string]interface{}, 0, len(probes))
|
|
for _, p := range probes {
|
|
probeJSON = append(probeJSON, map[string]interface{}{
|
|
"name": p.Name,
|
|
"chainKey": p.ChainKey,
|
|
"endpoint": p.Endpoint,
|
|
"ok": p.OK,
|
|
"latencyMs": p.LatencyMs,
|
|
"blockNumber": p.BlockNumber,
|
|
"blockNumberDec": p.BlockNumberDec,
|
|
"headAgeSeconds": p.HeadAgeSeconds,
|
|
"error": p.Error,
|
|
})
|
|
}
|
|
|
|
data := map[string]interface{}{
|
|
"status": overall,
|
|
"chains": chains,
|
|
"rpc_probe": probeJSON,
|
|
"checked_at": now,
|
|
}
|
|
if ov := readOptionalVerifyJSON(); ov != nil {
|
|
data["operator_verify"] = ov
|
|
}
|
|
if s.freshnessLoader != nil {
|
|
if snapshot, completeness, sampling, diagnostics, 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
|
|
if diagnostics != nil {
|
|
data["diagnostics"] = diagnostics
|
|
}
|
|
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 {
|
|
data["ccip_relay"] = ccip
|
|
}
|
|
for _, value := range relays {
|
|
relay, ok := value.(map[string]interface{})
|
|
if ok && relayNeedsAttention(relay) {
|
|
data["status"] = "degraded"
|
|
break
|
|
}
|
|
}
|
|
}
|
|
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"
|
|
}
|
|
}
|