refactor(server-api): Generalize and encapsulate server metrics endpoints

pull/140/head
NaiJi ✨ 2022-11-12 21:29:06 +04:00
parent e66b24d869
commit a7cbde663e
18 changed files with 253 additions and 206 deletions

View File

@ -10,6 +10,7 @@ import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/logic/models/metrics.dart';
import 'package:selfprivacy/logic/models/price.dart';
import 'package:selfprivacy/logic/models/server_basic_info.dart';
import 'package:selfprivacy/logic/models/server_metadata.dart';
@ -444,34 +445,14 @@ class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi {
return server.copyWith(startTime: DateTime.now());
}
Future<Map<String, dynamic>> getMetrics(
@override
Future<ServerMetrics?> getMetrics(
final int serverId,
final DateTime start,
final DateTime end,
final String type,
) async {
final ServerHostingDetails? hetznerServer =
getIt<ApiConfigModel>().serverDetails;
Map<String, dynamic> metrics = {};
final Dio client = await getClient();
try {
final Map<String, dynamic> queryParameters = {
'start': start.toUtc().toIso8601String(),
'end': end.toUtc().toIso8601String(),
'type': type
};
final Response res = await client.get(
'/servers/${hetznerServer!.id}/metrics',
queryParameters: queryParameters,
);
metrics = res.data;
} catch (e) {
print(e);
} finally {
close(client);
}
return metrics;
ServerMetrics? metrics;
return metrics!;
}
@override

View File

@ -11,6 +11,7 @@ import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/logic/models/metrics.dart';
import 'package:selfprivacy/logic/models/price.dart';
import 'package:selfprivacy/logic/models/server_basic_info.dart';
import 'package:selfprivacy/logic/models/server_metadata.dart';
@ -486,14 +487,12 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
return server.copyWith(startTime: DateTime.now());
}
Future<Map<String, dynamic>> getMetrics(
Future<Map<String, dynamic>> requestRawMetrics(
final int serverId,
final DateTime start,
final DateTime end,
final String type,
) async {
final ServerHostingDetails? hetznerServer =
getIt<ApiConfigModel>().serverDetails;
Map<String, dynamic> metrics = {};
final Dio client = await getClient();
try {
@ -503,10 +502,10 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
'type': type
};
final Response res = await client.get(
'/servers/${hetznerServer!.id}/metrics',
'/servers/$serverId/metrics',
queryParameters: queryParameters,
);
metrics = res.data;
metrics = res.data['metrics'];
} catch (e) {
print(e);
} finally {
@ -516,6 +515,70 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
return metrics;
}
List<TimeSeriesData> timeSeriesSerializer(
final Map<String, dynamic> json,
final String type,
) {
final List list = json['time_series'][type]['values'];
return list
.map((final el) => TimeSeriesData(el[0], double.parse(el[1])))
.toList();
}
@override
Future<ServerMetrics?> getMetrics(
final int serverId,
final DateTime start,
final DateTime end,
) async {
ServerMetrics? metrics;
final Map<String, dynamic> rawCpuMetrics = await requestRawMetrics(
serverId,
start,
end,
'cpu',
);
final Map<String, dynamic> rawNetworkMetrics = await requestRawMetrics(
serverId,
start,
end,
'network',
);
if (rawNetworkMetrics.isEmpty || rawCpuMetrics.isEmpty) {
return metrics;
}
metrics = ServerMetrics(
cpu: timeSeriesSerializer(
rawCpuMetrics,
'cpu',
),
ppsIn: timeSeriesSerializer(
rawNetworkMetrics,
'network.0.pps.in',
),
ppsOut: timeSeriesSerializer(
rawNetworkMetrics,
'network.0.pps.out',
),
bandwidthIn: timeSeriesSerializer(
rawNetworkMetrics,
'network.0.bandwidth.in',
),
bandwidthOut: timeSeriesSerializer(
rawNetworkMetrics,
'network.0.bandwidth.out',
),
end: end,
start: start,
stepsInSecond: rawCpuMetrics['step'],
);
return metrics;
}
@override
Future<List<ServerMetadataEntity>> getMetadata(final int serverId) async {
List<ServerMetadataEntity> metadata = [];

View File

@ -2,6 +2,7 @@ import 'package:selfprivacy/logic/api_maps/rest_maps/api_map.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/logic/models/metrics.dart';
import 'package:selfprivacy/logic/models/server_basic_info.dart';
import 'package:selfprivacy/logic/models/server_metadata.dart';
import 'package:selfprivacy/logic/models/server_provider_location.dart';
@ -41,6 +42,11 @@ abstract class ServerProviderApi extends ApiMap {
Future<bool> isApiTokenValid(final String token);
ProviderApiTokenValidation getApiTokenValidation();
Future<List<ServerMetadataEntity>> getMetadata(final int serverId);
Future<ServerMetrics?> getMetrics(
final int serverId,
final DateTime start,
final DateTime end,
);
abstract final String infectProviderName;
}

View File

@ -1,74 +0,0 @@
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/models/hetzner_metrics.dart';
import 'package:selfprivacy/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart';
class MetricsLoadException implements Exception {
MetricsLoadException(this.message);
final String message;
}
class HetznerMetricsRepository {
Future<HetznerMetricsLoaded> getMetrics(final Period period) async {
final DateTime end = DateTime.now();
DateTime start;
switch (period) {
case Period.hour:
start = end.subtract(const Duration(hours: 1));
break;
case Period.day:
start = end.subtract(const Duration(days: 1));
break;
case Period.month:
start = end.subtract(const Duration(days: 15));
break;
}
final HetznerApi api = HetznerApi(
/// TODO: Hetzner hardcode (???)
hasLogger: false,
region: getIt<ApiConfigModel>().serverLocation,
);
final List<Map<String, dynamic>> results = await Future.wait([
api.getMetrics(start, end, 'cpu'),
api.getMetrics(start, end, 'network'),
]);
final cpuMetricsData = results[0]['metrics'];
final networkMetricsData = results[1]['metrics'];
if (cpuMetricsData == null || networkMetricsData == null) {
throw MetricsLoadException('Metrics data is null');
}
return HetznerMetricsLoaded(
period: period,
start: start,
end: end,
stepInSeconds: cpuMetricsData['step'],
cpu: timeSeriesSerializer(cpuMetricsData, 'cpu'),
ppsIn: timeSeriesSerializer(networkMetricsData, 'network.0.pps.in'),
ppsOut: timeSeriesSerializer(networkMetricsData, 'network.0.pps.out'),
bandwidthIn:
timeSeriesSerializer(networkMetricsData, 'network.0.bandwidth.in'),
bandwidthOut: timeSeriesSerializer(
networkMetricsData,
'network.0.bandwidth.out',
),
);
}
}
List<TimeSeriesData> timeSeriesSerializer(
final Map<String, dynamic> json,
final String type,
) {
final List list = json['time_series'][type]['values'];
return list
.map((final el) => TimeSeriesData(el[0], double.parse(el[1])))
.toList();
}

View File

@ -1,45 +0,0 @@
part of 'hetzner_metrics_cubit.dart';
abstract class HetznerMetricsState extends Equatable {
const HetznerMetricsState();
abstract final Period period;
}
class HetznerMetricsLoading extends HetznerMetricsState {
const HetznerMetricsLoading(this.period);
@override
final Period period;
@override
List<Object?> get props => [period];
}
class HetznerMetricsLoaded extends HetznerMetricsState {
const HetznerMetricsLoaded({
required this.period,
required this.start,
required this.end,
required this.stepInSeconds,
required this.cpu,
required this.ppsIn,
required this.ppsOut,
required this.bandwidthIn,
required this.bandwidthOut,
});
@override
final Period period;
final DateTime start;
final DateTime end;
final num stepInSeconds;
final List<TimeSeriesData> cpu;
final List<TimeSeriesData> ppsIn;
final List<TimeSeriesData> ppsOut;
final List<TimeSeriesData> bandwidthIn;
final List<TimeSeriesData> bandwidthOut;
@override
List<Object?> get props => [period, start, end];
}

View File

@ -3,16 +3,16 @@ import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/models/hetzner_metrics.dart';
import 'package:selfprivacy/logic/models/metrics.dart';
import 'package:selfprivacy/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart';
import 'package:selfprivacy/logic/cubit/metrics/metrics_repository.dart';
part 'hetzner_metrics_state.dart';
part 'metrics_state.dart';
class HetznerMetricsCubit extends Cubit<HetznerMetricsState> {
HetznerMetricsCubit() : super(const HetznerMetricsLoading(Period.day));
class MetricsCubit extends Cubit<MetricsState> {
MetricsCubit() : super(const MetricsLoading(Period.day));
final HetznerMetricsRepository repository = HetznerMetricsRepository();
final MetricsRepository repository = MetricsRepository();
Timer? timer;
@ -30,7 +30,7 @@ class HetznerMetricsCubit extends Cubit<HetznerMetricsState> {
void changePeriod(final Period period) async {
closeTimer();
emit(HetznerMetricsLoading(period));
emit(MetricsLoading(period));
load(period);
}
@ -40,14 +40,14 @@ class HetznerMetricsCubit extends Cubit<HetznerMetricsState> {
void load(final Period period) async {
try {
final HetznerMetricsLoaded newState = await repository.getMetrics(period);
final MetricsLoaded newState = await repository.getMetrics(period);
timer = Timer(
Duration(seconds: newState.stepInSeconds.toInt()),
Duration(seconds: newState.metrics.stepsInSecond.toInt()),
() => load(newState.period),
);
emit(newState);
} on StateError {
print('Tried to emit Hetzner metrics when cubit is closed');
print('Tried to emit metrics when cubit is closed');
} on MetricsLoadException {
timer = Timer(
Duration(seconds: state.period.stepPeriodInSeconds),

View File

@ -0,0 +1,67 @@
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_creator.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_settings.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_factory.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/metrics/metrics_cubit.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/metrics.dart';
class MetricsLoadException implements Exception {
MetricsLoadException(this.message);
final String message;
}
class MetricsRepository {
ServerProviderApiFactory? serverProviderApiFactory;
void _buildServerProviderFactory() {
final ServerProvider? providerType = getIt<ApiConfigModel>().serverProvider;
final String? location = getIt<ApiConfigModel>().serverLocation;
serverProviderApiFactory = ApiFactoryCreator.createServerProviderApiFactory(
ServerProviderApiFactorySettings(
provider: providerType ?? ServerProvider.unknown,
location: location,
),
);
}
Future<MetricsLoaded> getMetrics(final Period period) async {
if (serverProviderApiFactory == null) {
_buildServerProviderFactory();
}
final DateTime end = DateTime.now();
DateTime start;
switch (period) {
case Period.hour:
start = end.subtract(const Duration(hours: 1));
break;
case Period.day:
start = end.subtract(const Duration(days: 1));
break;
case Period.month:
start = end.subtract(const Duration(days: 15));
break;
}
final serverId = getIt<ApiConfigModel>().serverDetails!.id;
final ServerMetrics? metrics =
await serverProviderApiFactory!.getServerProvider().getMetrics(
serverId,
start,
end,
);
if (metrics == null) {
throw MetricsLoadException('Metrics data is null');
}
return MetricsLoaded(
period: period,
metrics: metrics,
);
}
}

View File

@ -0,0 +1,31 @@
part of 'metrics_cubit.dart';
abstract class MetricsState extends Equatable {
const MetricsState();
abstract final Period period;
}
class MetricsLoading extends MetricsState {
const MetricsLoading(this.period);
@override
final Period period;
@override
List<Object?> get props => [period];
}
class MetricsLoaded extends MetricsState {
const MetricsLoaded({
required this.period,
required this.metrics,
});
@override
final Period period;
final ServerMetrics metrics;
@override
List<Object?> get props => [period, metrics];
}

View File

@ -98,13 +98,13 @@ class ApiProviderVolumeCubit
}
getIt<NavigationService>().showSnackBar(
'Hetzner resized, waiting 10 seconds',
'Provider volume resized, waiting 10 seconds',
);
await Future.delayed(const Duration(seconds: 10));
await ServerApi().resizeVolume(volume.name);
getIt<NavigationService>().showSnackBar(
'Server api resized, waiting 20 seconds',
'Server volume resized, waiting 20 seconds',
);
await Future.delayed(const Duration(seconds: 20));

View File

@ -305,14 +305,13 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
),
);
timer = Timer(pauseDuration, () async {
final ServerHostingDetails hetznerServerDetails =
await repository.restart();
final ServerHostingDetails serverDetails = await repository.restart();
await repository.saveIsServerResetedFirstTime(true);
await repository.saveServerDetails(hetznerServerDetails);
await repository.saveServerDetails(serverDetails);
final ServerInstallationNotFinished newState = dataState.copyWith(
isServerResetedFirstTime: true,
serverDetails: hetznerServerDetails,
serverDetails: serverDetails,
isLoading: false,
);
@ -347,14 +346,13 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
),
);
timer = Timer(pauseDuration, () async {
final ServerHostingDetails hetznerServerDetails =
await repository.restart();
final ServerHostingDetails serverDetails = await repository.restart();
await repository.saveIsServerResetedSecondTime(true);
await repository.saveServerDetails(hetznerServerDetails);
await repository.saveServerDetails(serverDetails);
final ServerInstallationNotFinished newState = dataState.copyWith(
isServerResetedSecondTime: true,
serverDetails: hetznerServerDetails,
serverDetails: serverDetails,
isLoading: false,
);
@ -560,8 +558,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
}
}
Future<List<ServerBasicInfoWithValidators>>
getServersOnHetznerAccount() async {
Future<List<ServerBasicInfoWithValidators>> getAvailableServers() async {
final ServerInstallationRecovery dataState =
state as ServerInstallationRecovery;
final List<ServerBasicInfo> servers =

View File

@ -1,11 +0,0 @@
class TimeSeriesData {
TimeSeriesData(
this.secondsSinceEpoch,
this.value,
);
final int secondsSinceEpoch;
DateTime get time =>
DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch * 1000);
final double value;
}

View File

@ -0,0 +1,34 @@
class TimeSeriesData {
TimeSeriesData(
this.secondsSinceEpoch,
this.value,
);
final int secondsSinceEpoch;
DateTime get time =>
DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch * 1000);
final double value;
}
class ServerMetrics {
ServerMetrics({
required this.stepsInSecond,
required this.cpu,
required this.ppsIn,
required this.ppsOut,
required this.bandwidthIn,
required this.bandwidthOut,
required this.start,
required this.end,
});
final num stepsInSecond;
final List<TimeSeriesData> cpu;
final List<TimeSeriesData> ppsIn;
final List<TimeSeriesData> ppsOut;
final List<TimeSeriesData> bandwidthIn;
final List<TimeSeriesData> bandwidthOut;
final DateTime start;
final DateTime end;
}

View File

@ -1,5 +1,5 @@
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/models/hetzner_metrics.dart';
import 'package:selfprivacy/logic/models/metrics.dart';
import 'package:intl/intl.dart';
String bottomTitle(

View File

@ -3,11 +3,11 @@ part of '../server_details_screen.dart';
class _Chart extends StatelessWidget {
@override
Widget build(final BuildContext context) {
final HetznerMetricsCubit cubit = context.watch<HetznerMetricsCubit>();
final MetricsCubit cubit = context.watch<MetricsCubit>();
final Period period = cubit.state.period;
final HetznerMetricsState state = cubit.state;
final MetricsState state = cubit.state;
List<Widget> charts;
if (state is HetznerMetricsLoaded || state is HetznerMetricsLoading) {
if (state is MetricsLoaded || state is MetricsLoading) {
charts = [
FilledCard(
clipped: false,
@ -26,10 +26,10 @@ class _Chart extends StatelessWidget {
Stack(
alignment: Alignment.center,
children: [
if (state is HetznerMetricsLoaded) getCpuChart(state),
if (state is MetricsLoaded) getCpuChart(state),
AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: state is HetznerMetricsLoading ? 1 : 0,
opacity: state is MetricsLoading ? 1 : 0,
child: const _GraphLoadingCardContent(),
),
],
@ -72,10 +72,10 @@ class _Chart extends StatelessWidget {
Stack(
alignment: Alignment.center,
children: [
if (state is HetznerMetricsLoaded) getBandwidthChart(state),
if (state is MetricsLoaded) getBandwidthChart(state),
AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: state is HetznerMetricsLoading ? 1 : 0,
opacity: state is MetricsLoading ? 1 : 0,
child: const _GraphLoadingCardContent(),
),
],
@ -122,29 +122,29 @@ class _Chart extends StatelessWidget {
);
}
Widget getCpuChart(final HetznerMetricsLoaded state) {
final data = state.cpu;
Widget getCpuChart(final MetricsLoaded state) {
final data = state.metrics.cpu;
return SizedBox(
height: 200,
child: CpuChart(
data: data,
period: state.period,
start: state.start,
start: state.metrics.start,
),
);
}
Widget getBandwidthChart(final HetznerMetricsLoaded state) {
final ppsIn = state.bandwidthIn;
final ppsOut = state.bandwidthOut;
Widget getBandwidthChart(final MetricsLoaded state) {
final ppsIn = state.metrics.bandwidthIn;
final ppsOut = state.metrics.bandwidthOut;
return SizedBox(
height: 200,
child: NetworkChart(
listData: [ppsIn, ppsOut],
period: state.period,
start: state.start,
start: state.metrics.start,
),
);
}

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/models/hetzner_metrics.dart';
import 'package:selfprivacy/logic/models/metrics.dart';
import 'package:intl/intl.dart';
import 'package:selfprivacy/ui/pages/server_details/charts/bottom_title.dart';

View File

@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/hetzner_metrics.dart';
import 'package:selfprivacy/logic/models/metrics.dart';
import 'package:selfprivacy/ui/pages/server_details/charts/bottom_title.dart';
class NetworkChart extends StatelessWidget {

View File

@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart';
import 'package:selfprivacy/logic/cubit/metrics/metrics_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_volumes/server_volume_cubit.dart';
@ -22,7 +22,6 @@ import 'package:selfprivacy/ui/pages/server_details/charts/cpu_chart.dart';
import 'package:selfprivacy/ui/pages/server_details/charts/network_charts.dart';
import 'package:selfprivacy/ui/pages/server_storage/storage_card.dart';
import 'package:selfprivacy/utils/extensions/duration.dart';
import 'package:selfprivacy/utils/extensions/string_extensions.dart';
import 'package:selfprivacy/utils/named_font_weight.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:timezone/timezone.dart';
@ -93,7 +92,7 @@ class _ServerDetailsScreenState extends State<ServerDetailsScreen>
),
const SizedBox(height: 8),
BlocProvider(
create: (final context) => HetznerMetricsCubit()..restart(),
create: (final context) => MetricsCubit()..restart(),
child: _Chart(),
),
const SizedBox(height: 8),

View File

@ -45,9 +45,8 @@ class _RecoveryConfirmServerState extends State<RecoveryConfirmServer> {
hasFlashButton: false,
children: [
FutureBuilder<List<ServerBasicInfoWithValidators>>(
future: context
.read<ServerInstallationCubit>()
.getServersOnHetznerAccount(),
future:
context.read<ServerInstallationCubit>().getAvailableServers(),
builder: (final context, final snapshot) {
if (snapshot.hasData) {
final servers = snapshot.data;