From bb58b61b8e0206db5c747907ed3d0e35a9bc1bf6 Mon Sep 17 00:00:00 2001 From: twmoonboy <108098442+Mrchinedum@users.noreply.github.com> Date: Sun, 26 Apr 2026 16:36:27 +0000 Subject: [PATCH 1/2] feat: add comprehensive contract sustainability metrics - Add SustainabilityMetrics type with KPIs: invocations, storage writes, events emitted, rewards distributed, content minted, active users, and efficiency score - Add SUSTAINABILITY_METRICS storage key - Add SustainabilityMetricsUpdatedEvent - Add SustainabilityManager with record, query, and health score logic - Expose 4 public contract entry points in lib.rs - Include unit tests for core metric tracking and health scoring --- contracts/teachlink/src/events.rs | 11 ++ contracts/teachlink/src/lib.rs | 35 ++++- contracts/teachlink/src/storage.rs | 3 + contracts/teachlink/src/sustainability.rs | 180 ++++++++++++++++++++++ contracts/teachlink/src/types.rs | 25 +++ 5 files changed, 251 insertions(+), 3 deletions(-) create mode 100644 contracts/teachlink/src/sustainability.rs diff --git a/contracts/teachlink/src/events.rs b/contracts/teachlink/src/events.rs index 3ad0148b..6ea1689b 100644 --- a/contracts/teachlink/src/events.rs +++ b/contracts/teachlink/src/events.rs @@ -712,3 +712,14 @@ pub struct ChainMetricsUpdatedEvent { pub average_fee: i128, pub updated_at: u64, } + +/// Emitted when sustainability metrics are updated. +#[contractevent] +#[derive(Clone, Debug)] +pub struct SustainabilityMetricsUpdatedEvent { + pub total_invocations: u64, + pub total_storage_writes: u64, + pub total_events_emitted: u64, + pub efficiency_score: u32, + pub updated_at: u64, +} diff --git a/contracts/teachlink/src/lib.rs b/contracts/teachlink/src/lib.rs index 52073185..66c3dcf1 100644 --- a/contracts/teachlink/src/lib.rs +++ b/contracts/teachlink/src/lib.rs @@ -156,6 +156,7 @@ mod slashing; // TODO: Fix social_learning module compilation errors (pre-existing issue) // mod social_learning; mod storage; +mod sustainability; mod tokenization; mod types; mod upgrade; @@ -187,9 +188,9 @@ pub use types::{ NotificationPreference, NotificationSchedule, NotificationTemplate, NotificationTracking, OperationType, PacketStatus, ProposalStatus, ProvenanceRecord, RecoveryRecord, ReportComment, ReportSchedule, ReportSnapshot, ReportTemplate, ReportType, ReportUsage, RewardRate, - RewardType, RtoTier, SlashingReason, SlashingRecord, SwapStatus, TransferType, - UserNotificationSettings, UserReputation, UserReward, ValidatorInfo, ValidatorReward, - ValidatorSignature, VisualizationDataPoint, + RewardType, RtoTier, SlashingReason, SlashingRecord, SustainabilityMetrics, SwapStatus, + TransferType, UserNotificationSettings, UserReputation, UserReward, ValidatorInfo, + ValidatorReward, ValidatorSignature, VisualizationDataPoint, }; /// TeachLink main contract. @@ -1783,4 +1784,32 @@ impl TeachLinkBridge { pub fn is_fallback_active(env: Env) -> bool { network_recovery::NetworkRecovery::is_fallback_active(&env) } + + // ========== Sustainability Metrics Functions ========== + + /// Get current sustainability metrics snapshot. + pub fn get_sustainability_metrics(env: Env) -> SustainabilityMetrics { + sustainability::SustainabilityManager::get_metrics(&env) + } + + /// Compute composite sustainability health score (0-100). + pub fn get_sustainability_health_score(env: Env) -> u32 { + sustainability::SustainabilityManager::health_score(&env) + } + + /// Update the efficiency score from a caller-supplied ops window. + /// `successful_ops` / `total_ops` determines the basis-point score. + pub fn update_sustainability_efficiency( + env: Env, + successful_ops: u64, + total_ops: u64, + ) { + sustainability::SustainabilityManager::update_efficiency(&env, successful_ops, total_ops) + } + + /// Record a contract invocation for resource-usage tracking. + /// Set `storage_write` to true when the invocation writes to storage. + pub fn record_sustainability_invocation(env: Env, storage_write: bool) { + sustainability::SustainabilityManager::record_invocation(&env, storage_write) + } } diff --git a/contracts/teachlink/src/storage.rs b/contracts/teachlink/src/storage.rs index 78c88d05..f9c098c0 100644 --- a/contracts/teachlink/src/storage.rs +++ b/contracts/teachlink/src/storage.rs @@ -209,6 +209,9 @@ pub const USER_FEEDBACK: Symbol = symbol_short!("feedback"); pub const UX_EXPERIMENTS: Symbol = symbol_short!("ux_exp"); pub const COMPONENT_CONFIG: Symbol = symbol_short!("comp_cfg"); +// Sustainability Metrics Storage +pub const SUSTAINABILITY_METRICS: Symbol = symbol_short!("sust_met"); + // Reentrancy guard locks pub const BRIDGE_GUARD: Symbol = symbol_short!("br_guard"); pub const REWARDS_GUARD: Symbol = symbol_short!("rw_guard"); diff --git a/contracts/teachlink/src/sustainability.rs b/contracts/teachlink/src/sustainability.rs new file mode 100644 index 00000000..baaf53c2 --- /dev/null +++ b/contracts/teachlink/src/sustainability.rs @@ -0,0 +1,180 @@ +#![no_std] + +//! Sustainability metrics for the TeachLink contract. +//! +//! Tracks resource usage, efficiency, and platform health KPIs. + +use crate::events::SustainabilityMetricsUpdatedEvent; +use crate::storage::SUSTAINABILITY_METRICS; +use crate::types::SustainabilityMetrics; +use soroban_sdk::Env; + +pub struct SustainabilityManager; + +impl SustainabilityManager { + fn load(env: &Env) -> SustainabilityMetrics { + env.storage() + .instance() + .get(&SUSTAINABILITY_METRICS) + .unwrap_or(SustainabilityMetrics { + total_invocations: 0, + total_storage_writes: 0, + total_events_emitted: 0, + total_rewards_distributed: 0, + total_content_minted: 0, + total_active_users: 0, + efficiency_score: 10000, // 100% initially + last_updated: env.ledger().timestamp(), + }) + } + + fn save(env: &Env, metrics: &SustainabilityMetrics) { + env.storage() + .instance() + .set(&SUSTAINABILITY_METRICS, metrics); + } + + fn publish(env: &Env, metrics: &SustainabilityMetrics) { + SustainabilityMetricsUpdatedEvent { + total_invocations: metrics.total_invocations, + total_storage_writes: metrics.total_storage_writes, + total_events_emitted: metrics.total_events_emitted, + efficiency_score: metrics.efficiency_score, + updated_at: metrics.last_updated, + } + .publish(env); + } + + /// Record a contract invocation and optional storage write. + pub fn record_invocation(env: &Env, storage_write: bool) { + let mut m = Self::load(env); + m.total_invocations += 1; + if storage_write { + m.total_storage_writes += 1; + } + m.last_updated = env.ledger().timestamp(); + Self::save(env, &m); + Self::publish(env, &m); + } + + /// Record an emitted event. + pub fn record_event(env: &Env) { + let mut m = Self::load(env); + m.total_events_emitted += 1; + m.last_updated = env.ledger().timestamp(); + Self::save(env, &m); + } + + /// Record rewards distributed. + pub fn record_rewards(env: &Env, amount: i128) { + let mut m = Self::load(env); + m.total_rewards_distributed += amount; + m.last_updated = env.ledger().timestamp(); + Self::save(env, &m); + } + + /// Record a content token minted. + pub fn record_content_minted(env: &Env) { + let mut m = Self::load(env); + m.total_content_minted += 1; + m.last_updated = env.ledger().timestamp(); + Self::save(env, &m); + } + + /// Record a new active user. + pub fn record_active_user(env: &Env) { + let mut m = Self::load(env); + m.total_active_users += 1; + m.last_updated = env.ledger().timestamp(); + Self::save(env, &m); + } + + /// Update the efficiency score (basis points, 0-10000). + /// `successful_ops` and `total_ops` are the caller-supplied window counts. + pub fn update_efficiency(env: &Env, successful_ops: u64, total_ops: u64) { + let mut m = Self::load(env); + m.efficiency_score = if total_ops == 0 { + 10000 + } else { + ((successful_ops * 10000) / total_ops) as u32 + }; + m.last_updated = env.ledger().timestamp(); + Self::save(env, &m); + Self::publish(env, &m); + } + + /// Return the current sustainability metrics snapshot. + pub fn get_metrics(env: &Env) -> SustainabilityMetrics { + Self::load(env) + } + + /// Compute a composite sustainability health score (0-100). + /// + /// Weights: + /// - Efficiency score: 50% + /// - Content creation activity: 25% (capped at 1000 tokens = full score) + /// - User adoption: 25% (capped at 1000 users = full score) + pub fn health_score(env: &Env) -> u32 { + let m = Self::load(env); + + let efficiency_component = m.efficiency_score / 100; // 0-100 + + let content_component = if m.total_content_minted >= 1000 { + 100u32 + } else { + (m.total_content_minted as u32 * 100) / 1000 + }; + + let user_component = if m.total_active_users >= 1000 { + 100u32 + } else { + (m.total_active_users as u32 * 100) / 1000 + }; + + (efficiency_component * 50 + content_component * 25 + user_component * 25) / 100 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use soroban_sdk::Env; + + #[test] + fn test_record_invocation() { + let env = Env::default(); + SustainabilityManager::record_invocation(&env, true); + let m = SustainabilityManager::get_metrics(&env); + assert_eq!(m.total_invocations, 1); + assert_eq!(m.total_storage_writes, 1); + } + + #[test] + fn test_update_efficiency() { + let env = Env::default(); + SustainabilityManager::update_efficiency(&env, 90, 100); + let m = SustainabilityManager::get_metrics(&env); + assert_eq!(m.efficiency_score, 9000); + } + + #[test] + fn test_health_score_full() { + let env = Env::default(); + // Set efficiency to 100%, content and users to max + SustainabilityManager::update_efficiency(&env, 1, 1); + let mut m = SustainabilityManager::get_metrics(&env); + m.total_content_minted = 1000; + m.total_active_users = 1000; + env.storage().instance().set(&SUSTAINABILITY_METRICS, &m); + assert_eq!(SustainabilityManager::health_score(&env), 100); + } + + #[test] + fn test_health_score_zero_ops() { + let env = Env::default(); + // No ops yet — efficiency defaults to 10000 (100%) + let score = SustainabilityManager::health_score(&env); + // efficiency=100, content=0, users=0 → (100*50 + 0 + 0)/100 = 50 + assert_eq!(score, 50); + } +} diff --git a/contracts/teachlink/src/types.rs b/contracts/teachlink/src/types.rs index ca7485cf..c595e9aa 100644 --- a/contracts/teachlink/src/types.rs +++ b/contracts/teachlink/src/types.rs @@ -1638,3 +1638,28 @@ pub struct MobileSocialFeatures { pub study_buddies: Vec
, pub mentor_quick_connect: bool, } + +// ========== Sustainability Metrics Types ========== + +/// Tracks contract-level sustainability KPIs: resource usage, efficiency, and platform health. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SustainabilityMetrics { + /// Total number of contract invocations (resource usage proxy) + pub total_invocations: u64, + /// Total storage entries written (resource usage) + pub total_storage_writes: u64, + /// Total events emitted (activity indicator) + pub total_events_emitted: u64, + /// Total rewards distributed (platform value flow) + pub total_rewards_distributed: i128, + /// Total content tokens minted (content creation KPI) + pub total_content_minted: u64, + /// Total active users (unique addresses that have interacted) + pub total_active_users: u64, + /// Efficiency score in basis points (0-10000 = 0%-100%) + /// Computed as: successful_ops / total_ops * 10000 + pub efficiency_score: u32, + /// Timestamp of last metrics update + pub last_updated: u64, +} From 6b60f23018fd6119e6eb82c0b6ef1ea013ca3042 Mon Sep 17 00:00:00 2001 From: twmoonboy <108098442+Mrchinedum@users.noreply.github.com> Date: Sun, 26 Apr 2026 16:48:03 +0000 Subject: [PATCH 2/2] feat: implement comprehensive monitoring dashboard - Add 8 sustainability Prometheus gauges to MetricsService (invocations, storage writes, events emitted, rewards distributed, content minted, active users, efficiency score, health score) - Add updateSustainabilityMetrics() to push gauges on each query - Add getSustainabilitySnapshot() to DashboardService: computes real-time KPIs (efficiency, health, dispute rate, reward claim rate) and pushes them to Prometheus - Add GET /analytics/sustainability endpoint in ReportingController - Add teachlink-sustainability alert group to prometheus/alerts.yml with 5 rules: low efficiency, critical efficiency, low health score, high error rate, no new transactions - Create teachlink-monitoring-dashboard.json (691 lines, 20 panels): - Row 1: Real-Time Platform Health (6 stat panels) - Row 2: Historical Trends (4 time-series panels) - Row 3: Alert Management (firing alerts, active count, critical count) - Row 4: Platform Insights (cache ratio, latency percentiles, dependency health, indexer progress, HTTP status breakdown) --- .../teachlink-monitoring-dashboard.json | 691 ++++++++++++++++++ indexer/observability/prometheus/alerts.yml | 59 ++ indexer/src/performance/metrics.service.ts | 71 ++ indexer/src/reporting/dashboard.service.ts | 61 ++ indexer/src/reporting/reporting.controller.ts | 6 + 5 files changed, 888 insertions(+) create mode 100644 indexer/observability/grafana/dashboards/teachlink-monitoring-dashboard.json diff --git a/indexer/observability/grafana/dashboards/teachlink-monitoring-dashboard.json b/indexer/observability/grafana/dashboards/teachlink-monitoring-dashboard.json new file mode 100644 index 00000000..be4b1c88 --- /dev/null +++ b/indexer/observability/grafana/dashboards/teachlink-monitoring-dashboard.json @@ -0,0 +1,691 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { "type": "grafana", "uid": "-- Grafana --" }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "Comprehensive TeachLink monitoring: real-time metrics, historical trends, alert management, and platform insights", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [ + { + "title": "Service Overview", + "type": "link", + "url": "/d/teachlink-service-overview", + "icon": "external link" + } + ], + "panels": [ + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, + "id": 100, + "title": "Real-Time Platform Health", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "red", "value": null }, + { "color": "yellow", "value": 60 }, + { "color": "green", "value": 80 } + ] + }, + "unit": "short", + "min": 0, + "max": 100 + }, + "overrides": [] + }, + "gridPos": { "h": 6, "w": 4, "x": 0, "y": 1 }, + "id": 1, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "value_and_name" + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "expr": "teachlink_contract_sustainability_health_score", + "legendFormat": "Health Score", + "refId": "A" + } + ], + "title": "Sustainability Health Score", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "red", "value": null }, + { "color": "yellow", "value": 7000 }, + { "color": "green", "value": 9000 } + ] + }, + "unit": "short", + "min": 0, + "max": 10000 + }, + "overrides": [] + }, + "gridPos": { "h": 6, "w": 4, "x": 4, "y": 1 }, + "id": 2, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "value_and_name" + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "expr": "teachlink_contract_sustainability_efficiency_score", + "legendFormat": "Efficiency (bps)", + "refId": "A" + } + ], + "title": "Contract Efficiency Score", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "red", "value": null }, + { "color": "yellow", "value": 1 }, + { "color": "green", "value": 2 } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 6, "w": 4, "x": 8, "y": 1 }, + "id": 3, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "value_and_name" + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "expr": "up{job=\"teachlink-indexer\"} + probe_success{job=\"teachlink-indexer-health\"}", + "legendFormat": "Availability", + "refId": "A" + } + ], + "title": "Indexer Availability", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 300 }, + { "color": "red", "value": 900 } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { "h": 6, "w": 4, "x": 12, "y": 1 }, + "id": 4, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "value_and_name" + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "expr": "teachlink_indexer_ledger_lag_seconds", + "legendFormat": "Ledger Lag", + "refId": "A" + } + ], + "title": "Ledger Lag", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 0.05 }, + { "color": "red", "value": 0.10 } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { "h": 6, "w": 4, "x": 16, "y": 1 }, + "id": 5, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "value_and_name" + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "expr": "sum(rate(teachlink_indexer_http_requests_total{status_code=~\"5..\"}[5m])) / clamp_min(sum(rate(teachlink_indexer_http_requests_total[5m])), 1)", + "legendFormat": "Error Rate", + "refId": "A" + } + ], + "title": "HTTP Error Rate", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 0.5 }, + { "color": "red", "value": 1.0 } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { "h": 6, "w": 4, "x": 20, "y": 1 }, + "id": 6, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "value_and_name" + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "expr": "rate(teachlink_indexer_http_request_duration_seconds_sum[5m]) / clamp_min(rate(teachlink_indexer_http_request_duration_seconds_count[5m]), 1)", + "legendFormat": "Avg Latency", + "refId": "A" + } + ], + "title": "API Avg Latency", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 7 }, + "id": 101, + "title": "Historical Trends", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 }, + "id": 10, + "options": { + "legend": { "displayMode": "list", "placement": "bottom" }, + "tooltip": { "mode": "multi" } + }, + "targets": [ + { + "expr": "teachlink_contract_sustainability_health_score", + "legendFormat": "Health Score", + "refId": "A" + }, + { + "expr": "teachlink_contract_sustainability_efficiency_score / 100", + "legendFormat": "Efficiency %", + "refId": "B" + } + ], + "title": "Sustainability Scores Over Time", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 }, + "id": 11, + "options": { + "legend": { "displayMode": "list", "placement": "bottom" }, + "tooltip": { "mode": "multi" } + }, + "targets": [ + { + "expr": "teachlink_contract_sustainability_invocations_total", + "legendFormat": "Invocations", + "refId": "A" + }, + { + "expr": "teachlink_contract_sustainability_storage_writes_total", + "legendFormat": "Storage Writes", + "refId": "B" + }, + { + "expr": "teachlink_contract_sustainability_events_emitted_total", + "legendFormat": "Events Emitted", + "refId": "C" + } + ], + "title": "Contract Resource Usage Over Time", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 }, + "id": 12, + "options": { + "legend": { "displayMode": "list", "placement": "bottom" }, + "tooltip": { "mode": "multi" } + }, + "targets": [ + { + "expr": "rate(teachlink_indexer_http_requests_total[5m])", + "legendFormat": "{{method}} {{route}} {{status_code}}", + "refId": "A" + } + ], + "title": "HTTP Throughput (req/s)", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 }, + "id": 13, + "options": { + "legend": { "displayMode": "list", "placement": "bottom" }, + "tooltip": { "mode": "multi" } + }, + "targets": [ + { + "expr": "teachlink_contract_sustainability_content_minted_total", + "legendFormat": "Content Minted", + "refId": "A" + }, + { + "expr": "teachlink_contract_sustainability_active_users_total", + "legendFormat": "Active Users", + "refId": "B" + }, + { + "expr": "teachlink_contract_sustainability_rewards_distributed_total / 1e7", + "legendFormat": "Rewards Distributed (XLM)", + "refId": "C" + } + ], + "title": "Platform Growth Over Time", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 24 }, + "id": 102, + "title": "Alert Management", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" } + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 25 }, + "id": 20, + "options": { + "legend": { "displayMode": "table", "placement": "bottom" }, + "tooltip": { "mode": "single" } + }, + "targets": [ + { + "expr": "ALERTS{alertstate=\"firing\"}", + "legendFormat": "{{alertname}} ({{severity}})", + "refId": "A" + } + ], + "title": "Firing Alerts", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 1 }, + { "color": "red", "value": 3 } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 6, "x": 12, "y": 25 }, + "id": 21, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "value_and_name" + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "expr": "count(ALERTS{alertstate=\"firing\"}) or vector(0)", + "legendFormat": "Firing", + "refId": "A" + } + ], + "title": "Active Alert Count", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 1 }, + { "color": "red", "value": 2 } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 6, "x": 18, "y": 25 }, + "id": 22, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "value_and_name" + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "expr": "count(ALERTS{alertstate=\"firing\", severity=\"critical\"}) or vector(0)", + "legendFormat": "Critical", + "refId": "A" + } + ], + "title": "Critical Alerts", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 33 }, + "id": 103, + "title": "Platform Insights", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 8, "x": 0, "y": 34 }, + "id": 30, + "options": { + "legend": { "displayMode": "list", "placement": "bottom" }, + "tooltip": { "mode": "multi" } + }, + "targets": [ + { + "expr": "sum(rate(teachlink_indexer_dashboard_cache_requests_total{result=\"hit\"}[5m])) / clamp_min(sum(rate(teachlink_indexer_dashboard_cache_requests_total[5m])), 1)", + "legendFormat": "Cache Hit Ratio", + "refId": "A" + } + ], + "title": "Dashboard Cache Hit Ratio", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 8, "x": 8, "y": 34 }, + "id": 31, + "options": { + "legend": { "displayMode": "list", "placement": "bottom" }, + "tooltip": { "mode": "multi" } + }, + "targets": [ + { + "expr": "rate(teachlink_indexer_http_request_duration_seconds_sum[5m]) / clamp_min(rate(teachlink_indexer_http_request_duration_seconds_count[5m]), 1)", + "legendFormat": "Avg Latency", + "refId": "A" + }, + { + "expr": "histogram_quantile(0.95, rate(teachlink_indexer_http_request_duration_seconds_bucket[5m]))", + "legendFormat": "p95 Latency", + "refId": "B" + }, + { + "expr": "histogram_quantile(0.99, rate(teachlink_indexer_http_request_duration_seconds_bucket[5m]))", + "legendFormat": "p99 Latency", + "refId": "C" + } + ], + "title": "API Latency Percentiles", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 8, "x": 16, "y": 34 }, + "id": 32, + "options": { + "legend": { "displayMode": "list", "placement": "bottom" }, + "tooltip": { "mode": "multi" } + }, + "targets": [ + { + "expr": "teachlink_indexer_dependency_up{dependency=\"database\"}", + "legendFormat": "Database", + "refId": "A" + }, + { + "expr": "teachlink_indexer_dependency_up{dependency=\"horizon\"}", + "legendFormat": "Horizon", + "refId": "B" + }, + { + "expr": "teachlink_indexer_dependency_up{dependency=\"indexer_state\"}", + "legendFormat": "Indexer State", + "refId": "C" + } + ], + "title": "Dependency Health (1=up, 0=down)", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 42 }, + "id": 33, + "options": { + "legend": { "displayMode": "list", "placement": "bottom" }, + "tooltip": { "mode": "multi" } + }, + "targets": [ + { + "expr": "teachlink_indexer_events_processed_total", + "legendFormat": "Events Processed", + "refId": "A" + }, + { + "expr": "teachlink_indexer_errors_total", + "legendFormat": "Errors", + "refId": "B" + }, + { + "expr": "teachlink_indexer_last_processed_ledger", + "legendFormat": "Last Ledger", + "refId": "C" + } + ], + "title": "Indexer Progress & Errors", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 42 }, + "id": 34, + "options": { + "legend": { "displayMode": "list", "placement": "bottom" }, + "tooltip": { "mode": "multi" } + }, + "targets": [ + { + "expr": "rate(teachlink_indexer_http_requests_total{status_code=~\"2..\"}[5m])", + "legendFormat": "2xx {{route}}", + "refId": "A" + }, + { + "expr": "rate(teachlink_indexer_http_requests_total{status_code=~\"4..\"}[5m])", + "legendFormat": "4xx {{route}}", + "refId": "B" + }, + { + "expr": "rate(teachlink_indexer_http_requests_total{status_code=~\"5..\"}[5m])", + "legendFormat": "5xx {{route}}", + "refId": "C" + } + ], + "title": "HTTP Status Code Breakdown", + "type": "timeseries" + } + ], + "refresh": "30s", + "schemaVersion": 39, + "style": "dark", + "tags": ["teachlink", "monitoring", "sustainability", "alerts"], + "templating": { "list": [] }, + "time": { "from": "now-6h", "to": "now" }, + "timepicker": {}, + "timezone": "", + "title": "TeachLink Comprehensive Monitoring", + "uid": "teachlink-monitoring-dashboard", + "version": 1, + "weekStart": "" +} diff --git a/indexer/observability/prometheus/alerts.yml b/indexer/observability/prometheus/alerts.yml index c646419f..80ee9cf7 100644 --- a/indexer/observability/prometheus/alerts.yml +++ b/indexer/observability/prometheus/alerts.yml @@ -100,3 +100,62 @@ groups: annotations: summary: Alertmanager is unavailable description: Alertmanager has not been scrapeable for at least 5 minutes. + + - name: teachlink-sustainability + rules: + - alert: TeachLinkLowEfficiencyScore + expr: teachlink_contract_sustainability_efficiency_score < 7000 + for: 15m + labels: + severity: warning + service: teachlink-contract + annotations: + summary: Contract efficiency score is below 70% + description: The sustainability efficiency score has been below 7000 bps (70%) for 15 minutes. + + - alert: TeachLinkCriticalEfficiencyScore + expr: teachlink_contract_sustainability_efficiency_score < 5000 + for: 5m + labels: + severity: critical + service: teachlink-contract + annotations: + summary: Contract efficiency score is critically low (below 50%) + description: The sustainability efficiency score has dropped below 5000 bps (50%) for 5 minutes. + + - alert: TeachLinkLowHealthScore + expr: teachlink_contract_sustainability_health_score < 60 + for: 15m + labels: + severity: warning + service: teachlink-contract + annotations: + summary: Contract sustainability health score is below 60 + description: The composite sustainability health score has been below 60/100 for 15 minutes. + + - alert: TeachLinkHighEscrowDisputeRate + expr: | + teachlink_indexer_http_requests_total > 0 + and + ( + sum(rate(teachlink_indexer_http_requests_total{status_code=~"5.."}[5m])) + / + clamp_min(sum(rate(teachlink_indexer_http_requests_total[5m])), 1) + ) > 0.10 + for: 10m + labels: + severity: warning + service: teachlink-indexer + annotations: + summary: Elevated error rate may indicate escrow or bridge disputes + description: More than 10% of API requests are failing, which may correlate with on-chain dispute activity. + + - alert: TeachLinkNoNewTransactions + expr: increase(teachlink_contract_sustainability_invocations_total[30m]) == 0 + for: 30m + labels: + severity: warning + service: teachlink-contract + annotations: + summary: No new contract invocations in 30 minutes + description: The sustainability invocation counter has not increased in 30 minutes, suggesting the contract may be idle or unreachable. diff --git a/indexer/src/performance/metrics.service.ts b/indexer/src/performance/metrics.service.ts index 476dd3b8..2b04eba4 100644 --- a/indexer/src/performance/metrics.service.ts +++ b/indexer/src/performance/metrics.service.ts @@ -18,6 +18,16 @@ export class MetricsService { private readonly indexerLedgerLagSeconds: Gauge; private readonly dependencyUp: Gauge; + // Sustainability KPI gauges + private readonly sustainabilityInvocations: Gauge; + private readonly sustainabilityStorageWrites: Gauge; + private readonly sustainabilityEventsEmitted: Gauge; + private readonly sustainabilityRewardsDistributed: Gauge; + private readonly sustainabilityContentMinted: Gauge; + private readonly sustainabilityActiveUsers: Gauge; + private readonly sustainabilityEfficiencyScore: Gauge; + private readonly sustainabilityHealthScore: Gauge; + constructor() { collectDefaultMetrics({ prefix: 'teachlink_indexer_', @@ -95,6 +105,47 @@ export class MetricsService { labelNames: ['dependency'], registers: [this.registry], }); + + this.sustainabilityInvocations = new Gauge({ + name: 'teachlink_contract_sustainability_invocations_total', + help: 'Total contract invocations tracked for sustainability', + registers: [this.registry], + }); + this.sustainabilityStorageWrites = new Gauge({ + name: 'teachlink_contract_sustainability_storage_writes_total', + help: 'Total contract storage writes tracked for sustainability', + registers: [this.registry], + }); + this.sustainabilityEventsEmitted = new Gauge({ + name: 'teachlink_contract_sustainability_events_emitted_total', + help: 'Total contract events emitted tracked for sustainability', + registers: [this.registry], + }); + this.sustainabilityRewardsDistributed = new Gauge({ + name: 'teachlink_contract_sustainability_rewards_distributed_total', + help: 'Total rewards distributed (stroops)', + registers: [this.registry], + }); + this.sustainabilityContentMinted = new Gauge({ + name: 'teachlink_contract_sustainability_content_minted_total', + help: 'Total content tokens minted', + registers: [this.registry], + }); + this.sustainabilityActiveUsers = new Gauge({ + name: 'teachlink_contract_sustainability_active_users_total', + help: 'Total unique active users recorded', + registers: [this.registry], + }); + this.sustainabilityEfficiencyScore = new Gauge({ + name: 'teachlink_contract_sustainability_efficiency_score', + help: 'Contract efficiency score in basis points (0-10000)', + registers: [this.registry], + }); + this.sustainabilityHealthScore = new Gauge({ + name: 'teachlink_contract_sustainability_health_score', + help: 'Composite sustainability health score (0-100)', + registers: [this.registry], + }); } recordHttpRequest( @@ -151,6 +202,26 @@ export class MetricsService { this.dependencyUp.set({ dependency }, isUp ? 1 : 0); } + updateSustainabilityMetrics(m: { + totalInvocations: number; + totalStorageWrites: number; + totalEventsEmitted: number; + totalRewardsDistributed: number; + totalContentMinted: number; + totalActiveUsers: number; + efficiencyScore: number; + healthScore: number; + }): void { + this.sustainabilityInvocations.set(m.totalInvocations); + this.sustainabilityStorageWrites.set(m.totalStorageWrites); + this.sustainabilityEventsEmitted.set(m.totalEventsEmitted); + this.sustainabilityRewardsDistributed.set(m.totalRewardsDistributed); + this.sustainabilityContentMinted.set(m.totalContentMinted); + this.sustainabilityActiveUsers.set(m.totalActiveUsers); + this.sustainabilityEfficiencyScore.set(m.efficiencyScore); + this.sustainabilityHealthScore.set(m.healthScore); + } + async getPrometheusMetrics(): Promise { return this.registry.metrics(); } diff --git a/indexer/src/reporting/dashboard.service.ts b/indexer/src/reporting/dashboard.service.ts index 2a1325a6..5f31a5be 100644 --- a/indexer/src/reporting/dashboard.service.ts +++ b/indexer/src/reporting/dashboard.service.ts @@ -189,4 +189,65 @@ export class DashboardService { .take(limit); return qb.getMany(); } + + /** + * Sustainability metrics snapshot: real-time KPIs derived from current analytics + * plus Prometheus-ready gauge values pushed via MetricsService. + */ + async getSustainabilitySnapshot(): Promise<{ + efficiencyScore: number; + healthScore: number; + bridgeSuccessRate: number; + escrowDisputeRateBps: number; + rewardClaimRate: number; + totalTransactions: number; + totalRewardsIssued: string; + generatedAt: string; + }> { + const a = await this.getCurrentAnalytics(); + + const escrowDisputeRateBps = + a.escrowTotalCount > 0 + ? Math.round((a.escrowDisputeCount / a.escrowTotalCount) * 10000) + : 0; + + const rewardClaimRate = + a.rewardClaimCount > 0 + ? Math.round((a.rewardClaimCount / Math.max(a.rewardClaimCount, 1)) * 10000) + : 0; + + // Efficiency: bridge success rate as proxy (basis points) + const efficiencyScore = a.bridgeSuccessRate; + // Health: weighted composite (50% efficiency, 25% low dispute rate, 25% reward activity) + const disputeComponent = Math.max(0, 100 - Math.round(escrowDisputeRateBps / 100)); + const rewardComponent = Math.min(100, Math.round(rewardClaimRate / 100)); + const healthScore = Math.round( + (Math.min(100, Math.round(efficiencyScore / 100)) * 50 + + disputeComponent * 25 + + rewardComponent * 25) / + 100, + ); + + this.metricsService.updateSustainabilityMetrics({ + totalInvocations: a.bridgeTotalTransactions, + totalStorageWrites: a.bridgeTotalTransactions, + totalEventsEmitted: a.bridgeTotalTransactions + a.escrowTotalCount, + totalRewardsDistributed: Number(a.totalRewardsIssued), + totalContentMinted: 0, + totalActiveUsers: a.rewardClaimCount, + efficiencyScore, + healthScore, + }); + + return { + efficiencyScore, + healthScore, + bridgeSuccessRate: a.bridgeSuccessRate, + escrowDisputeRateBps, + rewardClaimRate, + totalTransactions: a.bridgeTotalTransactions, + totalRewardsIssued: a.totalRewardsIssued, + generatedAt: a.generatedAt, + }; + } } diff --git a/indexer/src/reporting/reporting.controller.ts b/indexer/src/reporting/reporting.controller.ts index a266ed80..cd62218e 100644 --- a/indexer/src/reporting/reporting.controller.ts +++ b/indexer/src/reporting/reporting.controller.ts @@ -35,6 +35,12 @@ export class ReportingController { return this.dashboardService.getCurrentAnalytics(); } + /** Real-time sustainability KPIs and health score */ + @Get('sustainability') + async getSustainability() { + return this.dashboardService.getSustainabilitySnapshot(); + } + /** Generate and persist a report snapshot (manual trigger) */ @Post('reports/snapshots') async generateSnapshot(