Skip to content

Commit f52c3ca

Browse files
committed
Add AppInfo
1 parent 02238f2 commit f52c3ca

File tree

3 files changed

+184
-0
lines changed

3 files changed

+184
-0
lines changed

MyApp.ServiceInterface/AdminServices.cs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Microsoft.Extensions.Logging;
2+
using System.Diagnostics;
23
using MyApp.Data;
34
using MyApp.ServiceModel;
45
using ServiceStack;
@@ -1001,4 +1002,136 @@ void ResizeGeneration(List<WorkflowGeneration> generations)
10011002
}
10021003
}
10031004
}
1005+
1006+
public object Any(MyApp.ServiceModel.AppInfo request)
1007+
{
1008+
var proc = Process.GetCurrentProcess();
1009+
var nowUtc = DateTime.UtcNow;
1010+
var startUtc = proc.StartTime.ToUniversalTime();
1011+
var uptime = nowUtc - startUtc;
1012+
1013+
var totalCpu = proc.TotalProcessorTime;
1014+
var userCpu = proc.UserProcessorTime;
1015+
var cpuApprox = uptime.TotalMilliseconds > 0
1016+
? Math.Clamp(totalCpu.TotalMilliseconds / (uptime.TotalMilliseconds * Environment.ProcessorCount) * 100.0, 0, 100)
1017+
: 0;
1018+
1019+
var threads = new List<ThreadInfo>();
1020+
try
1021+
{
1022+
foreach (ProcessThread t in proc.Threads)
1023+
{
1024+
threads.Add(new ThreadInfo
1025+
{
1026+
ManagedThreadId = t.Id, // OS thread id, used here as identifier
1027+
Name = null,
1028+
State = t.ThreadState.ToString(),
1029+
TotalProcessorTime = SafeGet(() => t.TotalProcessorTime, TimeSpan.Zero),
1030+
});
1031+
}
1032+
}
1033+
catch { /* best-effort */ }
1034+
1035+
// Order threads by CPU time and cap list size
1036+
threads = threads
1037+
.OrderByDescending(x => x.TotalProcessorTime)
1038+
.Take(64)
1039+
.ToList();
1040+
1041+
// PostgreSQL connection stats
1042+
int total = 0, active = 0, idle = 0, idleTxn = 0, waiting = 0;
1043+
int? maxPool = null, minPool = null; bool? pooling = null;
1044+
try
1045+
{
1046+
var stats = Db.Single<(int Total, int Active, int Idle, int IdleInTxn, int Waiting)>(
1047+
@"select count(*) as Total,
1048+
count(*) filter (where state = 'active') as Active,
1049+
count(*) filter (where state = 'idle') as Idle,
1050+
count(*) filter (where state = 'idle in transaction') as IdleInTxn,
1051+
count(*) filter (where wait_event is not null) as Waiting
1052+
from pg_stat_activity
1053+
where datname = current_database();");
1054+
total = stats.Total; active = stats.Active; idle = stats.Idle; idleTxn = stats.IdleInTxn; waiting = stats.Waiting;
1055+
}
1056+
catch (Exception e)
1057+
{
1058+
log.LogDebug(e, "Failed to query pg_stat_activity");
1059+
}
1060+
1061+
try
1062+
{
1063+
var cs = Db.ConnectionString;
1064+
if (!string.IsNullOrEmpty(cs))
1065+
{
1066+
var dict = new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);
1067+
foreach (var part in cs.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
1068+
{
1069+
var kvp = part.Split('=', 2);
1070+
if (kvp.Length == 2)
1071+
dict[kvp[0].Trim()] = kvp[1].Trim();
1072+
}
1073+
1074+
int tryInt(string key)
1075+
=> dict.TryGetValue(key, out var s) && int.TryParse(s, out var i) ? i : 0;
1076+
bool? tryBool(string key)
1077+
=> dict.TryGetValue(key, out var s) && bool.TryParse(s, out var b) ? b : null;
1078+
1079+
// Handle common key variants
1080+
maxPool =
1081+
(dict.TryGetValue("Max Pool Size", out var mps) && int.TryParse(mps, out var i1)) ? i1 :
1082+
(dict.TryGetValue("Maximum Pool Size", out var mps2) && int.TryParse(mps2, out var i2)) ? i2 :
1083+
(dict.TryGetValue("MaxPoolSize", out var mps3) && int.TryParse(mps3, out var i3)) ? i3 : (int?)null;
1084+
1085+
minPool =
1086+
(dict.TryGetValue("Min Pool Size", out var mins) && int.TryParse(mins, out var mi1)) ? mi1 :
1087+
(dict.TryGetValue("Minimum Pool Size", out var mins2) && int.TryParse(mins2, out var mi2)) ? mi2 :
1088+
(dict.TryGetValue("MinPoolSize", out var mins3) && int.TryParse(mins3, out var mi3)) ? mi3 : (int?)null;
1089+
1090+
pooling =
1091+
(dict.TryGetValue("Pooling", out var pol) && bool.TryParse(pol, out var pb)) ? pb : (bool?)null;
1092+
}
1093+
}
1094+
catch (Exception e)
1095+
{
1096+
log.LogDebug(e, "Failed to parse connection string for pool settings");
1097+
}
1098+
1099+
var response = new AppInfoResponse
1100+
{
1101+
ProcessId = proc.Id,
1102+
StartTime = startUtc,
1103+
Uptime = uptime,
1104+
WorkingSetBytes = proc.WorkingSet64,
1105+
PrivateMemoryBytes = proc.PrivateMemorySize64,
1106+
ManagedMemoryBytes = GC.GetTotalMemory(false),
1107+
GcTotalAllocatedBytes = GC.GetTotalAllocatedBytes(precise: false),
1108+
GcGen0Collections = GC.CollectionCount(0),
1109+
GcGen1Collections = GC.CollectionCount(1),
1110+
GcGen2Collections = GC.CollectionCount(2),
1111+
1112+
TotalProcessorTime = totalCpu,
1113+
UserProcessorTime = userCpu,
1114+
CpuUsagePercentApprox = cpuApprox,
1115+
1116+
ThreadCount = proc.Threads.Count,
1117+
Threads = threads,
1118+
1119+
PgTotalConnections = total,
1120+
PgActiveConnections = active,
1121+
PgIdleConnections = idle,
1122+
PgIdleInTransaction = idleTxn,
1123+
PgWaitingConnections = waiting,
1124+
1125+
PoolMaxSize = maxPool,
1126+
PoolMinSize = minPool,
1127+
Pooling = pooling,
1128+
};
1129+
1130+
return response;
1131+
1132+
static T SafeGet<T>(Func<T> fn, T fallback)
1133+
{
1134+
try { return fn(); } catch { return fallback; }
1135+
}
1136+
}
10041137
}

MyApp.ServiceInterface/MyApp.ServiceInterface.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10+
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.*" />
1011
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="10.*" />
1112
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.*" />
1213
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.*" />

MyApp.ServiceModel/Admin.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,57 @@ public class GetAppDataResponse
2222
public ResponseStatus? ResponseStatus { get; set; }
2323
}
2424

25+
[Route("/appinfo")]
26+
[Tag(Tags.Admin)]
27+
[ValidateIsAdmin]
28+
public class AppInfo : IGet, IReturn<AppInfoResponse>
29+
{
30+
}
31+
public class AppInfoResponse
32+
{
33+
// Process / memory
34+
public int ProcessId { get; set; }
35+
public DateTime StartTime { get; set; }
36+
public TimeSpan Uptime { get; set; }
37+
public long WorkingSetBytes { get; set; }
38+
public long PrivateMemoryBytes { get; set; }
39+
public long ManagedMemoryBytes { get; set; }
40+
public long GcTotalAllocatedBytes { get; set; }
41+
public int GcGen0Collections { get; set; }
42+
public int GcGen1Collections { get; set; }
43+
public int GcGen2Collections { get; set; }
44+
45+
// CPU
46+
public TimeSpan TotalProcessorTime { get; set; }
47+
public TimeSpan UserProcessorTime { get; set; }
48+
public double CpuUsagePercentApprox { get; set; }
49+
50+
// Threads
51+
public int ThreadCount { get; set; }
52+
public List<ThreadInfo> Threads { get; set; } = [];
2553

54+
// Npgsql / DB connection stats (from pg_stat_activity)
55+
public int PgTotalConnections { get; set; }
56+
public int PgActiveConnections { get; set; }
57+
public int PgIdleConnections { get; set; }
58+
public int PgIdleInTransaction { get; set; }
59+
public int PgWaitingConnections { get; set; }
60+
61+
// Pool settings (parsed from connection string when available)
62+
public int? PoolMaxSize { get; set; }
63+
public int? PoolMinSize { get; set; }
64+
public bool? Pooling { get; set; }
65+
66+
public ResponseStatus? ResponseStatus { get; set; }
67+
}
68+
69+
public class ThreadInfo
70+
{
71+
public int ManagedThreadId { get; set; }
72+
public string? Name { get; set; }
73+
public string State { get; set; }
74+
public TimeSpan TotalProcessorTime { get; set; }
75+
}
2676

2777
[Tag(Tags.Admin)]
2878
[ValidateIsAdmin]

0 commit comments

Comments
 (0)