|
1 | 1 | using Microsoft.Extensions.Logging; |
| 2 | +using System.Diagnostics; |
2 | 3 | using MyApp.Data; |
3 | 4 | using MyApp.ServiceModel; |
4 | 5 | using ServiceStack; |
@@ -1001,4 +1002,136 @@ void ResizeGeneration(List<WorkflowGeneration> generations) |
1001 | 1002 | } |
1002 | 1003 | } |
1003 | 1004 | } |
| 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 | + } |
1004 | 1137 | } |
0 commit comments