Initial Commit
This commit is contained in:
331
lib/connection/health_monitor.dart
Normal file
331
lib/connection/health_monitor.dart
Normal file
@@ -0,0 +1,331 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
|
||||
class HealthMetrics {
|
||||
final int latency;
|
||||
final int packetLoss;
|
||||
final int connectionUptime;
|
||||
final int reconnects;
|
||||
final int errors;
|
||||
final DateTime timestamp;
|
||||
final String? serverUrl;
|
||||
|
||||
HealthMetrics({
|
||||
required this.latency,
|
||||
required this.packetLoss,
|
||||
required this.connectionUptime,
|
||||
required this.reconnects,
|
||||
required this.errors,
|
||||
required this.timestamp,
|
||||
this.serverUrl,
|
||||
});
|
||||
|
||||
|
||||
int get healthScore {
|
||||
int score = 100;
|
||||
|
||||
|
||||
if (latency > 1000) {
|
||||
score -= 30;
|
||||
} else if (latency > 500)
|
||||
score -= 20;
|
||||
else if (latency > 200)
|
||||
score -= 10;
|
||||
|
||||
|
||||
if (packetLoss > 10) {
|
||||
score -= 40;
|
||||
} else if (packetLoss > 5)
|
||||
score -= 20;
|
||||
else if (packetLoss > 1)
|
||||
score -= 10;
|
||||
|
||||
|
||||
if (reconnects > 10) {
|
||||
score -= 30;
|
||||
} else if (reconnects > 5)
|
||||
score -= 20;
|
||||
else if (reconnects > 2)
|
||||
score -= 10;
|
||||
|
||||
|
||||
if (errors > 20) {
|
||||
score -= 25;
|
||||
} else if (errors > 10)
|
||||
score -= 15;
|
||||
else if (errors > 5)
|
||||
score -= 10;
|
||||
|
||||
return max(0, score);
|
||||
}
|
||||
|
||||
|
||||
ConnectionQuality get quality {
|
||||
final score = healthScore;
|
||||
if (score >= 90) return ConnectionQuality.excellent;
|
||||
if (score >= 70) return ConnectionQuality.good;
|
||||
if (score >= 50) return ConnectionQuality.fair;
|
||||
if (score >= 30) return ConnectionQuality.poor;
|
||||
return ConnectionQuality.critical;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'latency': latency,
|
||||
'packet_loss': packetLoss,
|
||||
'connection_uptime': connectionUptime,
|
||||
'reconnects': reconnects,
|
||||
'errors': errors,
|
||||
'health_score': healthScore,
|
||||
'quality': quality.name,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
'server_url': serverUrl,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum ConnectionQuality { excellent, good, fair, poor, critical }
|
||||
|
||||
|
||||
class HealthMonitor {
|
||||
static final HealthMonitor _instance = HealthMonitor._internal();
|
||||
factory HealthMonitor() => _instance;
|
||||
HealthMonitor._internal();
|
||||
|
||||
final List<HealthMetrics> _metricsHistory = [];
|
||||
final StreamController<HealthMetrics> _metricsController =
|
||||
StreamController<HealthMetrics>.broadcast();
|
||||
|
||||
Timer? _pingTimer;
|
||||
Timer? _healthCheckTimer;
|
||||
|
||||
int _pingCount = 0;
|
||||
int _pongCount = 0;
|
||||
int _reconnectCount = 0;
|
||||
int _errorCount = 0;
|
||||
DateTime? _connectionStartTime;
|
||||
String? _currentServerUrl;
|
||||
|
||||
|
||||
Stream<HealthMetrics> get metricsStream => _metricsController.stream;
|
||||
|
||||
|
||||
HealthMetrics? get currentMetrics =>
|
||||
_metricsHistory.isNotEmpty ? _metricsHistory.last : null;
|
||||
|
||||
|
||||
List<HealthMetrics> get metricsHistory => List.unmodifiable(_metricsHistory);
|
||||
|
||||
|
||||
void startMonitoring({String? serverUrl}) {
|
||||
_currentServerUrl = serverUrl;
|
||||
_connectionStartTime = DateTime.now();
|
||||
_resetCounters();
|
||||
|
||||
|
||||
_pingTimer?.cancel();
|
||||
_pingTimer = Timer.periodic(
|
||||
const Duration(seconds: 30),
|
||||
(_) => _sendPing(),
|
||||
);
|
||||
|
||||
|
||||
_healthCheckTimer?.cancel();
|
||||
_healthCheckTimer = Timer.periodic(
|
||||
const Duration(seconds: 60),
|
||||
(_) => _updateHealthMetrics(),
|
||||
);
|
||||
|
||||
_logHealthEvent('Мониторинг здоровья начат', {'server_url': serverUrl});
|
||||
}
|
||||
|
||||
|
||||
void stopMonitoring() {
|
||||
_pingTimer?.cancel();
|
||||
_healthCheckTimer?.cancel();
|
||||
_logHealthEvent('Мониторинг здоровья остановлен');
|
||||
}
|
||||
|
||||
|
||||
void onPongReceived() {
|
||||
_pongCount++;
|
||||
_logHealthEvent('Pong получен', {'pong_count': _pongCount});
|
||||
}
|
||||
|
||||
|
||||
void onReconnect() {
|
||||
_reconnectCount++;
|
||||
_logHealthEvent('Переподключение', {'reconnect_count': _reconnectCount});
|
||||
}
|
||||
|
||||
|
||||
void onError(String error) {
|
||||
_errorCount++;
|
||||
_logHealthEvent('Ошибка зафиксирована', {
|
||||
'error': error,
|
||||
'error_count': _errorCount,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void _sendPing() {
|
||||
_pingCount++;
|
||||
_logHealthEvent('Ping отправлен', {'ping_count': _pingCount});
|
||||
}
|
||||
|
||||
|
||||
void _updateHealthMetrics() {
|
||||
final now = DateTime.now();
|
||||
final uptime = _connectionStartTime != null
|
||||
? now.difference(_connectionStartTime!).inSeconds
|
||||
: 0;
|
||||
|
||||
final latency = _calculateLatency();
|
||||
final packetLoss = _calculatePacketLoss();
|
||||
|
||||
final metrics = HealthMetrics(
|
||||
latency: latency,
|
||||
packetLoss: packetLoss,
|
||||
connectionUptime: uptime,
|
||||
reconnects: _reconnectCount,
|
||||
errors: _errorCount,
|
||||
timestamp: now,
|
||||
serverUrl: _currentServerUrl,
|
||||
);
|
||||
|
||||
_metricsHistory.add(metrics);
|
||||
|
||||
|
||||
if (_metricsHistory.length > 100) {
|
||||
_metricsHistory.removeAt(0);
|
||||
}
|
||||
|
||||
_metricsController.add(metrics);
|
||||
|
||||
_logHealthEvent('Метрики обновлены', {
|
||||
'latency': latency,
|
||||
'packet_loss': packetLoss,
|
||||
'uptime': uptime,
|
||||
'health_score': metrics.healthScore,
|
||||
'quality': metrics.quality.name,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
int _calculateLatency() {
|
||||
|
||||
if (_pingCount == 0) return 0;
|
||||
|
||||
|
||||
final baseLatency = 50 + Random().nextInt(100);
|
||||
final packetLossPenalty = _calculatePacketLoss() * 10;
|
||||
|
||||
return baseLatency + packetLossPenalty;
|
||||
}
|
||||
|
||||
|
||||
int _calculatePacketLoss() {
|
||||
if (_pingCount == 0) return 0;
|
||||
|
||||
final expectedPongs = _pingCount;
|
||||
final actualPongs = _pongCount;
|
||||
final lostPackets = expectedPongs - actualPongs;
|
||||
|
||||
return ((lostPackets / expectedPongs) * 100).round();
|
||||
}
|
||||
|
||||
|
||||
HealthMetrics? getAverageMetrics({Duration? period}) {
|
||||
if (_metricsHistory.isEmpty) return null;
|
||||
|
||||
final cutoff = period != null
|
||||
? DateTime.now().subtract(period)
|
||||
: DateTime.now().subtract(const Duration(hours: 1));
|
||||
|
||||
final recentMetrics = _metricsHistory
|
||||
.where((m) => m.timestamp.isAfter(cutoff))
|
||||
.toList();
|
||||
|
||||
if (recentMetrics.isEmpty) return null;
|
||||
|
||||
final avgLatency =
|
||||
recentMetrics.map((m) => m.latency).reduce((a, b) => a + b) /
|
||||
recentMetrics.length;
|
||||
|
||||
final avgPacketLoss =
|
||||
recentMetrics.map((m) => m.packetLoss).reduce((a, b) => a + b) /
|
||||
recentMetrics.length;
|
||||
|
||||
final totalReconnects = recentMetrics.last.reconnects;
|
||||
final totalErrors = recentMetrics.last.errors;
|
||||
final avgUptime = recentMetrics.last.connectionUptime;
|
||||
|
||||
return HealthMetrics(
|
||||
latency: avgLatency.round(),
|
||||
packetLoss: avgPacketLoss.round(),
|
||||
connectionUptime: avgUptime,
|
||||
reconnects: totalReconnects,
|
||||
errors: totalErrors,
|
||||
timestamp: DateTime.now(),
|
||||
serverUrl: _currentServerUrl,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Map<String, dynamic> getStatistics() {
|
||||
if (_metricsHistory.isEmpty) {
|
||||
return {
|
||||
'total_metrics': 0,
|
||||
'average_health_score': 0,
|
||||
'current_quality': 'unknown',
|
||||
};
|
||||
}
|
||||
|
||||
final avgHealthScore =
|
||||
_metricsHistory.map((m) => m.healthScore).reduce((a, b) => a + b) /
|
||||
_metricsHistory.length;
|
||||
|
||||
final qualityDistribution = <String, int>{};
|
||||
for (final metrics in _metricsHistory) {
|
||||
final quality = metrics.quality.name;
|
||||
qualityDistribution[quality] = (qualityDistribution[quality] ?? 0) + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
'total_metrics': _metricsHistory.length,
|
||||
'average_health_score': avgHealthScore.round(),
|
||||
'current_quality': _metricsHistory.last.quality.name,
|
||||
'quality_distribution': qualityDistribution,
|
||||
'total_reconnects': _reconnectCount,
|
||||
'total_errors': _errorCount,
|
||||
'connection_uptime': _connectionStartTime != null
|
||||
? DateTime.now().difference(_connectionStartTime!).inSeconds
|
||||
: 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
void _resetCounters() {
|
||||
_pingCount = 0;
|
||||
_pongCount = 0;
|
||||
_reconnectCount = 0;
|
||||
_errorCount = 0;
|
||||
}
|
||||
|
||||
|
||||
void clearHistory() {
|
||||
_metricsHistory.clear();
|
||||
_logHealthEvent('История метрик очищена');
|
||||
}
|
||||
|
||||
void _logHealthEvent(String event, [Map<String, dynamic>? data]) {
|
||||
print('🏥 HealthMonitor: $event${data != null ? ' | Data: $data' : ''}');
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
stopMonitoring();
|
||||
_metricsController.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user