@oneuptime/common 11.0.1 → 11.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/Models/DatabaseModels/Alert.ts +110 -0
  2. package/Models/DatabaseModels/CephCluster.ts +964 -0
  3. package/Models/DatabaseModels/CephClusterLabelRule.ts +514 -0
  4. package/Models/DatabaseModels/CephClusterOwnerRule.ts +596 -0
  5. package/Models/DatabaseModels/CephClusterOwnerTeam.ts +487 -0
  6. package/Models/DatabaseModels/CephClusterOwnerUser.ts +486 -0
  7. package/Models/DatabaseModels/CephResource.ts +809 -0
  8. package/Models/DatabaseModels/Host.ts +64 -0
  9. package/Models/DatabaseModels/Incident.ts +110 -0
  10. package/Models/DatabaseModels/Index.ts +24 -0
  11. package/Models/DatabaseModels/ProxmoxCluster.ts +943 -0
  12. package/Models/DatabaseModels/ProxmoxClusterLabelRule.ts +514 -0
  13. package/Models/DatabaseModels/ProxmoxClusterOwnerRule.ts +596 -0
  14. package/Models/DatabaseModels/ProxmoxClusterOwnerTeam.ts +487 -0
  15. package/Models/DatabaseModels/ProxmoxClusterOwnerUser.ts +486 -0
  16. package/Models/DatabaseModels/ProxmoxResource.ts +726 -0
  17. package/Models/DatabaseModels/ScheduledMaintenance.ts +110 -0
  18. package/Server/API/BillingInvoiceAPI.ts +47 -7
  19. package/Server/API/CephResourceAPI.ts +134 -0
  20. package/Server/API/DashboardAPI.ts +46 -0
  21. package/Server/API/ProjectAPI.ts +15 -0
  22. package/Server/API/ProxmoxResourceAPI.ts +132 -0
  23. package/Server/API/ResellerPlanAPI.ts +17 -0
  24. package/Server/Infrastructure/GlobalCache.ts +8 -2
  25. package/Server/Infrastructure/Postgres/SchemaMigrations/1781500000000-AddProxmoxAndCephClusterTables.ts +163 -0
  26. package/Server/Infrastructure/Postgres/SchemaMigrations/1781600000000-AddProxmoxCephV2Columns.ts +211 -0
  27. package/Server/Infrastructure/Postgres/SchemaMigrations/1781600000001-AddProxmoxCephActivityAndRules.ts +590 -0
  28. package/Server/Infrastructure/Postgres/SchemaMigrations/1781700000000-AddProxmoxCephV3Columns.ts +64 -0
  29. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  30. package/Server/Infrastructure/Redis.ts +40 -12
  31. package/Server/Services/AnalyticsDatabaseService.ts +1 -1
  32. package/Server/Services/BillingService.ts +109 -21
  33. package/Server/Services/CephClusterLabelRuleEngineService.ts +200 -0
  34. package/Server/Services/CephClusterLabelRuleService.ts +14 -0
  35. package/Server/Services/CephClusterOwnerRuleEngineService.ts +218 -0
  36. package/Server/Services/CephClusterOwnerRuleService.ts +14 -0
  37. package/Server/Services/CephClusterOwnerTeamService.ts +10 -0
  38. package/Server/Services/CephClusterOwnerUserService.ts +10 -0
  39. package/Server/Services/CephClusterService.ts +401 -0
  40. package/Server/Services/CephResourceService.ts +383 -0
  41. package/Server/Services/CloudResourceService.ts +11 -3
  42. package/Server/Services/DockerHostService.ts +11 -3
  43. package/Server/Services/ExceptionAggregationService.ts +2 -0
  44. package/Server/Services/HostService.ts +11 -3
  45. package/Server/Services/Index.ts +24 -0
  46. package/Server/Services/KubernetesClusterService.ts +11 -3
  47. package/Server/Services/LogAggregationService.ts +2 -0
  48. package/Server/Services/MetricAggregationService.ts +2 -0
  49. package/Server/Services/OpenTelemetryIngestService.ts +36 -0
  50. package/Server/Services/ProxmoxClusterLabelRuleEngineService.ts +204 -0
  51. package/Server/Services/ProxmoxClusterLabelRuleService.ts +14 -0
  52. package/Server/Services/ProxmoxClusterOwnerRuleEngineService.ts +222 -0
  53. package/Server/Services/ProxmoxClusterOwnerRuleService.ts +14 -0
  54. package/Server/Services/ProxmoxClusterOwnerTeamService.ts +10 -0
  55. package/Server/Services/ProxmoxClusterOwnerUserService.ts +10 -0
  56. package/Server/Services/ProxmoxClusterService.ts +382 -0
  57. package/Server/Services/ProxmoxResourceService.ts +404 -0
  58. package/Server/Services/RumApplicationService.ts +11 -3
  59. package/Server/Services/ServerlessFunctionService.ts +11 -3
  60. package/Server/Services/TelemetryUsageBillingService.ts +41 -3
  61. package/Server/Services/TraceAggregationService.ts +2 -0
  62. package/Server/Types/AnalyticsDatabase/AggregateBy.ts +8 -23
  63. package/Server/Utils/Monitor/MonitorAlert.ts +45 -0
  64. package/Server/Utils/Monitor/MonitorClusterContext.ts +129 -0
  65. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +344 -4
  66. package/Server/Utils/Monitor/MonitorIncident.ts +130 -7
  67. package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +39 -6
  68. package/Server/Utils/Monitor/MonitorTemplateUtil.ts +3 -1
  69. package/Server/Utils/Monitor/SeriesResourceLabels.ts +33 -0
  70. package/Server/Utils/Profiling.ts +37 -2
  71. package/Server/Utils/Telemetry/EntityRegistry.ts +4 -0
  72. package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +1096 -0
  73. package/Server/Utils/Telemetry/TelemetryEntity.ts +85 -0
  74. package/Server/Utils/Telemetry.ts +8 -19
  75. package/Tests/Server/API/BillingInvoiceAPI.test.ts +194 -0
  76. package/Tests/Server/API/ProjectAPI.test.ts +91 -0
  77. package/Tests/Server/API/ResellerPlanAPI.test.ts +207 -0
  78. package/Tests/Server/Infrastructure/GlobalCache.test.ts +100 -0
  79. package/Tests/Server/Services/BillingService.test.ts +323 -0
  80. package/Tests/Server/Services/CephResourceService.test.ts +264 -0
  81. package/Tests/Server/Services/ProxmoxResourceService.test.ts +326 -0
  82. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +322 -0
  83. package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +13 -0
  84. package/Tests/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.test.ts +879 -0
  85. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +196 -0
  86. package/Tests/Types/Monitor/CephAlertTemplates.test.ts +1231 -0
  87. package/Tests/Types/Monitor/ProxmoxAlertTemplates.test.ts +732 -0
  88. package/Tests/Utils/Telemetry/EntityRelationship.test.ts +49 -0
  89. package/Tests/Utils/Telemetry/HeartbeatAvailability.test.ts +423 -0
  90. package/Types/BaseDatabase/AggregationIntervalUtil.ts +74 -0
  91. package/Types/Dashboard/DashboardComponentType.ts +4 -0
  92. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +2 -0
  93. package/Types/Dashboard/DashboardComponents/DashboardCephOsdListComponent.ts +15 -0
  94. package/Types/Dashboard/DashboardComponents/DashboardCephPoolListComponent.ts +14 -0
  95. package/Types/Dashboard/DashboardComponents/DashboardProxmoxGuestListComponent.ts +17 -0
  96. package/Types/Dashboard/DashboardComponents/DashboardProxmoxNodeListComponent.ts +16 -0
  97. package/Types/Dashboard/DashboardTemplates.ts +446 -0
  98. package/Types/Icon/IconProp.ts +2 -0
  99. package/Types/Monitor/CephAlertTemplates.ts +1647 -0
  100. package/Types/Monitor/CephMetricCatalog.ts +409 -0
  101. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +44 -0
  102. package/Types/Monitor/MonitorStep.ts +64 -0
  103. package/Types/Monitor/MonitorStepCephMonitor.ts +57 -0
  104. package/Types/Monitor/MonitorStepProxmoxMonitor.ts +81 -0
  105. package/Types/Monitor/MonitorType.ts +29 -1
  106. package/Types/Monitor/ProxmoxAlertTemplates.ts +899 -0
  107. package/Types/Monitor/ProxmoxMetricCatalog.ts +382 -0
  108. package/Types/Permission.ts +464 -0
  109. package/Types/Telemetry/EntityType.ts +11 -0
  110. package/Types/Telemetry/ServiceType.ts +2 -0
  111. package/UI/Components/Icon/Icon.tsx +84 -0
  112. package/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.ts +9 -5
  113. package/UI/Utils/Telemetry/Telemetry.ts +16 -21
  114. package/UI/Utils/TelemetryService.ts +7 -3
  115. package/Utils/Dashboard/Components/DashboardCephOsdListComponent.ts +63 -0
  116. package/Utils/Dashboard/Components/DashboardCephPoolListComponent.ts +32 -0
  117. package/Utils/Dashboard/Components/DashboardCephResourceListShared.ts +61 -0
  118. package/Utils/Dashboard/Components/DashboardProxmoxGuestListComponent.ts +69 -0
  119. package/Utils/Dashboard/Components/DashboardProxmoxNodeListComponent.ts +55 -0
  120. package/Utils/Dashboard/Components/DashboardProxmoxResourceListShared.ts +61 -0
  121. package/Utils/Dashboard/Components/Index.ts +28 -0
  122. package/Utils/Telemetry/EntityKey.ts +35 -0
  123. package/Utils/Telemetry/EntityRelationship.ts +6 -0
  124. package/Utils/Telemetry/HeartbeatAvailability.ts +262 -0
  125. package/build/dist/Models/DatabaseModels/Alert.js +108 -0
  126. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  127. package/build/dist/Models/DatabaseModels/CephCluster.js +992 -0
  128. package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -0
  129. package/build/dist/Models/DatabaseModels/CephClusterLabelRule.js +522 -0
  130. package/build/dist/Models/DatabaseModels/CephClusterLabelRule.js.map +1 -0
  131. package/build/dist/Models/DatabaseModels/CephClusterOwnerRule.js +603 -0
  132. package/build/dist/Models/DatabaseModels/CephClusterOwnerRule.js.map +1 -0
  133. package/build/dist/Models/DatabaseModels/CephClusterOwnerTeam.js +503 -0
  134. package/build/dist/Models/DatabaseModels/CephClusterOwnerTeam.js.map +1 -0
  135. package/build/dist/Models/DatabaseModels/CephClusterOwnerUser.js +502 -0
  136. package/build/dist/Models/DatabaseModels/CephClusterOwnerUser.js.map +1 -0
  137. package/build/dist/Models/DatabaseModels/CephResource.js +846 -0
  138. package/build/dist/Models/DatabaseModels/CephResource.js.map +1 -0
  139. package/build/dist/Models/DatabaseModels/Host.js +63 -0
  140. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  141. package/build/dist/Models/DatabaseModels/Incident.js +108 -0
  142. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  143. package/build/dist/Models/DatabaseModels/Index.js +24 -0
  144. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  145. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +967 -0
  146. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -0
  147. package/build/dist/Models/DatabaseModels/ProxmoxClusterLabelRule.js +522 -0
  148. package/build/dist/Models/DatabaseModels/ProxmoxClusterLabelRule.js.map +1 -0
  149. package/build/dist/Models/DatabaseModels/ProxmoxClusterOwnerRule.js +603 -0
  150. package/build/dist/Models/DatabaseModels/ProxmoxClusterOwnerRule.js.map +1 -0
  151. package/build/dist/Models/DatabaseModels/ProxmoxClusterOwnerTeam.js +503 -0
  152. package/build/dist/Models/DatabaseModels/ProxmoxClusterOwnerTeam.js.map +1 -0
  153. package/build/dist/Models/DatabaseModels/ProxmoxClusterOwnerUser.js +502 -0
  154. package/build/dist/Models/DatabaseModels/ProxmoxClusterOwnerUser.js.map +1 -0
  155. package/build/dist/Models/DatabaseModels/ProxmoxResource.js +761 -0
  156. package/build/dist/Models/DatabaseModels/ProxmoxResource.js.map +1 -0
  157. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +108 -0
  158. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  159. package/build/dist/Server/API/BillingInvoiceAPI.js +35 -5
  160. package/build/dist/Server/API/BillingInvoiceAPI.js.map +1 -1
  161. package/build/dist/Server/API/CephResourceAPI.js +98 -0
  162. package/build/dist/Server/API/CephResourceAPI.js.map +1 -0
  163. package/build/dist/Server/API/DashboardAPI.js +46 -0
  164. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  165. package/build/dist/Server/API/ProjectAPI.js +11 -0
  166. package/build/dist/Server/API/ProjectAPI.js.map +1 -1
  167. package/build/dist/Server/API/ProxmoxResourceAPI.js +95 -0
  168. package/build/dist/Server/API/ProxmoxResourceAPI.js.map +1 -0
  169. package/build/dist/Server/API/ResellerPlanAPI.js +17 -3
  170. package/build/dist/Server/API/ResellerPlanAPI.js.map +1 -1
  171. package/build/dist/Server/Infrastructure/GlobalCache.js +7 -2
  172. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  173. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1781500000000-AddProxmoxAndCephClusterTables.js +76 -0
  174. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1781500000000-AddProxmoxAndCephClusterTables.js.map +1 -0
  175. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1781600000000-AddProxmoxCephV2Columns.js +108 -0
  176. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1781600000000-AddProxmoxCephV2Columns.js.map +1 -0
  177. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1781600000001-AddProxmoxCephActivityAndRules.js +253 -0
  178. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1781600000001-AddProxmoxCephActivityAndRules.js.map +1 -0
  179. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1781700000000-AddProxmoxCephV3Columns.js +43 -0
  180. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1781700000000-AddProxmoxCephV3Columns.js.map +1 -0
  181. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  182. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  183. package/build/dist/Server/Infrastructure/Redis.js +31 -8
  184. package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
  185. package/build/dist/Server/Services/AnalyticsDatabaseService.js +1 -1
  186. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  187. package/build/dist/Server/Services/BillingService.js +85 -23
  188. package/build/dist/Server/Services/BillingService.js.map +1 -1
  189. package/build/dist/Server/Services/CephClusterLabelRuleEngineService.js +166 -0
  190. package/build/dist/Server/Services/CephClusterLabelRuleEngineService.js.map +1 -0
  191. package/build/dist/Server/Services/CephClusterLabelRuleService.js +13 -0
  192. package/build/dist/Server/Services/CephClusterLabelRuleService.js.map +1 -0
  193. package/build/dist/Server/Services/CephClusterOwnerRuleEngineService.js +186 -0
  194. package/build/dist/Server/Services/CephClusterOwnerRuleEngineService.js.map +1 -0
  195. package/build/dist/Server/Services/CephClusterOwnerRuleService.js +13 -0
  196. package/build/dist/Server/Services/CephClusterOwnerRuleService.js.map +1 -0
  197. package/build/dist/Server/Services/CephClusterOwnerTeamService.js +9 -0
  198. package/build/dist/Server/Services/CephClusterOwnerTeamService.js.map +1 -0
  199. package/build/dist/Server/Services/CephClusterOwnerUserService.js +9 -0
  200. package/build/dist/Server/Services/CephClusterOwnerUserService.js.map +1 -0
  201. package/build/dist/Server/Services/CephClusterService.js +353 -0
  202. package/build/dist/Server/Services/CephClusterService.js.map +1 -0
  203. package/build/dist/Server/Services/CephResourceService.js +257 -0
  204. package/build/dist/Server/Services/CephResourceService.js.map +1 -0
  205. package/build/dist/Server/Services/CloudResourceService.js +10 -2
  206. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  207. package/build/dist/Server/Services/DockerHostService.js +10 -2
  208. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  209. package/build/dist/Server/Services/ExceptionAggregationService.js +2 -0
  210. package/build/dist/Server/Services/ExceptionAggregationService.js.map +1 -1
  211. package/build/dist/Server/Services/HostService.js +10 -2
  212. package/build/dist/Server/Services/HostService.js.map +1 -1
  213. package/build/dist/Server/Services/Index.js +24 -0
  214. package/build/dist/Server/Services/Index.js.map +1 -1
  215. package/build/dist/Server/Services/KubernetesClusterService.js +10 -2
  216. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  217. package/build/dist/Server/Services/LogAggregationService.js +2 -0
  218. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  219. package/build/dist/Server/Services/MetricAggregationService.js +2 -0
  220. package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
  221. package/build/dist/Server/Services/OpenTelemetryIngestService.js +37 -7
  222. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  223. package/build/dist/Server/Services/ProxmoxClusterLabelRuleEngineService.js +166 -0
  224. package/build/dist/Server/Services/ProxmoxClusterLabelRuleEngineService.js.map +1 -0
  225. package/build/dist/Server/Services/ProxmoxClusterLabelRuleService.js +13 -0
  226. package/build/dist/Server/Services/ProxmoxClusterLabelRuleService.js.map +1 -0
  227. package/build/dist/Server/Services/ProxmoxClusterOwnerRuleEngineService.js +186 -0
  228. package/build/dist/Server/Services/ProxmoxClusterOwnerRuleEngineService.js.map +1 -0
  229. package/build/dist/Server/Services/ProxmoxClusterOwnerRuleService.js +13 -0
  230. package/build/dist/Server/Services/ProxmoxClusterOwnerRuleService.js.map +1 -0
  231. package/build/dist/Server/Services/ProxmoxClusterOwnerTeamService.js +9 -0
  232. package/build/dist/Server/Services/ProxmoxClusterOwnerTeamService.js.map +1 -0
  233. package/build/dist/Server/Services/ProxmoxClusterOwnerUserService.js +9 -0
  234. package/build/dist/Server/Services/ProxmoxClusterOwnerUserService.js.map +1 -0
  235. package/build/dist/Server/Services/ProxmoxClusterService.js +337 -0
  236. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -0
  237. package/build/dist/Server/Services/ProxmoxResourceService.js +285 -0
  238. package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -0
  239. package/build/dist/Server/Services/RumApplicationService.js +10 -2
  240. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  241. package/build/dist/Server/Services/ServerlessFunctionService.js +10 -2
  242. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  243. package/build/dist/Server/Services/TelemetryUsageBillingService.js +30 -3
  244. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  245. package/build/dist/Server/Services/TraceAggregationService.js +2 -0
  246. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
  247. package/build/dist/Server/Types/AnalyticsDatabase/AggregateBy.js +8 -25
  248. package/build/dist/Server/Types/AnalyticsDatabase/AggregateBy.js.map +1 -1
  249. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +36 -0
  250. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  251. package/build/dist/Server/Utils/Monitor/MonitorClusterContext.js +90 -0
  252. package/build/dist/Server/Utils/Monitor/MonitorClusterContext.js.map +1 -0
  253. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +228 -4
  254. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  255. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +103 -8
  256. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  257. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +23 -6
  258. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
  259. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +3 -1
  260. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
  261. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +23 -0
  262. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
  263. package/build/dist/Server/Utils/Profiling.js +24 -3
  264. package/build/dist/Server/Utils/Profiling.js.map +1 -1
  265. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +4 -0
  266. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  267. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +854 -0
  268. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -0
  269. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +62 -0
  270. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  271. package/build/dist/Server/Utils/Telemetry.js +8 -10
  272. package/build/dist/Server/Utils/Telemetry.js.map +1 -1
  273. package/build/dist/Types/BaseDatabase/AggregationIntervalUtil.js +69 -0
  274. package/build/dist/Types/BaseDatabase/AggregationIntervalUtil.js.map +1 -0
  275. package/build/dist/Types/Dashboard/DashboardComponentType.js +4 -0
  276. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  277. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +2 -0
  278. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  279. package/build/dist/Types/Dashboard/DashboardComponents/DashboardCephOsdListComponent.js +2 -0
  280. package/build/dist/Types/Dashboard/DashboardComponents/DashboardCephOsdListComponent.js.map +1 -0
  281. package/build/dist/Types/Dashboard/DashboardComponents/DashboardCephPoolListComponent.js +2 -0
  282. package/build/dist/Types/Dashboard/DashboardComponents/DashboardCephPoolListComponent.js.map +1 -0
  283. package/build/dist/Types/Dashboard/DashboardComponents/DashboardProxmoxGuestListComponent.js +2 -0
  284. package/build/dist/Types/Dashboard/DashboardComponents/DashboardProxmoxGuestListComponent.js.map +1 -0
  285. package/build/dist/Types/Dashboard/DashboardComponents/DashboardProxmoxNodeListComponent.js +2 -0
  286. package/build/dist/Types/Dashboard/DashboardComponents/DashboardProxmoxNodeListComponent.js.map +1 -0
  287. package/build/dist/Types/Dashboard/DashboardTemplates.js +394 -0
  288. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  289. package/build/dist/Types/Icon/IconProp.js +2 -0
  290. package/build/dist/Types/Icon/IconProp.js.map +1 -1
  291. package/build/dist/Types/Monitor/CephAlertTemplates.js +1379 -0
  292. package/build/dist/Types/Monitor/CephAlertTemplates.js.map +1 -0
  293. package/build/dist/Types/Monitor/CephMetricCatalog.js +353 -0
  294. package/build/dist/Types/Monitor/CephMetricCatalog.js.map +1 -0
  295. package/build/dist/Types/Monitor/MonitorStep.js +46 -0
  296. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  297. package/build/dist/Types/Monitor/MonitorStepCephMonitor.js +34 -0
  298. package/build/dist/Types/Monitor/MonitorStepCephMonitor.js.map +1 -0
  299. package/build/dist/Types/Monitor/MonitorStepProxmoxMonitor.js +36 -0
  300. package/build/dist/Types/Monitor/MonitorStepProxmoxMonitor.js.map +1 -0
  301. package/build/dist/Types/Monitor/MonitorType.js +27 -1
  302. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  303. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +743 -0
  304. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js.map +1 -0
  305. package/build/dist/Types/Monitor/ProxmoxMetricCatalog.js +320 -0
  306. package/build/dist/Types/Monitor/ProxmoxMetricCatalog.js.map +1 -0
  307. package/build/dist/Types/Permission.js +408 -0
  308. package/build/dist/Types/Permission.js.map +1 -1
  309. package/build/dist/Types/Telemetry/EntityType.js +11 -0
  310. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  311. package/build/dist/Types/Telemetry/ServiceType.js +2 -0
  312. package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
  313. package/build/dist/UI/Components/Icon/Icon.js +33 -0
  314. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  315. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js +5 -1
  316. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js.map +1 -1
  317. package/build/dist/UI/Utils/Telemetry/Telemetry.js +11 -10
  318. package/build/dist/UI/Utils/Telemetry/Telemetry.js.map +1 -1
  319. package/build/dist/UI/Utils/TelemetryService.js +5 -2
  320. package/build/dist/UI/Utils/TelemetryService.js.map +1 -1
  321. package/build/dist/Utils/Dashboard/Components/DashboardCephOsdListComponent.js +50 -0
  322. package/build/dist/Utils/Dashboard/Components/DashboardCephOsdListComponent.js.map +1 -0
  323. package/build/dist/Utils/Dashboard/Components/DashboardCephPoolListComponent.js +27 -0
  324. package/build/dist/Utils/Dashboard/Components/DashboardCephPoolListComponent.js.map +1 -0
  325. package/build/dist/Utils/Dashboard/Components/DashboardCephResourceListShared.js +46 -0
  326. package/build/dist/Utils/Dashboard/Components/DashboardCephResourceListShared.js.map +1 -0
  327. package/build/dist/Utils/Dashboard/Components/DashboardProxmoxGuestListComponent.js +55 -0
  328. package/build/dist/Utils/Dashboard/Components/DashboardProxmoxGuestListComponent.js.map +1 -0
  329. package/build/dist/Utils/Dashboard/Components/DashboardProxmoxNodeListComponent.js +42 -0
  330. package/build/dist/Utils/Dashboard/Components/DashboardProxmoxNodeListComponent.js.map +1 -0
  331. package/build/dist/Utils/Dashboard/Components/DashboardProxmoxResourceListShared.js +46 -0
  332. package/build/dist/Utils/Dashboard/Components/DashboardProxmoxResourceListShared.js.map +1 -0
  333. package/build/dist/Utils/Dashboard/Components/Index.js +16 -0
  334. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  335. package/build/dist/Utils/Telemetry/EntityKey.js +27 -0
  336. package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
  337. package/build/dist/Utils/Telemetry/EntityRelationship.js +3 -0
  338. package/build/dist/Utils/Telemetry/EntityRelationship.js.map +1 -1
  339. package/build/dist/Utils/Telemetry/HeartbeatAvailability.js +174 -0
  340. package/build/dist/Utils/Telemetry/HeartbeatAvailability.js.map +1 -0
  341. package/package.json +29 -21
@@ -0,0 +1,1096 @@
1
+ import OneUptimeDate from "../../../Types/Date";
2
+ import { JSONArray, JSONObject, JSONValue } from "../../../Types/JSON";
3
+ import logger from "../Logger";
4
+
5
+ /*
6
+ * ------------------------------------------------------------------
7
+ * Proxmox / Ceph snapshot scan — pure fold & derive helpers
8
+ * ------------------------------------------------------------------
9
+ *
10
+ * Extracted from App/FeatureSet/Telemetry/Services/
11
+ * OtelMetricsIngestService.ts (WI-21 refactor-for-testability, no
12
+ * behavior change). The ingest service owns the I/O: it walks the
13
+ * OTLP payload, calls bufferProxmoxSnapshotMetric /
14
+ * bufferCephSnapshotMetric per datapoint, and at flush time maps the
15
+ * folded buffers through computeProxmoxGuestBackedUp /
16
+ * deriveProxmoxClusterSnapshotExtras / deriveCephClusterSnapshotExtras
17
+ * before handing the results to ProxmoxResourceService /
18
+ * CephResourceService / *ClusterService. Everything in this module is
19
+ * pure (Map/object mutation only — no DB, no network), which is what
20
+ * makes the snapshot-scan semantics unit-testable:
21
+ *
22
+ * - identity labels fold first-non-null-wins; status/metric fields
23
+ * fold newest-observedAt-wins,
24
+ * - count columns are only derived when the batch carried the
25
+ * matching identity series (never zero a count on a partial
26
+ * batch — the COALESCE-per-column contract),
27
+ * - non-allow-listed metric names are skipped via the exported
28
+ * PVE_/CEPH_SNAPSHOT_METRIC_NAMES sets.
29
+ */
30
+
31
+ /*
32
+ * Proxmox snapshot metrics — emitted by pve-exporter (prometheus
33
+ * receiver), identity in the `id` datapoint label (node/pve1,
34
+ * qemu/100, lxc/101, storage/local) plus the pve.scope / pve.type /
35
+ * pve.id attributes the agent's transform processor derives from it.
36
+ * Unlike K8s there is no separate object stream: identity, status AND
37
+ * the latest-metric mirror all arrive on every scrape, so the same
38
+ * scan feeds the ProxmoxResource inventory upsert and the
39
+ * ProxmoxCluster count/version snapshot columns (single source — the
40
+ * list-page counts and the sidebar badges can never drift).
41
+ *
42
+ * pve_cpu_usage_ratio is already a true 0..1 ratio — no allocatable-
43
+ * denominator cache is needed, unlike K8s cpuCoresToPercent.
44
+ */
45
+ export const PVE_SNAPSHOT_METRIC_NAMES: ReadonlySet<string> = new Set([
46
+ // Identity / status (WI-3 cluster counts derive from these)
47
+ "pve_up",
48
+ "pve_node_info",
49
+ "pve_guest_info",
50
+ "pve_storage_info",
51
+ "pve_version_info",
52
+ "pve_ha_state",
53
+ "pve_onboot_status",
54
+ // Latest-metric mirror (WI-6 inventory columns)
55
+ "pve_uptime_seconds",
56
+ "pve_cpu_usage_ratio",
57
+ "pve_memory_usage_bytes",
58
+ "pve_memory_size_bytes",
59
+ "pve_disk_usage_bytes",
60
+ "pve_disk_size_bytes",
61
+ /*
62
+ * Backup coverage (WI-24) — cluster-level backup-info collector.
63
+ * pve_not_backed_up_total = count of guests not covered by ANY
64
+ * backup job; pve_not_backed_up_info = one series per uncovered
65
+ * guest, labeled only `id`. "Covered by a job" ≠ "backed up
66
+ * recently/successfully" — freshness needs the PVE task log or PBS
67
+ * API (v4 API-agent track).
68
+ */
69
+ "pve_not_backed_up_total",
70
+ "pve_not_backed_up_info",
71
+ ]);
72
+
73
+ /*
74
+ * Ceph snapshot metrics — emitted by the ceph-mgr prometheus module
75
+ * (honor_labels), identity in the `ceph_daemon` (osd.3, mon.a, …) or
76
+ * `pool_id` datapoint labels. Same single-source rule as Proxmox: one
77
+ * scan feeds the CephResource inventory and the CephCluster
78
+ * count/health/capacity snapshot columns.
79
+ */
80
+ export const CEPH_SNAPSHOT_METRIC_NAMES: ReadonlySet<string> = new Set([
81
+ // Cluster-level health / capacity (WI-3 columns)
82
+ "ceph_health_status",
83
+ "ceph_cluster_total_bytes",
84
+ "ceph_cluster_total_used_bytes",
85
+ // Daemon identity / status
86
+ "ceph_mon_quorum_status",
87
+ "ceph_mon_metadata",
88
+ "ceph_osd_up",
89
+ "ceph_osd_in",
90
+ "ceph_osd_metadata",
91
+ "ceph_mgr_metadata",
92
+ "ceph_mds_metadata",
93
+ "ceph_rgw_metadata",
94
+ // OSD latest-metric mirror (WI-6 inventory columns)
95
+ "ceph_osd_stat_bytes",
96
+ "ceph_osd_stat_bytes_used",
97
+ "ceph_osd_apply_latency_ms",
98
+ "ceph_osd_commit_latency_ms",
99
+ "ceph_osd_numpg",
100
+ // Pool identity + latest-metric mirror
101
+ "ceph_pool_metadata",
102
+ "ceph_pool_stored",
103
+ "ceph_pool_max_avail",
104
+ "ceph_pool_objects",
105
+ "ceph_pool_rd",
106
+ "ceph_pool_wr",
107
+ ]);
108
+
109
+ /*
110
+ * One Proxmox resource (Node / Guest / Storage) folded across a batch.
111
+ * Identity labels are first-non-null-wins (stable for the lifetime of
112
+ * the resource); status / metric fields are newest-observedAt-wins.
113
+ */
114
+ export interface ProxmoxResourceBufferEntry {
115
+ kind: string; // Node | Guest | Storage
116
+ externalId: string; // raw `id` label
117
+ name: string | null;
118
+ vmid: number | null;
119
+ guestType: string | null;
120
+ parentNodeName: string | null;
121
+ isUp: boolean | null;
122
+ haState: string | null;
123
+ onboot: boolean | null;
124
+ uptimeSeconds: number | null;
125
+ latestCpuPercent: number | null;
126
+ latestMemoryBytes: number | null;
127
+ maxMemoryBytes: number | null;
128
+ latestDiskBytes: number | null;
129
+ maxDiskBytes: number | null;
130
+ observedAt: Date;
131
+ }
132
+
133
+ /*
134
+ * Per-cluster Proxmox snapshot state. The saw* flags implement the
135
+ * never-zero-a-count-on-a-partial-batch contract: a count column is
136
+ * only written when the batch carried the matching identity series.
137
+ */
138
+ export interface ProxmoxClusterSnapshotBufferEntry {
139
+ sawNodeIdentity: boolean; // pve_node_info, or pve_up on a node id
140
+ sawNodeUp: boolean; // pve_up on a node id
141
+ sawGuestIdentity: boolean; // pve_guest_info
142
+ sawStorageIdentity: boolean; // pve_storage_info
143
+ pveVersion: string | null; // pve_version_info `version` label
144
+ /*
145
+ * WI-24 backup coverage. sawBackupInfo flips when the batch carried
146
+ * the backup-info collector output at all (either series) — the
147
+ * per-guest isBackedUp flag and the cluster count are only written
148
+ * then (never-zero-on-partial-batch guard: a batch without the
149
+ * collector must not mark every guest "covered").
150
+ */
151
+ sawBackupInfo: boolean;
152
+ guestsWithoutBackupCount: number | null; // pve_not_backed_up_total value
153
+ notBackedUpIds: Set<string>; // raw `id` labels of pve_not_backed_up_info
154
+ }
155
+
156
+ /*
157
+ * One Ceph resource (Osd / Pool / Mon / Mgr / Mds / Rgw) folded across
158
+ * a batch. Same merge semantics as ProxmoxResourceBufferEntry.
159
+ */
160
+ export interface CephResourceBufferEntry {
161
+ kind: string;
162
+ externalId: string; // ceph_daemon or pool_id
163
+ name: string | null; // pool name
164
+ hostname: string | null;
165
+ daemonVersion: string | null;
166
+ deviceClass: string | null;
167
+ isUp: boolean | null;
168
+ isIn: boolean | null;
169
+ inQuorum: boolean | null;
170
+ statBytes: number | null;
171
+ statBytesUsed: number | null;
172
+ applyLatencyMs: number | null;
173
+ commitLatencyMs: number | null;
174
+ pgCount: number | null;
175
+ storedBytes: number | null;
176
+ maxAvailBytes: number | null;
177
+ objects: number | null;
178
+ readOpsCounter: number | null;
179
+ writeOpsCounter: number | null;
180
+ observedAt: Date;
181
+ }
182
+
183
+ // Per-cluster Ceph snapshot state — same saw* contract as Proxmox.
184
+ export interface CephClusterSnapshotBufferEntry {
185
+ sawMonMetadata: boolean;
186
+ sawOsdMetadata: boolean;
187
+ sawOsdUp: boolean;
188
+ sawOsdIn: boolean;
189
+ sawPoolMetadata: boolean;
190
+ healthStatus: number | null; // 0 OK / 1 WARN / 2 ERR
191
+ totalBytes: number | null;
192
+ totalUsedBytes: number | null;
193
+ // ceph_version label occurrences across ceph_mon_metadata — modal wins.
194
+ cephVersionCounts: Map<string, number>;
195
+ }
196
+
197
+ // The ProxmoxCluster snapshot columns derived from one folded batch.
198
+ export interface ProxmoxClusterSnapshotExtras {
199
+ pveVersion?: string | undefined;
200
+ nodeCount?: number | undefined;
201
+ onlineNodeCount?: number | undefined;
202
+ guestCount?: number | undefined;
203
+ storageCount?: number | undefined;
204
+ guestsWithoutBackupCount?: number | undefined;
205
+ }
206
+
207
+ // The CephCluster snapshot columns derived from one folded batch.
208
+ export interface CephClusterSnapshotExtras {
209
+ cephVersion?: string | undefined;
210
+ monCount?: number | undefined;
211
+ osdCount?: number | undefined;
212
+ osdUpCount?: number | undefined;
213
+ osdInCount?: number | undefined;
214
+ poolCount?: number | undefined;
215
+ healthStatus?: number | undefined;
216
+ capacityUsedPercent?: number | undefined;
217
+ }
218
+
219
+ /*
220
+ * Same finite-or-null coercion contract as the ingest service's
221
+ * toNumberOrNull (NaN / ±Infinity fold to null so a malformed
222
+ * datapoint is skipped rather than poisoning a snapshot column).
223
+ */
224
+ function toNumberOrNull(value: unknown): number | null {
225
+ if (typeof value === "number") {
226
+ return Number.isFinite(value) ? value : null;
227
+ }
228
+
229
+ if (typeof value === "string") {
230
+ const parsed: number = Number.parseFloat(value);
231
+ return Number.isFinite(parsed) ? parsed : null;
232
+ }
233
+
234
+ return null;
235
+ }
236
+
237
+ /*
238
+ * Same fall-back-to-now parse contract as the ingest service's
239
+ * safeParseUnixNano — only the Date is needed on the snapshot path.
240
+ */
241
+ function parseUnixNanoToDate(
242
+ value: string | number | undefined,
243
+ context: string,
244
+ ): Date {
245
+ let numericValue: number = OneUptimeDate.getCurrentDateAsUnixNano();
246
+
247
+ if (value !== undefined && value !== null) {
248
+ try {
249
+ if (typeof value === "string") {
250
+ const parsed: number = Number.parseFloat(value);
251
+ if (isNaN(parsed)) {
252
+ throw new Error(`Invalid timestamp string: ${value}`);
253
+ }
254
+ numericValue = parsed;
255
+ } else if (typeof value === "number") {
256
+ if (!Number.isFinite(value)) {
257
+ throw new Error(`Invalid timestamp number: ${value}`);
258
+ }
259
+ numericValue = value;
260
+ }
261
+ } catch (error) {
262
+ logger.warn(
263
+ `Error processing ${context}: ${error instanceof Error ? error.message : String(error)}, using current time`,
264
+ );
265
+ numericValue = OneUptimeDate.getCurrentDateAsUnixNano();
266
+ }
267
+ }
268
+
269
+ return OneUptimeDate.fromUnixNano(numericValue);
270
+ }
271
+
272
+ // Same trim-or-null read contract as OtelIngestBaseService.getStringAttribute.
273
+ function getStringAttribute(attributes: JSONArray, key: string): string | null {
274
+ for (const attribute of attributes) {
275
+ if (
276
+ attribute["key"] === key &&
277
+ attribute["value"] &&
278
+ (attribute["value"] as JSONObject)["stringValue"]
279
+ ) {
280
+ const value: JSONValue = (attribute["value"] as JSONObject)[
281
+ "stringValue"
282
+ ];
283
+ if (typeof value === "string" && value.trim()) {
284
+ return value.trim();
285
+ }
286
+ }
287
+ }
288
+ return null;
289
+ }
290
+
291
+ export function getOrCreateProxmoxClusterSnapshot(
292
+ buffer: Map<string, ProxmoxClusterSnapshotBufferEntry>,
293
+ clusterIdStr: string,
294
+ ): ProxmoxClusterSnapshotBufferEntry {
295
+ let entry: ProxmoxClusterSnapshotBufferEntry | undefined =
296
+ buffer.get(clusterIdStr);
297
+ if (!entry) {
298
+ entry = {
299
+ sawNodeIdentity: false,
300
+ sawNodeUp: false,
301
+ sawGuestIdentity: false,
302
+ sawStorageIdentity: false,
303
+ pveVersion: null,
304
+ sawBackupInfo: false,
305
+ guestsWithoutBackupCount: null,
306
+ notBackedUpIds: new Set(),
307
+ };
308
+ buffer.set(clusterIdStr, entry);
309
+ }
310
+ return entry;
311
+ }
312
+
313
+ /*
314
+ * Fold one pve_* datapoint into the per-cluster buffers. Identity
315
+ * lives in the `id` DATAPOINT label (prometheus receiver), so this
316
+ * reads the raw datapoint attribute array — not the merged
317
+ * resource-prefixed map the K8s scan uses.
318
+ */
319
+ export function bufferProxmoxSnapshotMetric(data: {
320
+ clusterIdStr: string;
321
+ metricName: string;
322
+ datapoint: JSONObject;
323
+ resourceBuffer: Map<string, Map<string, ProxmoxResourceBufferEntry>>;
324
+ clusterBuffer: Map<string, ProxmoxClusterSnapshotBufferEntry>;
325
+ }): void {
326
+ const valueFromInt: number | null = toNumberOrNull(data.datapoint["asInt"]);
327
+ const valueFromDouble: number | null = toNumberOrNull(
328
+ data.datapoint["asDouble"],
329
+ );
330
+ const rawValue: number | null = valueFromDouble ?? valueFromInt;
331
+ if (rawValue === null) {
332
+ return;
333
+ }
334
+
335
+ const observedAt: Date = parseUnixNanoToDate(
336
+ data.datapoint["timeUnixNano"] as string | number | undefined,
337
+ "pve snapshot timeUnixNano",
338
+ );
339
+
340
+ const dpAttributes: JSONArray =
341
+ (data.datapoint["attributes"] as JSONArray) || [];
342
+
343
+ const cluster: ProxmoxClusterSnapshotBufferEntry =
344
+ getOrCreateProxmoxClusterSnapshot(data.clusterBuffer, data.clusterIdStr);
345
+
346
+ /*
347
+ * pve_version_info carries no `id` label — it is the cluster-level
348
+ * PVE version (this is what populates ProxmoxCluster.pveVersion).
349
+ */
350
+ if (data.metricName === "pve_version_info") {
351
+ const version: string | null = getStringAttribute(dpAttributes, "version");
352
+ if (version) {
353
+ cluster.pveVersion = version;
354
+ }
355
+ return;
356
+ }
357
+
358
+ /*
359
+ * WI-24 backup coverage — cluster-level backup-info collector
360
+ * series, handled before the generic scope parse:
361
+ * pve_not_backed_up_total carries no `id` label, and the
362
+ * pve_not_backed_up_info `id` label format is unverified upstream
363
+ * (`qemu/100` vs bare vmid — §3.9 spike), so neither goes through
364
+ * the inventory-row path. The ids are folded into the cluster
365
+ * buffer and joined to the buffered Guest rows at flush time.
366
+ */
367
+ if (data.metricName === "pve_not_backed_up_total") {
368
+ cluster.sawBackupInfo = true;
369
+ cluster.guestsWithoutBackupCount = Math.max(0, Math.trunc(rawValue));
370
+ return;
371
+ }
372
+ if (data.metricName === "pve_not_backed_up_info") {
373
+ cluster.sawBackupInfo = true;
374
+ const uncoveredId: string | null = getStringAttribute(dpAttributes, "id");
375
+ if (uncoveredId) {
376
+ cluster.notBackedUpIds.add(uncoveredId);
377
+ }
378
+ return;
379
+ }
380
+
381
+ const id: string | null = getStringAttribute(dpAttributes, "id");
382
+ if (!id) {
383
+ return;
384
+ }
385
+
386
+ /*
387
+ * Scope/type: prefer the pve.scope / pve.type attributes stamped
388
+ * by the agent's transform/pve-identity processor; fall back to
389
+ * parsing the id prefix so inventory still populates on a
390
+ * hand-rolled collector config without the transform.
391
+ */
392
+ const slashIndex: number = id.indexOf("/");
393
+ const idPrefix: string = slashIndex > 0 ? id.substring(0, slashIndex) : "";
394
+ const pveType: string =
395
+ getStringAttribute(dpAttributes, "pve.type") || idPrefix;
396
+ let scope: string | null = getStringAttribute(dpAttributes, "pve.scope");
397
+ if (!scope) {
398
+ scope = idPrefix === "qemu" || idPrefix === "lxc" ? "guest" : idPrefix;
399
+ }
400
+
401
+ let kind: string;
402
+ if (scope === "node") {
403
+ kind = "Node";
404
+ } else if (scope === "guest") {
405
+ kind = "Guest";
406
+ } else if (scope === "storage") {
407
+ kind = "Storage";
408
+ } else {
409
+ // cluster/* series and unknown scopes aren't inventory rows.
410
+ return;
411
+ }
412
+
413
+ const patch: ProxmoxResourceBufferEntry = {
414
+ kind,
415
+ externalId: id,
416
+ name: null,
417
+ vmid: null,
418
+ guestType: null,
419
+ parentNodeName: null,
420
+ isUp: null,
421
+ haState: null,
422
+ onboot: null,
423
+ uptimeSeconds: null,
424
+ latestCpuPercent: null,
425
+ latestMemoryBytes: null,
426
+ maxMemoryBytes: null,
427
+ latestDiskBytes: null,
428
+ maxDiskBytes: null,
429
+ observedAt,
430
+ };
431
+
432
+ if (kind === "Guest") {
433
+ patch.guestType = pveType === "qemu" || pveType === "lxc" ? pveType : null;
434
+ if (slashIndex > 0) {
435
+ const vmidParsed: number = parseInt(id.substring(slashIndex + 1), 10);
436
+ patch.vmid = isNaN(vmidParsed) ? null : vmidParsed;
437
+ }
438
+ }
439
+
440
+ // `node` label: present on the info series and pve_onboot_status.
441
+ const nodeLabel: string | null = getStringAttribute(dpAttributes, "node");
442
+ if (nodeLabel && kind !== "Node") {
443
+ patch.parentNodeName = nodeLabel;
444
+ }
445
+
446
+ switch (data.metricName) {
447
+ case "pve_up": {
448
+ patch.isUp = rawValue >= 1;
449
+ if (kind === "Node") {
450
+ // pve_up doubles as the node-identity fallback per the spec.
451
+ cluster.sawNodeIdentity = true;
452
+ cluster.sawNodeUp = true;
453
+ }
454
+ break;
455
+ }
456
+ case "pve_node_info": {
457
+ patch.name = getStringAttribute(dpAttributes, "name");
458
+ cluster.sawNodeIdentity = true;
459
+ break;
460
+ }
461
+ case "pve_guest_info": {
462
+ patch.name = getStringAttribute(dpAttributes, "name");
463
+ cluster.sawGuestIdentity = true;
464
+ break;
465
+ }
466
+ case "pve_storage_info": {
467
+ patch.name = getStringAttribute(dpAttributes, "storage");
468
+ cluster.sawStorageIdentity = true;
469
+ break;
470
+ }
471
+ case "pve_ha_state": {
472
+ /*
473
+ * Enum-series: one row per possible state, value 1 marks the
474
+ * current one. Only the active row carries signal — skip the
475
+ * zero-valued rows entirely so they don't create empty patches.
476
+ */
477
+ if (rawValue < 1) {
478
+ return;
479
+ }
480
+ patch.haState = getStringAttribute(dpAttributes, "state");
481
+ break;
482
+ }
483
+ case "pve_onboot_status": {
484
+ patch.onboot = rawValue >= 1;
485
+ break;
486
+ }
487
+ case "pve_uptime_seconds": {
488
+ patch.uptimeSeconds = Math.max(0, Math.trunc(rawValue));
489
+ break;
490
+ }
491
+ case "pve_cpu_usage_ratio": {
492
+ /*
493
+ * Already a true 0..1 ratio — no allocatable-denominator cache
494
+ * needed, unlike K8s cpuCoresToPercent.
495
+ */
496
+ patch.latestCpuPercent = rawValue * 100;
497
+ break;
498
+ }
499
+ case "pve_memory_usage_bytes": {
500
+ patch.latestMemoryBytes = Math.max(0, Math.trunc(rawValue));
501
+ break;
502
+ }
503
+ case "pve_memory_size_bytes": {
504
+ patch.maxMemoryBytes = Math.max(0, Math.trunc(rawValue));
505
+ break;
506
+ }
507
+ case "pve_disk_usage_bytes": {
508
+ patch.latestDiskBytes = Math.max(0, Math.trunc(rawValue));
509
+ break;
510
+ }
511
+ case "pve_disk_size_bytes": {
512
+ patch.maxDiskBytes = Math.max(0, Math.trunc(rawValue));
513
+ break;
514
+ }
515
+ default: {
516
+ return;
517
+ }
518
+ }
519
+
520
+ foldProxmoxResourceSnapshot({
521
+ buffer: data.resourceBuffer,
522
+ clusterIdStr: data.clusterIdStr,
523
+ patch,
524
+ });
525
+ }
526
+
527
+ /*
528
+ * Merge a patch into the per-cluster Proxmox buffer: identity labels
529
+ * are first-non-null-wins (stable, and a batch missing an info
530
+ * series must not blank them), status/metric fields are
531
+ * newest-observedAt-wins (K8s buffer semantics).
532
+ */
533
+ export function foldProxmoxResourceSnapshot(data: {
534
+ buffer: Map<string, Map<string, ProxmoxResourceBufferEntry>>;
535
+ clusterIdStr: string;
536
+ patch: ProxmoxResourceBufferEntry;
537
+ }): void {
538
+ let perCluster: Map<string, ProxmoxResourceBufferEntry> | undefined =
539
+ data.buffer.get(data.clusterIdStr);
540
+ if (!perCluster) {
541
+ perCluster = new Map();
542
+ data.buffer.set(data.clusterIdStr, perCluster);
543
+ }
544
+ const key: string = `${data.patch.kind}|${data.patch.externalId}`;
545
+ const existing: ProxmoxResourceBufferEntry | undefined = perCluster.get(key);
546
+ if (!existing) {
547
+ perCluster.set(key, data.patch);
548
+ return;
549
+ }
550
+
551
+ const patch: ProxmoxResourceBufferEntry = data.patch;
552
+ const newer: boolean = patch.observedAt >= existing.observedAt;
553
+
554
+ // Identity: first-non-null wins.
555
+ if (existing.name === null && patch.name !== null) {
556
+ existing.name = patch.name;
557
+ }
558
+ if (existing.vmid === null && patch.vmid !== null) {
559
+ existing.vmid = patch.vmid;
560
+ }
561
+ if (existing.guestType === null && patch.guestType !== null) {
562
+ existing.guestType = patch.guestType;
563
+ }
564
+ if (existing.parentNodeName === null && patch.parentNodeName !== null) {
565
+ existing.parentNodeName = patch.parentNodeName;
566
+ }
567
+
568
+ // Status / metrics: newest observation wins.
569
+ if (patch.isUp !== null && newer) {
570
+ existing.isUp = patch.isUp;
571
+ }
572
+ if (patch.haState !== null && newer) {
573
+ existing.haState = patch.haState;
574
+ }
575
+ if (patch.onboot !== null && newer) {
576
+ existing.onboot = patch.onboot;
577
+ }
578
+ if (patch.uptimeSeconds !== null && newer) {
579
+ existing.uptimeSeconds = patch.uptimeSeconds;
580
+ }
581
+ if (patch.latestCpuPercent !== null && newer) {
582
+ existing.latestCpuPercent = patch.latestCpuPercent;
583
+ }
584
+ if (patch.latestMemoryBytes !== null && newer) {
585
+ existing.latestMemoryBytes = patch.latestMemoryBytes;
586
+ }
587
+ if (patch.maxMemoryBytes !== null && newer) {
588
+ existing.maxMemoryBytes = patch.maxMemoryBytes;
589
+ }
590
+ if (patch.latestDiskBytes !== null && newer) {
591
+ existing.latestDiskBytes = patch.latestDiskBytes;
592
+ }
593
+ if (patch.maxDiskBytes !== null && newer) {
594
+ existing.maxDiskBytes = patch.maxDiskBytes;
595
+ }
596
+ if (patch.observedAt > existing.observedAt) {
597
+ existing.observedAt = patch.observedAt;
598
+ }
599
+ }
600
+
601
+ /*
602
+ * WI-24: a guest is "covered by a backup job" exactly when the batch
603
+ * carried the backup-info collector output at all AND no
604
+ * pve_not_backed_up_info series carries its id (series present =
605
+ * NOT covered — the exporter emits one info row per uncovered
606
+ * guest). A batch without the collector output yields null, so the
607
+ * upsert COALESCE keeps the last-known value (the same
608
+ * never-zero-on-partial-batch guard the count columns use).
609
+ *
610
+ * The info `id` label's value format is unverified upstream
611
+ * (`qemu/100` vs bare vmid — §3.9 spike), so match both the full
612
+ * externalId and the bare vmid.
613
+ */
614
+ export function computeProxmoxGuestBackedUp(
615
+ entry: ProxmoxResourceBufferEntry,
616
+ snap: ProxmoxClusterSnapshotBufferEntry | undefined,
617
+ ): boolean | null {
618
+ if (entry.kind !== "Guest" || !snap?.sawBackupInfo) {
619
+ return null;
620
+ }
621
+ if (snap.notBackedUpIds.has(entry.externalId)) {
622
+ return false;
623
+ }
624
+ if (entry.vmid !== null && snap.notBackedUpIds.has(String(entry.vmid))) {
625
+ return false;
626
+ }
627
+ return true;
628
+ }
629
+
630
+ /*
631
+ * Derive the ProxmoxCluster snapshot columns from one folded batch.
632
+ * Counts are only set when the batch carried the matching identity
633
+ * series — never zero a count on a partial batch. Returns an object
634
+ * whose keys are exactly the columns to write (empty object = nothing
635
+ * to write).
636
+ */
637
+ export function deriveProxmoxClusterSnapshotExtras(
638
+ entries: Array<ProxmoxResourceBufferEntry>,
639
+ snap: ProxmoxClusterSnapshotBufferEntry | undefined,
640
+ ): ProxmoxClusterSnapshotExtras {
641
+ const extras: ProxmoxClusterSnapshotExtras = {};
642
+
643
+ if (snap?.pveVersion) {
644
+ extras.pveVersion = snap.pveVersion;
645
+ }
646
+ if (snap?.sawNodeIdentity) {
647
+ extras.nodeCount = entries.filter((e: ProxmoxResourceBufferEntry) => {
648
+ return e.kind === "Node";
649
+ }).length;
650
+ }
651
+ if (snap?.sawNodeUp) {
652
+ extras.onlineNodeCount = entries.filter((e: ProxmoxResourceBufferEntry) => {
653
+ return e.kind === "Node" && e.isUp === true;
654
+ }).length;
655
+ }
656
+ if (snap?.sawGuestIdentity) {
657
+ extras.guestCount = entries.filter((e: ProxmoxResourceBufferEntry) => {
658
+ return e.kind === "Guest";
659
+ }).length;
660
+ }
661
+ if (snap?.sawStorageIdentity) {
662
+ extras.storageCount = entries.filter((e: ProxmoxResourceBufferEntry) => {
663
+ return e.kind === "Storage";
664
+ }).length;
665
+ }
666
+ /*
667
+ * WI-24: written only when the batch carried pve_not_backed_up_total
668
+ * itself — NULL in Postgres stays "collector not reporting",
669
+ * distinct from 0 uncovered guests.
670
+ */
671
+ if (
672
+ snap &&
673
+ snap.guestsWithoutBackupCount !== null &&
674
+ snap.guestsWithoutBackupCount !== undefined
675
+ ) {
676
+ extras.guestsWithoutBackupCount = snap.guestsWithoutBackupCount;
677
+ }
678
+
679
+ return extras;
680
+ }
681
+
682
+ export function getOrCreateCephClusterSnapshot(
683
+ buffer: Map<string, CephClusterSnapshotBufferEntry>,
684
+ clusterIdStr: string,
685
+ ): CephClusterSnapshotBufferEntry {
686
+ let entry: CephClusterSnapshotBufferEntry | undefined =
687
+ buffer.get(clusterIdStr);
688
+ if (!entry) {
689
+ entry = {
690
+ sawMonMetadata: false,
691
+ sawOsdMetadata: false,
692
+ sawOsdUp: false,
693
+ sawOsdIn: false,
694
+ sawPoolMetadata: false,
695
+ healthStatus: null,
696
+ totalBytes: null,
697
+ totalUsedBytes: null,
698
+ cephVersionCounts: new Map(),
699
+ };
700
+ buffer.set(clusterIdStr, entry);
701
+ }
702
+ return entry;
703
+ }
704
+
705
+ export function emptyCephResourceEntry(
706
+ kind: string,
707
+ externalId: string,
708
+ observedAt: Date,
709
+ ): CephResourceBufferEntry {
710
+ return {
711
+ kind,
712
+ externalId,
713
+ name: null,
714
+ hostname: null,
715
+ daemonVersion: null,
716
+ deviceClass: null,
717
+ isUp: null,
718
+ isIn: null,
719
+ inQuorum: null,
720
+ statBytes: null,
721
+ statBytesUsed: null,
722
+ applyLatencyMs: null,
723
+ commitLatencyMs: null,
724
+ pgCount: null,
725
+ storedBytes: null,
726
+ maxAvailBytes: null,
727
+ objects: null,
728
+ readOpsCounter: null,
729
+ writeOpsCounter: null,
730
+ observedAt,
731
+ };
732
+ }
733
+
734
+ /*
735
+ * Fold one ceph_* datapoint into the per-cluster buffers. Pool
736
+ * series are keyed by the `pool_id` datapoint label, daemon series
737
+ * by `ceph_daemon` (osd.3, mon.a, mgr.x, …).
738
+ */
739
+ export function bufferCephSnapshotMetric(data: {
740
+ clusterIdStr: string;
741
+ metricName: string;
742
+ datapoint: JSONObject;
743
+ resourceBuffer: Map<string, Map<string, CephResourceBufferEntry>>;
744
+ clusterBuffer: Map<string, CephClusterSnapshotBufferEntry>;
745
+ }): void {
746
+ const valueFromInt: number | null = toNumberOrNull(data.datapoint["asInt"]);
747
+ const valueFromDouble: number | null = toNumberOrNull(
748
+ data.datapoint["asDouble"],
749
+ );
750
+ const rawValue: number | null = valueFromDouble ?? valueFromInt;
751
+ if (rawValue === null) {
752
+ return;
753
+ }
754
+
755
+ const observedAt: Date = parseUnixNanoToDate(
756
+ data.datapoint["timeUnixNano"] as string | number | undefined,
757
+ "ceph snapshot timeUnixNano",
758
+ );
759
+
760
+ const dpAttributes: JSONArray =
761
+ (data.datapoint["attributes"] as JSONArray) || [];
762
+
763
+ const cluster: CephClusterSnapshotBufferEntry =
764
+ getOrCreateCephClusterSnapshot(data.clusterBuffer, data.clusterIdStr);
765
+
766
+ // Cluster-level series — no per-resource row.
767
+ if (data.metricName === "ceph_health_status") {
768
+ // 0 = HEALTH_OK, 1 = HEALTH_WARN, 2 = HEALTH_ERR.
769
+ cluster.healthStatus = Math.max(0, Math.trunc(rawValue));
770
+ return;
771
+ }
772
+ if (data.metricName === "ceph_cluster_total_bytes") {
773
+ cluster.totalBytes = Math.max(0, rawValue);
774
+ return;
775
+ }
776
+ if (data.metricName === "ceph_cluster_total_used_bytes") {
777
+ cluster.totalUsedBytes = Math.max(0, rawValue);
778
+ return;
779
+ }
780
+
781
+ if (data.metricName.startsWith("ceph_pool_")) {
782
+ const poolId: string | null = getStringAttribute(dpAttributes, "pool_id");
783
+ if (!poolId) {
784
+ return;
785
+ }
786
+
787
+ const patch: CephResourceBufferEntry = emptyCephResourceEntry(
788
+ "Pool",
789
+ poolId,
790
+ observedAt,
791
+ );
792
+
793
+ switch (data.metricName) {
794
+ case "ceph_pool_metadata": {
795
+ // The only pool series that carries the human-readable name.
796
+ patch.name = getStringAttribute(dpAttributes, "name");
797
+ cluster.sawPoolMetadata = true;
798
+ break;
799
+ }
800
+ case "ceph_pool_stored": {
801
+ patch.storedBytes = Math.max(0, Math.trunc(rawValue));
802
+ break;
803
+ }
804
+ case "ceph_pool_max_avail": {
805
+ patch.maxAvailBytes = Math.max(0, Math.trunc(rawValue));
806
+ break;
807
+ }
808
+ case "ceph_pool_objects": {
809
+ patch.objects = Math.max(0, Math.trunc(rawValue));
810
+ break;
811
+ }
812
+ /*
813
+ * Latest RAW cumulative counters — the Pools list computes
814
+ * IOPS rates on read from ClickHouse, never from these.
815
+ */
816
+ case "ceph_pool_rd": {
817
+ patch.readOpsCounter = Math.max(0, Math.trunc(rawValue));
818
+ break;
819
+ }
820
+ case "ceph_pool_wr": {
821
+ patch.writeOpsCounter = Math.max(0, Math.trunc(rawValue));
822
+ break;
823
+ }
824
+ default: {
825
+ return;
826
+ }
827
+ }
828
+
829
+ foldCephResourceSnapshot({
830
+ buffer: data.resourceBuffer,
831
+ clusterIdStr: data.clusterIdStr,
832
+ patch,
833
+ });
834
+ return;
835
+ }
836
+
837
+ const cephDaemon: string | null = getStringAttribute(
838
+ dpAttributes,
839
+ "ceph_daemon",
840
+ );
841
+ if (!cephDaemon) {
842
+ return;
843
+ }
844
+
845
+ const dotIndex: number = cephDaemon.indexOf(".");
846
+ const daemonPrefix: string =
847
+ dotIndex > 0 ? cephDaemon.substring(0, dotIndex) : "";
848
+ let kind: string;
849
+ if (daemonPrefix === "osd") {
850
+ kind = "Osd";
851
+ } else if (daemonPrefix === "mon") {
852
+ kind = "Mon";
853
+ } else if (daemonPrefix === "mgr") {
854
+ kind = "Mgr";
855
+ } else if (daemonPrefix === "mds") {
856
+ kind = "Mds";
857
+ } else if (daemonPrefix === "rgw") {
858
+ kind = "Rgw";
859
+ } else {
860
+ return;
861
+ }
862
+
863
+ const patch: CephResourceBufferEntry = emptyCephResourceEntry(
864
+ kind,
865
+ cephDaemon,
866
+ observedAt,
867
+ );
868
+
869
+ switch (data.metricName) {
870
+ case "ceph_mon_quorum_status": {
871
+ patch.inQuorum = rawValue >= 1;
872
+ break;
873
+ }
874
+ case "ceph_mon_metadata": {
875
+ patch.hostname = getStringAttribute(dpAttributes, "hostname");
876
+ patch.daemonVersion = getStringAttribute(dpAttributes, "ceph_version");
877
+ cluster.sawMonMetadata = true;
878
+ // CephCluster.cephVersion = modal mon version across the batch.
879
+ if (patch.daemonVersion) {
880
+ cluster.cephVersionCounts.set(
881
+ patch.daemonVersion,
882
+ (cluster.cephVersionCounts.get(patch.daemonVersion) || 0) + 1,
883
+ );
884
+ }
885
+ break;
886
+ }
887
+ case "ceph_osd_up": {
888
+ patch.isUp = rawValue >= 1;
889
+ cluster.sawOsdUp = true;
890
+ break;
891
+ }
892
+ case "ceph_osd_in": {
893
+ patch.isIn = rawValue >= 1;
894
+ cluster.sawOsdIn = true;
895
+ break;
896
+ }
897
+ case "ceph_osd_metadata": {
898
+ patch.hostname = getStringAttribute(dpAttributes, "hostname");
899
+ patch.deviceClass = getStringAttribute(dpAttributes, "device_class");
900
+ patch.daemonVersion = getStringAttribute(dpAttributes, "ceph_version");
901
+ cluster.sawOsdMetadata = true;
902
+ break;
903
+ }
904
+ case "ceph_mgr_metadata":
905
+ case "ceph_mds_metadata":
906
+ case "ceph_rgw_metadata": {
907
+ patch.hostname = getStringAttribute(dpAttributes, "hostname");
908
+ patch.daemonVersion = getStringAttribute(dpAttributes, "ceph_version");
909
+ break;
910
+ }
911
+ case "ceph_osd_stat_bytes": {
912
+ patch.statBytes = Math.max(0, Math.trunc(rawValue));
913
+ break;
914
+ }
915
+ case "ceph_osd_stat_bytes_used": {
916
+ patch.statBytesUsed = Math.max(0, Math.trunc(rawValue));
917
+ break;
918
+ }
919
+ case "ceph_osd_apply_latency_ms": {
920
+ patch.applyLatencyMs = Math.max(0, rawValue);
921
+ break;
922
+ }
923
+ case "ceph_osd_commit_latency_ms": {
924
+ patch.commitLatencyMs = Math.max(0, rawValue);
925
+ break;
926
+ }
927
+ case "ceph_osd_numpg": {
928
+ patch.pgCount = Math.max(0, Math.trunc(rawValue));
929
+ break;
930
+ }
931
+ default: {
932
+ return;
933
+ }
934
+ }
935
+
936
+ foldCephResourceSnapshot({
937
+ buffer: data.resourceBuffer,
938
+ clusterIdStr: data.clusterIdStr,
939
+ patch,
940
+ });
941
+ }
942
+
943
+ /*
944
+ * Merge a patch into the per-cluster Ceph buffer — same semantics
945
+ * as foldProxmoxResourceSnapshot (identity first-non-null-wins,
946
+ * status/metrics newest-observedAt-wins).
947
+ */
948
+ export function foldCephResourceSnapshot(data: {
949
+ buffer: Map<string, Map<string, CephResourceBufferEntry>>;
950
+ clusterIdStr: string;
951
+ patch: CephResourceBufferEntry;
952
+ }): void {
953
+ let perCluster: Map<string, CephResourceBufferEntry> | undefined =
954
+ data.buffer.get(data.clusterIdStr);
955
+ if (!perCluster) {
956
+ perCluster = new Map();
957
+ data.buffer.set(data.clusterIdStr, perCluster);
958
+ }
959
+ const key: string = `${data.patch.kind}|${data.patch.externalId}`;
960
+ const existing: CephResourceBufferEntry | undefined = perCluster.get(key);
961
+ if (!existing) {
962
+ perCluster.set(key, data.patch);
963
+ return;
964
+ }
965
+
966
+ const patch: CephResourceBufferEntry = data.patch;
967
+ const newer: boolean = patch.observedAt >= existing.observedAt;
968
+
969
+ // Identity: first-non-null wins.
970
+ if (existing.name === null && patch.name !== null) {
971
+ existing.name = patch.name;
972
+ }
973
+ if (existing.hostname === null && patch.hostname !== null) {
974
+ existing.hostname = patch.hostname;
975
+ }
976
+ if (existing.daemonVersion === null && patch.daemonVersion !== null) {
977
+ existing.daemonVersion = patch.daemonVersion;
978
+ }
979
+ if (existing.deviceClass === null && patch.deviceClass !== null) {
980
+ existing.deviceClass = patch.deviceClass;
981
+ }
982
+
983
+ // Status / metrics: newest observation wins.
984
+ if (patch.isUp !== null && newer) {
985
+ existing.isUp = patch.isUp;
986
+ }
987
+ if (patch.isIn !== null && newer) {
988
+ existing.isIn = patch.isIn;
989
+ }
990
+ if (patch.inQuorum !== null && newer) {
991
+ existing.inQuorum = patch.inQuorum;
992
+ }
993
+ if (patch.statBytes !== null && newer) {
994
+ existing.statBytes = patch.statBytes;
995
+ }
996
+ if (patch.statBytesUsed !== null && newer) {
997
+ existing.statBytesUsed = patch.statBytesUsed;
998
+ }
999
+ if (patch.applyLatencyMs !== null && newer) {
1000
+ existing.applyLatencyMs = patch.applyLatencyMs;
1001
+ }
1002
+ if (patch.commitLatencyMs !== null && newer) {
1003
+ existing.commitLatencyMs = patch.commitLatencyMs;
1004
+ }
1005
+ if (patch.pgCount !== null && newer) {
1006
+ existing.pgCount = patch.pgCount;
1007
+ }
1008
+ if (patch.storedBytes !== null && newer) {
1009
+ existing.storedBytes = patch.storedBytes;
1010
+ }
1011
+ if (patch.maxAvailBytes !== null && newer) {
1012
+ existing.maxAvailBytes = patch.maxAvailBytes;
1013
+ }
1014
+ if (patch.objects !== null && newer) {
1015
+ existing.objects = patch.objects;
1016
+ }
1017
+ if (patch.readOpsCounter !== null && newer) {
1018
+ existing.readOpsCounter = patch.readOpsCounter;
1019
+ }
1020
+ if (patch.writeOpsCounter !== null && newer) {
1021
+ existing.writeOpsCounter = patch.writeOpsCounter;
1022
+ }
1023
+ if (patch.observedAt > existing.observedAt) {
1024
+ existing.observedAt = patch.observedAt;
1025
+ }
1026
+ }
1027
+
1028
+ /*
1029
+ * Derive the CephCluster snapshot columns from one folded batch —
1030
+ * same never-zero-a-count-on-a-partial-batch contract as the Proxmox
1031
+ * derive above.
1032
+ */
1033
+ export function deriveCephClusterSnapshotExtras(
1034
+ entries: Array<CephResourceBufferEntry>,
1035
+ snap: CephClusterSnapshotBufferEntry | undefined,
1036
+ ): CephClusterSnapshotExtras {
1037
+ const extras: CephClusterSnapshotExtras = {};
1038
+
1039
+ if (snap?.sawMonMetadata) {
1040
+ extras.monCount = entries.filter((e: CephResourceBufferEntry) => {
1041
+ return e.kind === "Mon";
1042
+ }).length;
1043
+ }
1044
+ if (snap?.sawOsdMetadata) {
1045
+ extras.osdCount = entries.filter((e: CephResourceBufferEntry) => {
1046
+ return e.kind === "Osd";
1047
+ }).length;
1048
+ }
1049
+ if (snap?.sawOsdUp) {
1050
+ extras.osdUpCount = entries.filter((e: CephResourceBufferEntry) => {
1051
+ return e.kind === "Osd" && e.isUp === true;
1052
+ }).length;
1053
+ }
1054
+ if (snap?.sawOsdIn) {
1055
+ extras.osdInCount = entries.filter((e: CephResourceBufferEntry) => {
1056
+ return e.kind === "Osd" && e.isIn === true;
1057
+ }).length;
1058
+ }
1059
+ if (snap?.sawPoolMetadata) {
1060
+ extras.poolCount = entries.filter((e: CephResourceBufferEntry) => {
1061
+ return e.kind === "Pool";
1062
+ }).length;
1063
+ }
1064
+ if (snap && snap.healthStatus !== null) {
1065
+ extras.healthStatus = snap.healthStatus;
1066
+ }
1067
+ if (
1068
+ snap &&
1069
+ snap.totalBytes !== null &&
1070
+ snap.totalBytes > 0 &&
1071
+ snap.totalUsedBytes !== null
1072
+ ) {
1073
+ /*
1074
+ * Round to 2 decimals: full float precision would change the
1075
+ * extras fingerprint on every scrape and defeat the 60-s
1076
+ * write throttle for an invisible difference.
1077
+ */
1078
+ extras.capacityUsedPercent =
1079
+ Math.round((snap.totalUsedBytes / snap.totalBytes) * 10000) / 100;
1080
+ }
1081
+ if (snap && snap.cephVersionCounts.size > 0) {
1082
+ let modalVersion: string | null = null;
1083
+ let modalCount: number = 0;
1084
+ for (const [version, count] of snap.cephVersionCounts.entries()) {
1085
+ if (count > modalCount) {
1086
+ modalVersion = version;
1087
+ modalCount = count;
1088
+ }
1089
+ }
1090
+ if (modalVersion) {
1091
+ extras.cephVersion = modalVersion;
1092
+ }
1093
+ }
1094
+
1095
+ return extras;
1096
+ }