Add max concurrent players over 24 hours badge to home

This commit is contained in:
RaidMax 2021-08-26 17:35:05 -05:00
parent 19a49504b8
commit a815bcbff5
34 changed files with 4904 additions and 47 deletions

View File

@ -792,8 +792,10 @@ namespace IW4MAdmin
};
}
private async Task<long> GetIdForServer(Server server)
public override async Task<long> GetIdForServer(Server server = null)
{
server ??= this;
if ($"{server.IP}:{server.Port.ToString()}" == "66.150.121.184:28965")
{
return 886229536;

View File

@ -185,6 +185,8 @@ namespace IW4MAdmin.Application
? WebfrontCore.Program.Init(ServerManager, serviceProvider, services, ServerManager.CancellationToken)
: Task.CompletedTask;
var collectionService = serviceProvider.GetRequiredService<IServerDataCollector>();
// we want to run this one on a manual thread instead of letting the thread pool handle it,
// because we can't exit early from waiting on console input, and it prevents us from restarting
var inputThread = new Thread(async () => await ReadConsoleInput(logger));
@ -195,7 +197,8 @@ namespace IW4MAdmin.Application
ServerManager.Start(),
webfrontTask,
serviceProvider.GetRequiredService<IMasterCommunication>()
.RunUploadStatus(ServerManager.CancellationToken)
.RunUploadStatus(ServerManager.CancellationToken),
collectionService.BeginCollectionAsync(cancellationToken: ServerManager.CancellationToken)
};
logger.LogDebug("Starting webfront and input tasks");
@ -410,6 +413,8 @@ namespace IW4MAdmin.Application
.AddSingleton<IHitInfoBuilder, HitInfoBuilder>()
.AddSingleton(typeof(ILookupCache<>), typeof(LookupCache<>))
.AddSingleton(typeof(IDataValueCache<,>), typeof(DataValueCache<,>))
.AddSingleton<IServerDataViewer, ServerDataViewer>()
.AddSingleton<IServerDataCollector, ServerDataCollector>()
.AddSingleton(translationLookup)
.AddDatabaseContextOptions(appConfig);

View File

@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Data.Models.Client.Stats.Reference;
using Data.Models.Server;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Misc
{
/// <inheritdoc/>
public class ServerDataCollector : IServerDataCollector
{
private readonly ILogger _logger;
private readonly IManager _manager;
private readonly IDatabaseContextFactory _contextFactory;
private bool _inProgress;
private TimeSpan _period;
public ServerDataCollector(ILogger<ServerDataCollector> logger, IManager manager,
IDatabaseContextFactory contextFactory)
{
_logger = logger;
_manager = manager;
_contextFactory = contextFactory;
}
public async Task BeginCollectionAsync(TimeSpan? period = null, CancellationToken cancellationToken = default)
{
if (_inProgress)
{
throw new InvalidOperationException($"{nameof(ServerDataCollector)} is already collecting data");
}
_logger.LogDebug("Initializing data collection with {Name}", nameof(ServerDataCollector));
_inProgress = true;
_period = period ?? TimeSpan.FromMinutes(Utilities.IsDevelopment ? 1 : 5);
while (!cancellationToken.IsCancellationRequested)
{
try
{
await Task.Delay(_period, cancellationToken);
_logger.LogDebug("{Name} is collecting server data", nameof(ServerDataCollector));
var data = await BuildCollectionData(cancellationToken);
await SaveData(data, cancellationToken);
}
catch (TaskCanceledException)
{
_logger.LogInformation("Shutdown requested for {Name}", nameof(ServerDataCollector));
return;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error encountered collecting server data for {Name}",
nameof(ServerDataCollector));
}
}
}
private async Task<IEnumerable<EFServerSnapshot>> BuildCollectionData(CancellationToken token)
{
var data = await Task.WhenAll(_manager.GetServers()
.Select(async server => new EFServerSnapshot
{
CapturedAt = DateTime.UtcNow,
PeriodBlock = (int) (DateTimeOffset.UtcNow - DateTimeOffset.UnixEpoch).TotalMinutes,
ServerId = await server.GetIdForServer(),
MapId = await GetOrCreateMap(server.CurrentMap.Name, (Reference.Game) server.GameName, token),
ClientCount = server.ClientNum
}));
return data;
}
private async Task<int> GetOrCreateMap(string mapName, Reference.Game game, CancellationToken token)
{
await using var context = _contextFactory.CreateContext();
var existingMap =
await context.Maps.FirstOrDefaultAsync(map => map.Name == mapName && map.Game == game, token);
if (existingMap != null)
{
return existingMap.MapId;
}
var newMap = new EFMap
{
Name = mapName,
Game = game
};
context.Maps.Add(newMap);
await context.SaveChangesAsync(token);
return newMap.MapId;
}
private async Task SaveData(IEnumerable<EFServerSnapshot> snapshots, CancellationToken token)
{
await using var context = _contextFactory.CreateContext();
context.ServerSnapshots.AddRange(snapshots);
await context.SaveChangesAsync(token);
}
}
}

View File

@ -0,0 +1,99 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models.Client;
using Data.Models.Server;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc
{
/// <inheritdoc/>
public class ServerDataViewer : IServerDataViewer
{
private readonly ILogger _logger;
private readonly IDataValueCache<EFServerSnapshot, int> _snapshotCache;
private readonly IDataValueCache<EFClient, (int, int)> _serverStatsCache;
private readonly TimeSpan? _cacheTimeSpan =
Utilities.IsDevelopment ? TimeSpan.FromSeconds(1) : (TimeSpan?) null;
public ServerDataViewer(ILogger<ServerDataViewer> logger, IDataValueCache<EFServerSnapshot, int> snapshotCache,
IDataValueCache<EFClient, (int, int)> serverStatsCache)
{
_logger = logger;
_snapshotCache = snapshotCache;
_serverStatsCache = serverStatsCache;
}
public async Task<int> MaxConcurrentClientsAsync(long? serverId = null, TimeSpan? overPeriod = null,
CancellationToken token = default)
{
_snapshotCache.SetCacheItem(async (snapshots, cancellationToken) =>
{
var oldestEntry = overPeriod.HasValue
? DateTime.UtcNow - overPeriod.Value
: DateTime.UtcNow.AddDays(-1);
var maxClients = 0;
if (serverId != null)
{
maxClients = await snapshots.Where(snapshot => snapshot.ServerId == serverId)
.Where(snapshot => snapshot.CapturedAt >= oldestEntry)
.MaxAsync(snapshot => (int?)snapshot.ClientCount, cancellationToken) ?? 0;
}
else
{
maxClients = await snapshots.Where(snapshot => snapshot.CapturedAt >= oldestEntry)
.GroupBy(snapshot => snapshot.PeriodBlock)
.Select(grp => grp.Sum(snapshot => (int?)snapshot.ClientCount))
.MaxAsync(cancellationToken) ?? 0;
}
_logger.LogDebug("Max concurrent clients since {Start} is {Clients}", oldestEntry, maxClients);
return maxClients;
}, nameof(MaxConcurrentClientsAsync), _cacheTimeSpan);
try
{
return await _snapshotCache.GetCacheItem(nameof(MaxConcurrentClientsAsync), token);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not retrieve data for {Name}", nameof(MaxConcurrentClientsAsync));
return 0;
}
}
public async Task<(int, int)> ClientCountsAsync(TimeSpan? overPeriod = null, CancellationToken token = default)
{
_serverStatsCache.SetCacheItem(async (set, cancellationToken) =>
{
var count = await set.CountAsync(cancellationToken);
var startOfPeriod =
DateTime.UtcNow.AddHours(-overPeriod?.TotalHours ?? -24);
var recentCount = await set.CountAsync(client => client.LastConnection >= startOfPeriod,
cancellationToken);
return (count, recentCount);
}, nameof(_serverStatsCache), _cacheTimeSpan);
try
{
return await _serverStatsCache.GetCacheItem(nameof(_serverStatsCache), token);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not retrieve data for {Name}", nameof(ClientCountsAsync));
return (0, 0);
}
}
}
}

View File

@ -1,4 +1,5 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
@ -6,7 +7,7 @@ namespace Data.Abstractions
{
public interface IDataValueCache<T, V> where T : class
{
void SetCacheItem(Func<DbSet<T>, Task<V>> itemGetter, string keyName, TimeSpan? expirationTime = null);
Task<V> GetCacheItem(string keyName);
void SetCacheItem(Func<DbSet<T>, CancellationToken, Task<V>> itemGetter, string keyName, TimeSpan? expirationTime = null);
Task<V> GetCacheItem(string keyName, CancellationToken token = default);
}
}

View File

@ -36,12 +36,13 @@ namespace Data.Context
public DbSet<EFWeapon> Weapons { get; set; }
public DbSet<EFWeaponAttachment> WeaponAttachments { get; set; }
public DbSet<EFMap> Maps { get; set; }
#endregion
#region MISC
public DbSet<EFInboxMessage> InboxMessages { get; set; }
public DbSet<EFServerSnapshot> ServerSnapshots { get;set; }
#endregion
@ -133,6 +134,7 @@ namespace Data.Context
modelBuilder.Entity<EFAlias>().ToTable("EFAlias");
modelBuilder.Entity<EFAliasLink>().ToTable("EFAliasLinks");
modelBuilder.Entity<EFPenalty>().ToTable("EFPenalties");
modelBuilder.Entity<EFServerSnapshot>().ToTable(nameof(EFServerSnapshot));
Models.Configuration.StatsModelConfiguration.Configure(modelBuilder);

View File

@ -8,7 +8,7 @@
<PackageId>RaidMax.IW4MAdmin.Data</PackageId>
<Title>RaidMax.IW4MAdmin.Data</Title>
<Authors />
<PackageVersion>1.0.4</PackageVersion>
<PackageVersion>1.0.5</PackageVersion>
</PropertyGroup>
<ItemGroup>

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Microsoft.EntityFrameworkCore;
@ -19,7 +20,7 @@ namespace Data.Helpers
public string Key { get; set; }
public DateTime LastRetrieval { get; set; }
public TimeSpan ExpirationTime { get; set; }
public Func<DbSet<T>, Task<V>> Getter { get; set; }
public Func<DbSet<T>, CancellationToken, Task<V>> Getter { get; set; }
public V Value { get; set; }
public bool IsExpired => (DateTime.Now - LastRetrieval.Add(ExpirationTime)).TotalSeconds > 0;
}
@ -30,14 +31,14 @@ namespace Data.Helpers
_contextFactory = contextFactory;
}
public void SetCacheItem(Func<DbSet<T>, Task<V>> getter, string key, TimeSpan? expirationTime = null)
public void SetCacheItem(Func<DbSet<T>, CancellationToken, Task<V>> getter, string key, TimeSpan? expirationTime = null)
{
if (_cacheStates.ContainsKey(key))
{
_logger.LogDebug("Cache key {key} is already added", key);
return;
}
var state = new CacheState()
{
Key = key,
@ -48,7 +49,7 @@ namespace Data.Helpers
_cacheStates.Add(key, state);
}
public async Task<V> GetCacheItem(string keyName)
public async Task<V> GetCacheItem(string keyName, CancellationToken cancellationToken = default)
{
if (!_cacheStates.ContainsKey(keyName))
{
@ -59,19 +60,19 @@ namespace Data.Helpers
if (state.IsExpired)
{
await RunCacheUpdate(state);
await RunCacheUpdate(state, cancellationToken);
}
return state.Value;
}
private async Task RunCacheUpdate(CacheState state)
private async Task RunCacheUpdate(CacheState state, CancellationToken token)
{
try
{
await using var context = _contextFactory.CreateContext(false);
var set = context.Set<T>();
var value = await state.Getter(set);
var value = await state.Getter(set, token);
state.Value = value;
state.LastRetrieval = DateTime.Now;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.MySql
{
public partial class AddEFServerSnapshot : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "EFServerSnapshot",
columns: table => new
{
ServerSnapshotId = table.Column<long>(nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Active = table.Column<bool>(nullable: false),
CapturedAt = table.Column<DateTime>(nullable: false),
PeriodBlock = table.Column<int>(nullable: false),
ServerId = table.Column<long>(nullable: false),
MapId = table.Column<int>(nullable: false),
ClientCount = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_EFServerSnapshot", x => x.ServerSnapshotId);
table.ForeignKey(
name: "FK_EFServerSnapshot_EFMaps_MapId",
column: x => x.MapId,
principalTable: "EFMaps",
principalColumn: "MapId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_EFServerSnapshot_EFServers_ServerId",
column: x => x.ServerId,
principalTable: "EFServers",
principalColumn: "ServerId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_EFServerSnapshot_MapId",
table: "EFServerSnapshot",
column: "MapId");
migrationBuilder.CreateIndex(
name: "IX_EFServerSnapshot_ServerId",
table: "EFServerSnapshot",
column: "ServerId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "EFServerSnapshot");
}
}
}

View File

@ -1001,6 +1001,39 @@ namespace Data.Migrations.MySql
b.ToTable("EFServers");
});
modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b =>
{
b.Property<long>("ServerSnapshotId")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
b.Property<bool>("Active")
.HasColumnType("tinyint(1)");
b.Property<DateTime>("CapturedAt")
.HasColumnType("datetime(6)");
b.Property<int>("ClientCount")
.HasColumnType("int");
b.Property<int>("MapId")
.HasColumnType("int");
b.Property<int>("PeriodBlock")
.HasColumnType("int");
b.Property<long>("ServerId")
.HasColumnType("bigint");
b.HasKey("ServerSnapshotId");
b.HasIndex("MapId");
b.HasIndex("ServerId");
b.ToTable("EFServerSnapshot");
});
modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b =>
{
b.Property<int>("StatisticId")
@ -1339,6 +1372,21 @@ namespace Data.Migrations.MySql
.IsRequired();
});
modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b =>
{
b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map")
.WithMany()
.HasForeignKey("MapId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Data.Models.Server.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b =>
{
b.HasOne("Data.Models.Server.EFServer", "Server")

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Data.Migrations.Postgresql
{
public partial class AddEFServerSnapshot : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "EFServerSnapshot",
columns: table => new
{
ServerSnapshotId = table.Column<long>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn),
Active = table.Column<bool>(nullable: false),
CapturedAt = table.Column<DateTime>(nullable: false),
PeriodBlock = table.Column<int>(nullable: false),
ServerId = table.Column<long>(nullable: false),
MapId = table.Column<int>(nullable: false),
ClientCount = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_EFServerSnapshot", x => x.ServerSnapshotId);
table.ForeignKey(
name: "FK_EFServerSnapshot_EFMaps_MapId",
column: x => x.MapId,
principalTable: "EFMaps",
principalColumn: "MapId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_EFServerSnapshot_EFServers_ServerId",
column: x => x.ServerId,
principalTable: "EFServers",
principalColumn: "ServerId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_EFServerSnapshot_MapId",
table: "EFServerSnapshot",
column: "MapId");
migrationBuilder.CreateIndex(
name: "IX_EFServerSnapshot_ServerId",
table: "EFServerSnapshot",
column: "ServerId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "EFServerSnapshot");
}
}
}

View File

@ -1025,6 +1025,40 @@ namespace Data.Migrations.Postgresql
b.ToTable("EFServers");
});
modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b =>
{
b.Property<long>("ServerSnapshotId")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn);
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTime>("CapturedAt")
.HasColumnType("timestamp without time zone");
b.Property<int>("ClientCount")
.HasColumnType("integer");
b.Property<int>("MapId")
.HasColumnType("integer");
b.Property<int>("PeriodBlock")
.HasColumnType("integer");
b.Property<long>("ServerId")
.HasColumnType("bigint");
b.HasKey("ServerSnapshotId");
b.HasIndex("MapId");
b.HasIndex("ServerId");
b.ToTable("EFServerSnapshot");
});
modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b =>
{
b.Property<int>("StatisticId")
@ -1365,6 +1399,21 @@ namespace Data.Migrations.Postgresql
.IsRequired();
});
modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b =>
{
b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map")
.WithMany()
.HasForeignKey("MapId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Data.Models.Server.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b =>
{
b.HasOne("Data.Models.Server.EFServer", "Server")

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,57 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.Sqlite
{
public partial class AddEFServerSnapshot : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "EFServerSnapshot",
columns: table => new
{
ServerSnapshotId = table.Column<long>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Active = table.Column<bool>(nullable: false),
CapturedAt = table.Column<DateTime>(nullable: false),
PeriodBlock = table.Column<int>(nullable: false),
ServerId = table.Column<long>(nullable: false),
MapId = table.Column<int>(nullable: false),
ClientCount = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_EFServerSnapshot", x => x.ServerSnapshotId);
table.ForeignKey(
name: "FK_EFServerSnapshot_EFMaps_MapId",
column: x => x.MapId,
principalTable: "EFMaps",
principalColumn: "MapId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_EFServerSnapshot_EFServers_ServerId",
column: x => x.ServerId,
principalTable: "EFServers",
principalColumn: "ServerId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_EFServerSnapshot_MapId",
table: "EFServerSnapshot",
column: "MapId");
migrationBuilder.CreateIndex(
name: "IX_EFServerSnapshot_ServerId",
table: "EFServerSnapshot",
column: "ServerId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "EFServerSnapshot");
}
}
}

View File

@ -1000,6 +1000,39 @@ namespace Data.Migrations.Sqlite
b.ToTable("EFServers");
});
modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b =>
{
b.Property<long>("ServerSnapshotId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<DateTime>("CapturedAt")
.HasColumnType("TEXT");
b.Property<int>("ClientCount")
.HasColumnType("INTEGER");
b.Property<int>("MapId")
.HasColumnType("INTEGER");
b.Property<int>("PeriodBlock")
.HasColumnType("INTEGER");
b.Property<long>("ServerId")
.HasColumnType("INTEGER");
b.HasKey("ServerSnapshotId");
b.HasIndex("MapId");
b.HasIndex("ServerId");
b.ToTable("EFServerSnapshot");
});
modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b =>
{
b.Property<int>("StatisticId")
@ -1338,6 +1371,21 @@ namespace Data.Migrations.Sqlite
.IsRequired();
});
modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b =>
{
b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map")
.WithMany()
.HasForeignKey("MapId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Data.Models.Server.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b =>
{
b.HasOne("Data.Models.Server.EFServer", "Server")

View File

@ -0,0 +1,36 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Data.Models.Client.Stats.Reference;
namespace Data.Models.Server
{
public class EFServerSnapshot : SharedEntity
{
[Key]
public long ServerSnapshotId { get; set; }
public DateTime CapturedAt { get; set; }
/// <summary>
/// Specifies at which time block during a period the snapshot occured
/// | 1:00 | 1:05 | 1:10 |
/// | 5 minutes | 5 minutes | 5 minutes |
/// | 0 | 1 | 2 |
/// </summary>
public int PeriodBlock { get; set; }
[Required]
public long ServerId { get; set; }
[ForeignKey(nameof(ServerId))]
public EFServer Server { get; set; }
public int MapId { get; set; }
[ForeignKey(nameof(MapId))]
public EFMap Map { get; set; }
public int ClientCount { get; set; }
}
}

View File

@ -10,7 +10,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.6.29.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.26.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.6.29.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.26.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -23,7 +23,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.6.29.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.26.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -19,7 +19,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.6.29.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.26.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -16,7 +16,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.6.29.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.26.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -17,7 +17,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.6.29.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.26.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -20,7 +20,7 @@
</Target>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.6.29.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.26.1" PrivateAssets="All" />
</ItemGroup>
</Project>

View File

@ -8,6 +8,7 @@ namespace SharedLibraryCore.Dtos
public int RecentClientCount { get; set; }
public int TotalOccupiedClientSlots { get; set; }
public int TotalAvailableClientSlots { get; set; }
public int MaxConcurrentClients { get; set; }
/// <summary>
/// specifies the game name filter

View File

@ -0,0 +1,17 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace SharedLibraryCore.Interfaces
{
public interface IServerDataCollector
{
/// <summary>
/// Begins to collection on servers for analytical purposes
/// </summary>
/// <param name="period">interval at which to collect data</param>
/// <param name="cancellationToken">Token</param>
/// <returns>Task</returns>
Task BeginCollectionAsync(TimeSpan? period = null, CancellationToken cancellationToken = default);
}
}

View File

@ -0,0 +1,29 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace SharedLibraryCore.Interfaces
{
/// <summary>
/// Exposes methods to get analytical data about server(s)
/// </summary>
public interface IServerDataViewer
{
/// <summary>
/// Retrieves the max concurrent clients over a give time period for all servers or given server id
/// </summary>
/// <param name="serverId">ServerId to query on</param>
/// <param name="overPeriod">how far in the past to search</param>
/// <param name="token">CancellationToken</param>
/// <returns></returns>
Task<int> MaxConcurrentClientsAsync(long? serverId = null, TimeSpan? overPeriod = null, CancellationToken token = default);
/// <summary>
/// Gets the total number of clients connected and total clients connected in the given time frame
/// </summary>
/// <param name="overPeriod">how far in the past to search</param>
/// <param name="token"></param>
/// <returns></returns>
Task<(int, int)> ClientCountsAsync(TimeSpan? overPeriod = null, CancellationToken token = default);
}
}

View File

@ -284,6 +284,8 @@ namespace SharedLibraryCore
}
}
public abstract Task<long> GetIdForServer(Server server = null);
// Objects
public IManager Manager { get; protected set; }
[Obsolete]

View File

@ -4,7 +4,7 @@
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
<Version>2021.6.29.1</Version>
<Version>2021.8.26.1</Version>
<Authors>RaidMax</Authors>
<Company>Forever None</Company>
<Configurations>Debug;Release;Prerelease</Configurations>
@ -19,7 +19,7 @@
<IsPackable>true</IsPackable>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Description>Shared Library for IW4MAdmin</Description>
<PackageVersion>2021.6.29.1</PackageVersion>
<PackageVersion>2021.8.26.1</PackageVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Prerelease|AnyCPU'">
@ -44,7 +44,7 @@
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.10" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="3.1.10" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="RaidMax.IW4MAdmin.Data" Version="1.0.4" />
<PackageReference Include="RaidMax.IW4MAdmin.Data" Version="1.0.5" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
<PackageReference Include="SimpleCrypto.NetCore" Version="1.0.0" />
</ItemGroup>

View File

@ -1,15 +1,11 @@
using System;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models.Client;
using Data.Models.Client.Stats;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using static SharedLibraryCore.Server;
using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -20,33 +16,25 @@ namespace WebfrontCore.Controllers
{
private readonly ITranslationLookup _translationLookup;
private readonly ILogger _logger;
private readonly IDataValueCache<EFClient, (int, int)> _serverStatsCache;
private const string ServerStatKey = nameof(ServerStatKey);
private readonly IServerDataViewer _serverDataViewer;
public HomeController(ILogger<HomeController> logger, IManager manager, ITranslationLookup translationLookup,
IDataValueCache<EFClient, (int, int)> serverStatsCache) : base(manager)
IServerDataViewer serverDataViewer) : base(manager)
{
_logger = logger;
_translationLookup = translationLookup;
_serverStatsCache = serverStatsCache;
_serverStatsCache.SetCacheItem(async set =>
{
var count = await set.CountAsync();
var startOfPeriod = DateTime.UtcNow.AddHours(-24);
var recentCount = await set.CountAsync(client => client.LastConnection >= startOfPeriod);
return (count, recentCount);
}, ServerStatKey);
_serverDataViewer = serverDataViewer;
}
public async Task<IActionResult> Index(Game? game = null)
public async Task<IActionResult> Index(Game? game = null, CancellationToken cancellationToken = default)
{
ViewBag.Description = Localization["WEBFRONT_DESCRIPTION_HOME"];
ViewBag.Title = Localization["WEBFRONT_HOME_TITLE"];
ViewBag.Keywords = Localization["WEBFRONT_KEWORDS_HOME"];
var servers = Manager.GetServers().Where(_server => !game.HasValue || _server.GameName == game);
var (count, recentCount) = await _serverStatsCache.GetCacheItem(ServerStatKey);
var maxConcurrentClients = await _serverDataViewer.MaxConcurrentClientsAsync(token: cancellationToken);
var (count, recentCount) = await _serverDataViewer.ClientCountsAsync(token: cancellationToken);
var model = new IW4MAdminInfo()
{
@ -54,6 +42,7 @@ namespace WebfrontCore.Controllers
TotalOccupiedClientSlots = servers.SelectMany(_server => _server.GetClientsAsList()).Count(),
TotalClientCount = count,
RecentClientCount = recentCount,
MaxConcurrentClients = maxConcurrentClients,
Game = game,
ActiveServerGames = Manager.GetServers().Select(_server => _server.GameName).Distinct().ToArray()
};

View File

@ -129,6 +129,7 @@ namespace WebfrontCore
.GetRequiredService<IConfigurationHandler<DefaultSettings>>());
services.AddSingleton(Program.ApplicationServiceProvider
.GetRequiredService<IConfigurationHandler<StatsConfiguration>>());
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IServerDataViewer>());
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

View File

@ -8,13 +8,16 @@
}
}
<div class="row mb-4 border-bottom border-top pt-3 pb-3 bg-dark">
<div class="col-xl-4 col-12">
<div class="col-xl-3 col-12">
<div class="text-muted text-center text-xl-left">@Html.Raw(formatTranslation("WEBFRONT_HOME_CLIENTS_ONLINE", Model.TotalOccupiedClientSlots, Model.TotalAvailableClientSlots))</div>
</div>
<div class="col-xl-4 col-12">
<div class="col-xl-3 col-12">
<div class="text-muted text-center text-xl-left">@Html.Raw(formatTranslation("WEBFRONT_HOME_MAX_CONCURRENT_CLIENTS", Model.MaxConcurrentClients.ToString("#,##0")))</div>
</div>
<div class="col-xl-3 col-12">
<div class="text-muted text-center">@Html.Raw(formatTranslation("WEBFRONT_HOME_RECENT_CLIENTS", Model.RecentClientCount.ToString("#,##0")))</div>
</div>
<div class="col-xl-4 col-12">
<div class="col-xl-3 col-12">
<div class="text-muted text-center text-xl-right">@Html.Raw(formatTranslation("WEBFRONT_HOME_TOTAL_CLIENTS", Model.TotalClientCount.ToString("#,##0")))</div>
</div>
</div>

View File

@ -97,6 +97,6 @@
</Target>
<Target Name="MyPreCompileTarget" BeforeTargets="Build">
<Exec Command="dotnet bundle" />
<!--<Exec Command="dotnet bundle" />-->
</Target>
</Project>