feat(metrics): Implement disk usage metrics

- Refactor metrics_cubit
- Implement fallback to legacy when less than 20 dots
This commit is contained in:
NaiJi 2024-08-02 18:37:00 +04:00 committed by Inex Code
parent 48c7d7be2c
commit 68f34dc7b7
7 changed files with 457 additions and 23 deletions

View file

@ -119,4 +119,60 @@ mixin MonitoringApi on GraphQLApiMap {
);
}
}
Future<GenericResult<DiskMetrics?>> getDiskMetrics({
required final int step,
required final DateTime start,
required final DateTime end,
}) async {
QueryResult<Query$GetDiskMetrics> response;
try {
final GraphQLClient client = await getClient();
final variables = Variables$Query$GetDiskMetrics(
step: step,
start: start,
end: end,
);
final query = Options$Query$GetDiskMetrics(variables: variables);
response = await client.query$GetDiskMetrics(query);
if (response.hasException) {
print(response.exception.toString());
return GenericResult<DiskMetrics?>(
success: false,
data: null,
);
}
if (response.parsedData == null) {
return GenericResult<DiskMetrics?>(
success: false,
data: null,
);
}
if (response.parsedData?.monitoring.diskUsage.overallUsage
is Fragment$MonitoringQueryError) {
return GenericResult<DiskMetrics?>(
success: false,
data: null,
);
}
final metrics = DiskMetrics.fromGraphQL(
data: response.parsedData!.monitoring,
stepsInSecond: step,
start: start,
end: end,
);
return GenericResult<DiskMetrics?>(
success: true,
data: metrics,
);
} catch (e) {
print(e);
return GenericResult<DiskMetrics?>(
success: false,
data: null,
message: e.toString(),
);
}
}
}

View file

@ -39,30 +39,21 @@ class MetricsCubit extends Cubit<MetricsState> {
void load(final Period period) async {
try {
final MetricsLoaded newState = await repository.getServerMetrics(period);
final MetricsStateUpdate newStateUpdate =
await repository.getRelevantServerMetrics(period);
int duration = newStateUpdate.nextCheckInSeconds;
if (duration <= 0) {
duration = state.period.stepPeriodInSeconds;
}
timer = Timer(
Duration(seconds: newState.metrics.stepsInSecond.toInt()),
() => load(newState.period),
Duration(seconds: duration),
() => load(period),
);
emit(newState);
return;
} catch (_) {}
try {
final MetricsLoaded newState = await repository.getLegacyMetrics(period);
timer = Timer(
Duration(seconds: newState.metrics.stepsInSecond.toInt()),
() => load(newState.period),
);
emit(newState);
emit(newStateUpdate.newState);
} on StateError {
print('Tried to emit metrics when cubit is closed');
} on MetricsLoadException {
timer = Timer(
Duration(seconds: state.period.stepPeriodInSeconds),
() => load(state.period),
);
} on MetricsUnsupportedException {
emit(MetricsUnsupported(period));
}
}
}

View file

@ -17,9 +17,50 @@ class MetricsUnsupportedException implements Exception {
final String message;
}
class MetricsStateUpdate {
MetricsStateUpdate(this.newState, this.nextCheckInSeconds);
final MetricsState newState;
final int nextCheckInSeconds;
}
class MetricsRepository {
static const String metricsSupportedVersion = '>=3.3.0';
Future<MetricsLoaded> getServerMetrics(final Period period) async {
Future<MetricsStateUpdate> getRelevantServerMetrics(
final Period period,
) async {
MetricsLoaded? state;
int nextUpdate = 0;
try {
final stateLoaded = await _getServerMetrics(period);
nextUpdate = stateLoaded.metrics.stepsInSecond.toInt();
state = stateLoaded;
} catch (_) {}
const minAmountForRendering = 20;
if (state != null &&
state.metrics.cpu.length >= minAmountForRendering &&
state.metrics.bandwidthIn.length >= minAmountForRendering &&
state.metrics.bandwidthOut.length >= minAmountForRendering) {
return MetricsStateUpdate(state, nextUpdate);
}
try {
final stateLoaded = await _getLegacyMetrics(period);
nextUpdate = stateLoaded.metrics.stepsInSecond.toInt();
state = stateLoaded;
} catch (_) {}
if (state != null) {
return MetricsStateUpdate(state, nextUpdate);
}
return MetricsStateUpdate(MetricsUnsupported(period), nextUpdate);
}
Future<MetricsLoaded> _getServerMetrics(final Period period) async {
final String? apiVersion =
getIt<ApiConnectionRepository>().apiData.apiVersion.data;
if (apiVersion == null) {
@ -69,15 +110,23 @@ class MetricsRepository {
step: end.difference(start).inSeconds ~/ 120,
);
final diskResult =
await getIt<ApiConnectionRepository>().api.getDiskMetrics(
start: start,
end: end,
step: end.difference(start).inSeconds ~/ 120,
);
return MetricsLoaded(
period: period,
metrics: result.data!,
source: MetricsDataSource.server,
memoryMetrics: memoryResult.data,
diskMetrics: diskResult.data,
);
}
Future<MetricsLoaded> getLegacyMetrics(final Period period) async {
Future<MetricsLoaded> _getLegacyMetrics(final Period period) async {
if (!(ProvidersController.currentServerProvider?.isAuthorized ?? false)) {
throw MetricsUnsupportedException('Server Provider data is null');
}

View file

@ -146,8 +146,36 @@ class DiskMetrics {
required this.end,
});
DiskMetrics.fromGraphQL({
required final Query$GetDiskMetrics$monitoring data,
required final int stepsInSecond,
required final DateTime start,
required final DateTime end,
}) : this(
stepsInSecond: stepsInSecond,
diskMetrics: Map.fromEntries(
(data.diskUsage.overallUsage as Fragment$MonitoringMetrics)
.metrics
.map(
(final metric) => MapEntry(
metric.metricId,
metric.values
.map(
(final value) => TimeSeriesData(
value.timestamp.millisecondsSinceEpoch ~/ 1000,
double.parse(value.value),
),
)
.toList(),
),
),
),
start: start,
end: end,
);
final num stepsInSecond;
final List<TimeSeriesData> diskMetrics;
final Map<String, List<TimeSeriesData>> diskMetrics;
final DateTime start;
final DateTime end;

View file

@ -145,6 +145,68 @@ class _Chart extends StatelessWidget {
),
),
),
const SizedBox(height: 8),
if (state is MetricsLoaded && state.diskMetrics != null)
FilledCard(
clipped: false,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
'resource_chart.network_title'.tr(),
style:
Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
),
Flexible(
fit: FlexFit.loose,
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
alignment: WrapAlignment.end,
runAlignment: WrapAlignment.end,
children: [
Legend(
color: Theme.of(context).colorScheme.primary,
text: 'resource_chart.in'.tr(),
),
Legend(
color: Theme.of(context).colorScheme.tertiary,
text: 'resource_chart.out'.tr(),
),
],
),
),
],
),
const SizedBox(height: 20),
Stack(
alignment: Alignment.center,
children: [
getDiskChart(state),
AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: state is MetricsLoading ? 1 : 0,
child: const _GraphLoadingCardContent(),
),
],
),
],
),
),
),
];
} else if (state is MetricsUnsupported) {
charts = [
@ -249,6 +311,23 @@ class _Chart extends StatelessWidget {
}
}
Widget getDiskChart(final MetricsLoaded state) {
final data = state.diskMetrics;
if (data == null) {
return const SizedBox();
}
return SizedBox(
height: 200,
child: DiskChart(
listData: data.diskMetrics.values.toList(),
period: state.period,
start: state.metrics.start,
),
);
}
class _GraphLoadingCardContent extends StatelessWidget {
const _GraphLoadingCardContent();

View file

@ -0,0 +1,230 @@
import 'dart:math';
import 'package:easy_localization/easy_localization.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/metrics.dart';
import 'package:selfprivacy/ui/pages/server_details/charts/bottom_title.dart';
class DiskChart extends StatelessWidget {
const DiskChart({
required this.listData,
required this.period,
required this.start,
super.key,
});
final List<List<TimeSeriesData>> listData;
final Period period;
final DateTime start;
List<FlSpot> getSpots(final data) {
var i = 0;
final List<FlSpot> res = [];
for (final d in data) {
res.add(FlSpot(i.toDouble(), d.value));
i++;
}
return res;
}
@override
Widget build(final BuildContext context) => LineChart(
LineChartData(
lineTouchData: LineTouchData(
enabled: true,
touchTooltipData: LineTouchTooltipData(
getTooltipColor: (final LineBarSpot _) =>
Theme.of(context).colorScheme.surface,
tooltipPadding: const EdgeInsets.all(8),
getTooltipItems: (final List<LineBarSpot> touchedBarSpots) {
final List<LineTooltipItem> res = [];
bool timeShown = false;
for (final spot in touchedBarSpots) {
final value = spot.y;
final date = listData[0][spot.x.toInt()].time;
res.add(
LineTooltipItem(
'${timeShown ? '' : DateFormat('HH:mm dd.MM.yyyy').format(date)} ${spot.barIndex == 0 ? 'resource_chart.in'.tr() : 'resource_chart.out'.tr()} ${DiskSize(byte: value.toInt()).toString()}',
TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
);
timeShown = true;
}
return res;
},
),
),
lineBarsData: [
// IN
LineChartBarData(
spots: getSpots(listData[0]),
isCurved: false,
barWidth: 2,
color: Theme.of(context).colorScheme.primary,
dotData: const FlDotData(
show: false,
),
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.primary.withOpacity(0.5),
Theme.of(context).colorScheme.primary.withOpacity(0.0),
],
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
),
),
),
// OUT
LineChartBarData(
spots: getSpots(listData[1]),
isCurved: false,
barWidth: 2,
color: Theme.of(context).colorScheme.tertiary,
dotData: const FlDotData(
show: false,
),
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.tertiary.withOpacity(0.5),
Theme.of(context).colorScheme.tertiary.withOpacity(0.0),
],
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
),
),
),
],
minY: 0,
maxY: [
...listData[0].map((final e) => e.value),
...listData[1].map((final e) => e.value),
].reduce(max) *
1.2,
minX: 0,
titlesData: FlTitlesData(
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
interval: 40,
reservedSize: 30,
getTitlesWidget: (final value, final titleMeta) => Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
bottomTitle(
value.toInt(),
listData[0],
period,
),
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
showTitles: true,
),
),
leftTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: AxisTitles(
sideTitles: SideTitles(
reservedSize: 50,
getTitlesWidget: (final value, final titleMeta) => Padding(
padding: const EdgeInsets.only(left: 5),
child: Text(
DiskSize(byte: value.toInt()).toString(),
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
interval: [
...listData[0].map((final e) => e.value),
...listData[1].map((final e) => e.value),
].reduce(max) *
2 /
6.5,
showTitles: true,
),
),
),
gridData: FlGridData(
show: true,
drawVerticalLine: true,
verticalInterval: 40,
horizontalInterval: [
...listData[0].map((final e) => e.value),
...listData[1].map((final e) => e.value),
].reduce(max) *
2 /
6.5,
getDrawingHorizontalLine: (final value) => FlLine(
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
strokeWidth: 1,
),
getDrawingVerticalLine: (final value) => FlLine(
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
strokeWidth: 1,
),
),
borderData: FlBorderData(
show: true,
border: Border(
bottom: BorderSide(
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
width: 1,
),
left: BorderSide(
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
width: 1,
),
right: BorderSide(
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
width: 1,
),
top: BorderSide(
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
width: 1,
),
),
),
),
);
bool checkToShowTitle(
final double minValue,
final double maxValue,
final SideTitles sideTitles,
final double appliedInterval,
final double value,
) {
if (value < 0) {
return false;
} else if (value == 0) {
return true;
}
final diff = value - minValue;
final finalValue = diff / 20;
return finalValue - finalValue.floor() == 0;
}
}

View file

@ -13,6 +13,7 @@ import 'package:selfprivacy/ui/components/cards/filled_card.dart';
import 'package:selfprivacy/ui/components/list_tiles/list_tile_on_surface_variant.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/pages/server_details/charts/cpu_chart.dart';
import 'package:selfprivacy/ui/pages/server_details/charts/disk_charts.dart';
import 'package:selfprivacy/ui/pages/server_details/charts/memory_chart.dart';
import 'package:selfprivacy/ui/pages/server_details/charts/network_charts.dart';
import 'package:selfprivacy/ui/pages/server_storage/storage_card.dart';