diff --git a/contracts/teachlink/src/events.rs b/contracts/teachlink/src/events.rs index 8b5b6a32..5c98b4c3 100644 --- a/contracts/teachlink/src/events.rs +++ b/contracts/teachlink/src/events.rs @@ -725,4 +725,4 @@ pub struct ChainMetricsUpdatedEvent { pub transaction_count: u64, pub average_fee: i128, pub updated_at: u64, -} \ No newline at end of file +} diff --git a/contracts/teachlink/src/lib.rs b/contracts/teachlink/src/lib.rs index b7133695..e180722e 100644 --- a/contracts/teachlink/src/lib.rs +++ b/contracts/teachlink/src/lib.rs @@ -160,6 +160,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; @@ -2045,4 +2046,4 @@ impl TeachLinkBridge { &env, ) } -} \ No newline at end of file +} 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 9bca9576..6ef34da2 100644 --- a/contracts/teachlink/src/types.rs +++ b/contracts/teachlink/src/types.rs @@ -1768,4 +1768,4 @@ pub struct ScalingMetrics { pub shed_operations: u64, pub average_batch_size: u32, pub last_scaling_adjustment: u64, -} \ No newline at end of file +} 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(