334 lines
7.8 KiB
Dart
334 lines
7.8 KiB
Dart
// hey
|
|
|
|
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();
|
|
}
|
|
}
|