277 lines
8.1 KiB
Go
277 lines
8.1 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, 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 {
|
|
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"
|
|
}
|
|
}
|