@oneuptime/common 12.0.13 → 12.0.14

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 (376) hide show
  1. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +14 -0
  2. package/Models/DatabaseModels/UserCall.ts +94 -3
  3. package/Models/DatabaseModels/UserEmail.ts +94 -3
  4. package/Models/DatabaseModels/UserIncomingCallNumber.ts +94 -3
  5. package/Models/DatabaseModels/UserPush.ts +37 -0
  6. package/Models/DatabaseModels/UserSMS.ts +94 -3
  7. package/Models/DatabaseModels/UserWhatsApp.ts +93 -2
  8. package/Server/API/BaseAPI.ts +79 -14
  9. package/Server/API/UserCallAPI.ts +53 -45
  10. package/Server/API/UserEmailAPI.ts +53 -44
  11. package/Server/API/UserIncomingCallNumberAPI.ts +48 -38
  12. package/Server/API/UserNotificationMethodAdminAPI.ts +481 -0
  13. package/Server/API/UserPushAPI.ts +126 -2
  14. package/Server/API/UserSmsAPI.ts +53 -44
  15. package/Server/API/UserWhatsAppAPI.ts +53 -50
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.ts +154 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.ts +17 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.ts +45 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  20. package/Server/Middleware/VerificationCodeRateLimit.ts +586 -0
  21. package/Server/Services/AnalyticsDatabaseService.ts +169 -0
  22. package/Server/Services/DatabaseService.ts +101 -0
  23. package/Server/Services/LogAggregationService.ts +5 -0
  24. package/Server/Services/PushNotificationService.ts +127 -12
  25. package/Server/Services/TeamMemberService.ts +53 -13
  26. package/Server/Services/UserCallService.ts +55 -24
  27. package/Server/Services/UserEmailService.ts +83 -17
  28. package/Server/Services/UserIncomingCallNumberService.ts +49 -85
  29. package/Server/Services/UserNotificationMethodAdminService.ts +1527 -0
  30. package/Server/Services/UserNotificationRuleService.ts +26 -0
  31. package/Server/Services/UserPushService.ts +89 -0
  32. package/Server/Services/UserSmsService.ts +50 -17
  33. package/Server/Services/UserWhatsAppService.ts +48 -15
  34. package/Server/Types/Database/QueryHelper.ts +30 -0
  35. package/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.ts +59 -0
  36. package/Server/Utils/ChannelVerification.ts +510 -0
  37. package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +13 -16
  38. package/Server/Utils/Monitor/MonitorAlert.ts +24 -21
  39. package/Server/Utils/Monitor/MonitorIncident.ts +21 -18
  40. package/Server/Utils/Monitor/MonitorResourceContext.ts +107 -0
  41. package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +442 -0
  42. package/Server/Utils/Monitor/SeriesResourceLabels.ts +3 -3
  43. package/Server/Utils/Monitor/SeriesResourceLinker.ts +96 -28
  44. package/Server/Utils/TotpAuth.ts +137 -9
  45. package/Server/Utils/UserRegistrationToken.ts +182 -0
  46. package/Server/Utils/VerificationCode.ts +134 -0
  47. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +173 -94
  48. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +1151 -0
  49. package/Tests/App/Dashboard/AdminUserOnCallPages.test.tsx +905 -0
  50. package/Tests/App/Dashboard/DashboardCommandPalette.test.tsx +338 -0
  51. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +1 -1
  52. package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +28 -0
  53. package/Tests/Models/UserPushCriticalAlertColumn.test.ts +172 -0
  54. package/Tests/Server/API/BaseAPIGetListParallel.test.ts +519 -0
  55. package/Tests/Server/API/NotificationChannelVerificationAPI.test.ts +543 -0
  56. package/Tests/Server/API/UserIncomingCallNumberApi.test.ts +108 -18
  57. package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +963 -0
  58. package/Tests/Server/API/UserPushCriticalAlertsApi.test.ts +114 -0
  59. package/Tests/Server/API/UserSmsApi.test.ts +107 -14
  60. package/Tests/Server/API/UserTotpAuthAPI.test.ts +427 -0
  61. package/Tests/Server/Infrastructure/Postgres/FixTotpOtpUrlAlgorithmMigration.test.ts +315 -0
  62. package/Tests/Server/Middleware/VerificationCodeRateLimit.test.ts +775 -0
  63. package/Tests/Server/Services/AddNetworkDeviceReachabilityColumnsMigration.test.ts +10 -62
  64. package/Tests/Server/Services/AnalyticsDatabasePaginationStability.test.ts +33 -13
  65. package/Tests/Server/Services/AnalyticsDatabaseSortKeyBoundary.test.ts +658 -0
  66. package/Tests/Server/Services/CriticalOnCallAlertDelivery.test.ts +458 -0
  67. package/Tests/Server/Services/CriticalPushAlertPayload.test.ts +301 -0
  68. package/Tests/Server/Services/DatabaseServiceAtomicIncrement.test.ts +143 -0
  69. package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +1 -0
  70. package/Tests/Server/Services/LogAggregationScanMemory.test.ts +421 -0
  71. package/Tests/Server/Services/LogAggregationService.test.ts +2 -2
  72. package/Tests/Server/Services/NotificationChannelCodeIssuance.test.ts +401 -0
  73. package/Tests/Server/Services/PushNotificationDeliveryOptions.test.ts +204 -0
  74. package/Tests/Server/Services/TeamMemberAutoAcceptInvitation.test.ts +21 -1
  75. package/Tests/Server/Services/TeamMemberInviteRegistrationToken.test.ts +290 -0
  76. package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +948 -0
  77. package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +1 -0
  78. package/Tests/Server/Services/UserPushCriticalAlertToggle.test.ts +255 -0
  79. package/Tests/Server/Services/UserTotpAuthEnrolment.test.ts +218 -0
  80. package/Tests/Server/TestingUtils/AuthenticatorApp.ts +248 -0
  81. package/Tests/Server/Types/Database/Permissions/NotificationChannelVerificationColumns.test.ts +135 -0
  82. package/Tests/Server/Types/Database/Permissions/OnCallScheduleRelationSelect.test.ts +504 -0
  83. package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAnyOf.test.ts +91 -0
  84. package/Tests/Server/Types/Workflow/Components/JsonToText.test.ts +384 -0
  85. package/Tests/Server/Types/Workflow/Components/MergeJson.test.ts +403 -0
  86. package/Tests/Server/Utils/Browser.test.ts +162 -0
  87. package/Tests/Server/Utils/ChannelVerification.test.ts +689 -0
  88. package/Tests/Server/Utils/Express.test.ts +371 -0
  89. package/Tests/Server/Utils/Monitor/Criteria/CustomCodeMonitorCriteria.test.ts +707 -0
  90. package/Tests/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.test.ts +698 -0
  91. package/Tests/Server/Utils/Monitor/Criteria/IncomingEmailBodyCriteria.test.ts +243 -0
  92. package/Tests/Server/Utils/Monitor/Criteria/IncomingRequestBodyCriteria.test.ts +278 -0
  93. package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +160 -20
  94. package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +13 -8
  95. package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +162 -18
  96. package/Tests/Server/Utils/Monitor/MonitorResourceContext.test.ts +349 -0
  97. package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +690 -0
  98. package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +7 -2
  99. package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +92 -18
  100. package/Tests/Server/Utils/PushNotificationUtilCreators.test.ts +267 -0
  101. package/Tests/Server/Utils/Telemetry/OneuptimeLabel.test.ts +439 -0
  102. package/Tests/Server/Utils/TotpAuth.test.ts +719 -0
  103. package/Tests/Server/Utils/UserRegistrationToken.test.ts +322 -0
  104. package/Tests/Server/Utils/VerificationCode.test.ts +296 -0
  105. package/Tests/Types/AutoRemediation/AutoRemediationSuggestionStatus.test.ts +192 -0
  106. package/Tests/Types/Call/CallRequest.test.ts +189 -0
  107. package/Tests/Types/Dashboard/DashboardSize.test.ts +226 -0
  108. package/Tests/Types/Incident/IncidentSlaStatus.test.ts +191 -0
  109. package/Tests/Types/Metrics/MetricDashboardMetricType.test.ts +278 -0
  110. package/Tests/Types/Metrics/MetricPipelineRuleType.test.ts +517 -0
  111. package/Tests/Types/Monitor/IncomingMonitorDefaultCriteria.test.ts +470 -0
  112. package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +6 -4
  113. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +262 -5
  114. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +1 -1
  115. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +1 -1
  116. package/Tests/Types/Monitor/ServiceAlertTemplates.test.ts +905 -0
  117. package/Tests/Types/Service/ServiceLanguage.test.ts +255 -0
  118. package/Tests/Types/TextRandomGeneration.test.ts +211 -0
  119. package/Tests/UI/Components/Button.test.tsx +175 -1
  120. package/Tests/UI/Components/CodeBlockLanguages.test.tsx +236 -0
  121. package/Tests/UI/Components/CommandPalette/CommandPalette.test.tsx +568 -0
  122. package/Tests/UI/Components/CommandPalette/CommandPaletteKeyboard.test.tsx +239 -0
  123. package/Tests/UI/Components/CommandPalette/CommandPaletteProviders.test.tsx +400 -0
  124. package/Tests/UI/Components/CommandPalette/PaletteFilter.test.ts +234 -0
  125. package/Tests/UI/Components/CommandPalette/RecentCommands.test.ts +97 -0
  126. package/Tests/UI/Components/CountModelSideMenuItemGuard.test.tsx +198 -0
  127. package/Tests/UI/Components/FeedItemSafeMode.test.tsx +1 -1
  128. package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +267 -0
  129. package/Tests/UI/Components/List.test.tsx +9 -2
  130. package/Tests/UI/Components/ListLoadingStates.test.tsx +173 -0
  131. package/Tests/UI/Components/MarkdownLazy.test.tsx +113 -0
  132. package/Tests/UI/Components/MarkdownMermaidRetry.test.tsx +110 -0
  133. package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +8 -0
  134. package/Tests/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.test.ts +692 -0
  135. package/Tests/UI/Components/MoreMenuMotion.test.tsx +118 -0
  136. package/Tests/UI/Components/NavBarCommandKGating.test.tsx +178 -0
  137. package/Tests/UI/Components/OrderedStatesList.test.tsx +19 -2
  138. package/Tests/UI/Components/SideOverMotion.test.tsx +109 -0
  139. package/Tests/UI/Components/Skeleton.test.tsx +71 -0
  140. package/Tests/UI/Components/TableLoadingStates.test.tsx +279 -0
  141. package/Tests/UI/Components/Tabs.test.tsx +24 -0
  142. package/Tests/UI/Components/Toast.test.tsx +130 -7
  143. package/Tests/UI/Components/ToastStacking.test.tsx +198 -0
  144. package/Tests/UI/Utils/Dropdown.test.ts +423 -0
  145. package/Tests/UI/Utils/ModelAPITenantHeader.test.ts +122 -0
  146. package/Tests/UI/Utils/ModelListCache.test.ts +338 -0
  147. package/Tests/UI/Utils/ProjectListCacheInvalidation.test.ts +225 -0
  148. package/Tests/Utils/API.test.ts +16 -2
  149. package/Tests/Utils/Dashboard/Components/DashboardKubernetesResourceListShared.test.ts +347 -0
  150. package/Tests/Utils/Dashboard/Components/DashboardListSharedArgs.test.ts +259 -0
  151. package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +466 -0
  152. package/Tests/Utils/Rum/UrlScrubberSegments.test.ts +362 -0
  153. package/Types/Email/EmailTemplateType.ts +1 -0
  154. package/Types/Monitor/MonitorCriteriaInstance.ts +41 -20
  155. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +131 -8
  156. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +48 -8
  157. package/Types/Monitor/ServiceAlertTemplates.ts +1352 -0
  158. package/Types/Permission.ts +35 -0
  159. package/Types/PushNotification/AndroidNotificationChannel.ts +26 -0
  160. package/Types/PushNotification/PushNotificationMessage.ts +15 -0
  161. package/Types/PushNotification/PushNotificationRequest.ts +8 -16
  162. package/Types/Service/ServiceLanguage.ts +144 -0
  163. package/Types/Text.ts +111 -19
  164. package/UI/Components/Button/Button.tsx +27 -14
  165. package/UI/Components/CodeBlock/CodeBlock.tsx +54 -7
  166. package/UI/Components/CodeBlock/LanguageRegistry.ts +103 -0
  167. package/UI/Components/CommandPalette/CommandPalette.tsx +790 -0
  168. package/UI/Components/CommandPalette/PaletteFilter.ts +196 -0
  169. package/UI/Components/CommandPalette/PaletteRow.tsx +173 -0
  170. package/UI/Components/CommandPalette/RecentCommands.ts +57 -0
  171. package/UI/Components/CommandPalette/Types.ts +49 -0
  172. package/UI/Components/CommandPalette/UseProviderSearch.ts +129 -0
  173. package/UI/Components/Dropdown/Dropdown.tsx +6 -1
  174. package/UI/Components/Feed/FeedItem.tsx +1 -1
  175. package/UI/Components/List/List.tsx +37 -5
  176. package/UI/Components/List/ListSkeleton.tsx +54 -0
  177. package/UI/Components/Markdown.tsx/LazyMarkdownViewer.tsx +6 -1
  178. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +71 -26
  179. package/UI/Components/Modal/Modal.tsx +2 -2
  180. package/UI/Components/ModelTable/BaseModelTable.tsx +1 -1
  181. package/UI/Components/ModelTable/useCustomFieldColumns.ts +11 -5
  182. package/UI/Components/MoreMenu/MoreMenu.tsx +39 -1
  183. package/UI/Components/Navbar/NavBar.tsx +14 -1
  184. package/UI/Components/OrderedStatesList/OrderedStatesList.tsx +50 -5
  185. package/UI/Components/SideMenu/CountModelSideMenuItem.tsx +40 -1
  186. package/UI/Components/SideOver/SideOver.tsx +37 -2
  187. package/UI/Components/Skeleton/Skeleton.tsx +53 -0
  188. package/UI/Components/Table/Table.tsx +74 -44
  189. package/UI/Components/Table/TableSkeletonRows.tsx +141 -0
  190. package/UI/Components/Tabs/Tab.tsx +1 -1
  191. package/UI/Components/Toast/Toast.tsx +118 -53
  192. package/UI/Components/Toast/ToastInit.tsx +82 -32
  193. package/UI/Styles/Theme.css +40 -0
  194. package/UI/Utils/ModelAPI/ModelAPI.ts +20 -10
  195. package/UI/Utils/ModelListCache.ts +188 -0
  196. package/UI/Utils/Project.ts +29 -0
  197. package/Utils/API.ts +6 -1
  198. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +14 -0
  199. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  200. package/build/dist/Models/DatabaseModels/UserCall.js +101 -3
  201. package/build/dist/Models/DatabaseModels/UserCall.js.map +1 -1
  202. package/build/dist/Models/DatabaseModels/UserEmail.js +101 -3
  203. package/build/dist/Models/DatabaseModels/UserEmail.js.map +1 -1
  204. package/build/dist/Models/DatabaseModels/UserIncomingCallNumber.js +101 -3
  205. package/build/dist/Models/DatabaseModels/UserIncomingCallNumber.js.map +1 -1
  206. package/build/dist/Models/DatabaseModels/UserPush.js +38 -0
  207. package/build/dist/Models/DatabaseModels/UserPush.js.map +1 -1
  208. package/build/dist/Models/DatabaseModels/UserSMS.js +101 -3
  209. package/build/dist/Models/DatabaseModels/UserSMS.js.map +1 -1
  210. package/build/dist/Models/DatabaseModels/UserWhatsApp.js +100 -2
  211. package/build/dist/Models/DatabaseModels/UserWhatsApp.js.map +1 -1
  212. package/build/dist/Server/API/BaseAPI.js +69 -13
  213. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  214. package/build/dist/Server/API/UserCallAPI.js +42 -38
  215. package/build/dist/Server/API/UserCallAPI.js.map +1 -1
  216. package/build/dist/Server/API/UserEmailAPI.js +42 -38
  217. package/build/dist/Server/API/UserEmailAPI.js.map +1 -1
  218. package/build/dist/Server/API/UserIncomingCallNumberAPI.js +37 -32
  219. package/build/dist/Server/API/UserIncomingCallNumberAPI.js.map +1 -1
  220. package/build/dist/Server/API/UserNotificationMethodAdminAPI.js +341 -0
  221. package/build/dist/Server/API/UserNotificationMethodAdminAPI.js.map +1 -0
  222. package/build/dist/Server/API/UserPushAPI.js +95 -2
  223. package/build/dist/Server/API/UserPushAPI.js.map +1 -1
  224. package/build/dist/Server/API/UserSmsAPI.js +42 -38
  225. package/build/dist/Server/API/UserSmsAPI.js.map +1 -1
  226. package/build/dist/Server/API/UserWhatsAppAPI.js +42 -40
  227. package/build/dist/Server/API/UserWhatsAppAPI.js.map +1 -1
  228. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.js +89 -0
  229. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.js.map +1 -0
  230. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.js +12 -0
  231. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.js.map +1 -0
  232. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.js +40 -0
  233. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.js.map +1 -0
  234. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  235. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  236. package/build/dist/Server/Middleware/VerificationCodeRateLimit.js +393 -0
  237. package/build/dist/Server/Middleware/VerificationCodeRateLimit.js.map +1 -0
  238. package/build/dist/Server/Services/AnalyticsDatabaseService.js +136 -0
  239. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  240. package/build/dist/Server/Services/DatabaseService.js +74 -0
  241. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  242. package/build/dist/Server/Services/LogAggregationService.js +5 -0
  243. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  244. package/build/dist/Server/Services/PushNotificationService.js +69 -30
  245. package/build/dist/Server/Services/PushNotificationService.js.map +1 -1
  246. package/build/dist/Server/Services/TeamMemberService.js +43 -7
  247. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  248. package/build/dist/Server/Services/UserCallService.js +54 -22
  249. package/build/dist/Server/Services/UserCallService.js.map +1 -1
  250. package/build/dist/Server/Services/UserEmailService.js +78 -17
  251. package/build/dist/Server/Services/UserEmailService.js.map +1 -1
  252. package/build/dist/Server/Services/UserIncomingCallNumberService.js +45 -74
  253. package/build/dist/Server/Services/UserIncomingCallNumberService.js.map +1 -1
  254. package/build/dist/Server/Services/UserNotificationMethodAdminService.js +1117 -0
  255. package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -0
  256. package/build/dist/Server/Services/UserNotificationRuleService.js +20 -0
  257. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  258. package/build/dist/Server/Services/UserPushService.js +77 -0
  259. package/build/dist/Server/Services/UserPushService.js.map +1 -1
  260. package/build/dist/Server/Services/UserSmsService.js +50 -17
  261. package/build/dist/Server/Services/UserSmsService.js.map +1 -1
  262. package/build/dist/Server/Services/UserWhatsAppService.js +48 -15
  263. package/build/dist/Server/Services/UserWhatsAppService.js.map +1 -1
  264. package/build/dist/Server/Types/Database/QueryHelper.js +27 -0
  265. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  266. package/build/dist/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.js +46 -0
  267. package/build/dist/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.js.map +1 -1
  268. package/build/dist/Server/Utils/ChannelVerification.js +348 -0
  269. package/build/dist/Server/Utils/ChannelVerification.js.map +1 -0
  270. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +13 -12
  271. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
  272. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +20 -17
  273. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  274. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +17 -14
  275. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  276. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js +95 -0
  277. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js.map +1 -0
  278. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +313 -0
  279. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -0
  280. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +3 -3
  281. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +67 -26
  282. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -1
  283. package/build/dist/Server/Utils/TotpAuth.js +127 -8
  284. package/build/dist/Server/Utils/TotpAuth.js.map +1 -1
  285. package/build/dist/Server/Utils/UserRegistrationToken.js +170 -0
  286. package/build/dist/Server/Utils/UserRegistrationToken.js.map +1 -0
  287. package/build/dist/Server/Utils/VerificationCode.js +120 -0
  288. package/build/dist/Server/Utils/VerificationCode.js.map +1 -0
  289. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  290. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  291. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +42 -21
  292. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  293. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +77 -5
  294. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
  295. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +18 -6
  296. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
  297. package/build/dist/Types/Monitor/ServiceAlertTemplates.js +1028 -0
  298. package/build/dist/Types/Monitor/ServiceAlertTemplates.js.map +1 -0
  299. package/build/dist/Types/Permission.js +33 -0
  300. package/build/dist/Types/Permission.js.map +1 -1
  301. package/build/dist/Types/PushNotification/AndroidNotificationChannel.js +27 -0
  302. package/build/dist/Types/PushNotification/AndroidNotificationChannel.js.map +1 -0
  303. package/build/dist/Types/Service/ServiceLanguage.js +96 -0
  304. package/build/dist/Types/Service/ServiceLanguage.js.map +1 -0
  305. package/build/dist/Types/Text.js +82 -17
  306. package/build/dist/Types/Text.js.map +1 -1
  307. package/build/dist/UI/Components/Button/Button.js +22 -14
  308. package/build/dist/UI/Components/Button/Button.js.map +1 -1
  309. package/build/dist/UI/Components/CodeBlock/CodeBlock.js +35 -3
  310. package/build/dist/UI/Components/CodeBlock/CodeBlock.js.map +1 -1
  311. package/build/dist/UI/Components/CodeBlock/LanguageRegistry.js +57 -0
  312. package/build/dist/UI/Components/CodeBlock/LanguageRegistry.js.map +1 -0
  313. package/build/dist/UI/Components/CommandPalette/CommandPalette.js +439 -0
  314. package/build/dist/UI/Components/CommandPalette/CommandPalette.js.map +1 -0
  315. package/build/dist/UI/Components/CommandPalette/PaletteFilter.js +135 -0
  316. package/build/dist/UI/Components/CommandPalette/PaletteFilter.js.map +1 -0
  317. package/build/dist/UI/Components/CommandPalette/PaletteRow.js +83 -0
  318. package/build/dist/UI/Components/CommandPalette/PaletteRow.js.map +1 -0
  319. package/build/dist/UI/Components/CommandPalette/RecentCommands.js +47 -0
  320. package/build/dist/UI/Components/CommandPalette/RecentCommands.js.map +1 -0
  321. package/build/dist/UI/Components/CommandPalette/Types.js +2 -0
  322. package/build/dist/UI/Components/CommandPalette/Types.js.map +1 -0
  323. package/build/dist/UI/Components/CommandPalette/UseProviderSearch.js +88 -0
  324. package/build/dist/UI/Components/CommandPalette/UseProviderSearch.js.map +1 -0
  325. package/build/dist/UI/Components/Dropdown/Dropdown.js +6 -1
  326. package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
  327. package/build/dist/UI/Components/Feed/FeedItem.js +1 -1
  328. package/build/dist/UI/Components/Feed/FeedItem.js.map +1 -1
  329. package/build/dist/UI/Components/List/List.js +19 -5
  330. package/build/dist/UI/Components/List/List.js.map +1 -1
  331. package/build/dist/UI/Components/List/ListSkeleton.js +21 -0
  332. package/build/dist/UI/Components/List/ListSkeleton.js.map +1 -0
  333. package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js.map +1 -1
  334. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +52 -25
  335. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  336. package/build/dist/UI/Components/Modal/Modal.js +2 -2
  337. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +1 -1
  338. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  339. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +9 -2
  340. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
  341. package/build/dist/UI/Components/MoreMenu/MoreMenu.js +32 -1
  342. package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
  343. package/build/dist/UI/Components/Navbar/NavBar.js +5 -1
  344. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  345. package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js +29 -5
  346. package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js.map +1 -1
  347. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js +23 -1
  348. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js.map +1 -1
  349. package/build/dist/UI/Components/SideOver/SideOver.js +23 -2
  350. package/build/dist/UI/Components/SideOver/SideOver.js.map +1 -1
  351. package/build/dist/UI/Components/Skeleton/Skeleton.js +23 -0
  352. package/build/dist/UI/Components/Skeleton/Skeleton.js.map +1 -0
  353. package/build/dist/UI/Components/Table/Table.js +35 -24
  354. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  355. package/build/dist/UI/Components/Table/TableSkeletonRows.js +52 -0
  356. package/build/dist/UI/Components/Table/TableSkeletonRows.js.map +1 -0
  357. package/build/dist/UI/Components/Tabs/Tab.js +1 -1
  358. package/build/dist/UI/Components/Tabs/Tab.js.map +1 -1
  359. package/build/dist/UI/Components/Toast/Toast.js +77 -23
  360. package/build/dist/UI/Components/Toast/Toast.js.map +1 -1
  361. package/build/dist/UI/Components/Toast/ToastInit.js +37 -15
  362. package/build/dist/UI/Components/Toast/ToastInit.js.map +1 -1
  363. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +18 -9
  364. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
  365. package/build/dist/UI/Utils/ModelListCache.js +126 -0
  366. package/build/dist/UI/Utils/ModelListCache.js.map +1 -0
  367. package/build/dist/UI/Utils/Project.js +24 -1
  368. package/build/dist/UI/Utils/Project.js.map +1 -1
  369. package/build/dist/Utils/API.js +6 -1
  370. package/build/dist/Utils/API.js.map +1 -1
  371. package/jest.config.json +1 -1
  372. package/package.json +2 -3
  373. package/Server/Utils/Monitor/MonitorClusterContext.ts +0 -182
  374. package/Tests/__mocks__/otpauth.js +0 -30
  375. package/build/dist/Server/Utils/Monitor/MonitorClusterContext.js +0 -141
  376. package/build/dist/Server/Utils/Monitor/MonitorClusterContext.js.map +0 -1
@@ -0,0 +1,1352 @@
1
+ import { SpanStatus } from "../../Models/AnalyticsModels/Span";
2
+ import ObjectID from "../ObjectID";
3
+ import MonitorStep from "./MonitorStep";
4
+ import MonitorCriteria from "./MonitorCriteria";
5
+ import MonitorCriteriaInstance from "./MonitorCriteriaInstance";
6
+ import FilterCondition from "../Filter/FilterCondition";
7
+ import { CheckOn, EvaluateOverTimeType, FilterType } from "./CriteriaFilter";
8
+ import MonitorType from "./MonitorType";
9
+ import RollingTime from "../RollingTime/RollingTime";
10
+ import MetricsAggregationType from "../Metrics/MetricsAggregationType";
11
+ import MetricQueryConfigData from "../Metrics/MetricQueryConfigData";
12
+ import MetricFormulaConfigData from "../Metrics/MetricFormulaConfigData";
13
+ import { ServiceLanguage } from "../Service/ServiceLanguage";
14
+
15
+ /*
16
+ * The curated alert library for an APM telemetry service — a backend process
17
+ * reporting OpenTelemetry traces, metrics and exceptions under one
18
+ * `service.name`.
19
+ *
20
+ * WHAT MAKES THIS MODULE DIFFERENT FROM THE OTHER NINE
21
+ *
22
+ * Every other `<X>AlertTemplates.ts` returns a constant: every Kubernetes
23
+ * cluster is offered the same eighteen templates. A service's useful alerts
24
+ * are not a constant, because the signals a runtime emits are a property of
25
+ * the runtime. "Heap above 90% of -Xmx" is the single best leading indicator
26
+ * of a Java outage and is meaningless on a Go service, which has no heap
27
+ * limit metric and instead leaks goroutines. Offering JVM templates to a Go
28
+ * service does not produce a wrong alert — it produces a monitor that queries
29
+ * a metric nobody emits and therefore never fires, which is the worst outcome
30
+ * available: the team believes they are covered.
31
+ *
32
+ * So templates carry a `language` discriminator and callers ask for the set
33
+ * that matches the service's detected runtime — see `getServiceAlertTemplates`.
34
+ * Templates with no `language` apply to every service whatever it runs.
35
+ *
36
+ * THREE CONSTRAINTS THE PLATFORM IMPOSES, WHICH SHAPE EVERY TEMPLATE HERE
37
+ *
38
+ * 1. Only one CheckOn exists per telemetry monitor type: `MetricValue` for
39
+ * Metrics, `SpanCount` for Traces, `ExceptionCount` for Exceptions. There
40
+ * is no span-duration CheckOn (`TraceMonitorResponse` carries only a
41
+ * count), so latency alerts MUST be metric monitors over a duration
42
+ * histogram, never trace monitors.
43
+ *
44
+ * 2. Cumulative monotonic counters are unusable. Ingest stores OTel counters
45
+ * raw — the per-second-rate transform on `MetricQueryConfigData` is a
46
+ * chart-side transform the monitor worker never reads — so thresholding
47
+ * one compares against a since-process-start total that only grows: it
48
+ * fires once and never clears. That disqualifies the most tempting
49
+ * metrics in three languages (`dotnet.exceptions`,
50
+ * `cpython.gc.collections`, `process.runtime.go.gc.count`) and is why
51
+ * counting signals below go through trace and exception monitors, which
52
+ * count rows in a rolling window. Everything thresholded here is a gauge,
53
+ * an UpDownCounter, or a histogram read through a percentile.
54
+ *
55
+ * 3. A monitor holds exactly ONE metric name — there is no server-side
56
+ * equivalent of the dashboard's candidate probing (`probeRuntimeCharts`,
57
+ * which tries a stable name then a legacy one and keeps whichever has
58
+ * data). Every template below therefore targets the STABLE semantic
59
+ * convention name, and names the pre-stabilization metric in its
60
+ * description so a team on an older SDK can retarget the created monitor
61
+ * in one edit rather than wonder why it is silent.
62
+ *
63
+ * The metric names, and the attribute filters that go with them, are taken
64
+ * verbatim from the service overview's runtime charts
65
+ * (`serviceGoldenMetrics.ts`) wherever the two overlap, so a chart the user
66
+ * can see and an alert they can create describe the same number.
67
+ */
68
+
69
+ export type ServiceAlertTemplateCategory =
70
+ | "Errors"
71
+ | "Latency"
72
+ | "Throughput"
73
+ | "Saturation"
74
+ | "JVM Runtime"
75
+ | ".NET Runtime"
76
+ | "Node.js Runtime"
77
+ | "Python Runtime"
78
+ | "Go Runtime";
79
+
80
+ export type ServiceAlertTemplateSeverity = "Critical" | "Warning";
81
+
82
+ export interface ServiceAlertTemplateArgs {
83
+ serviceId: string;
84
+ onlineMonitorStatusId: ObjectID;
85
+ offlineMonitorStatusId: ObjectID;
86
+ defaultIncidentSeverityId: ObjectID;
87
+ defaultAlertSeverityId: ObjectID;
88
+ monitorName: string;
89
+ }
90
+
91
+ export interface ServiceAlertTemplate {
92
+ id: string;
93
+ name: string;
94
+ description: string;
95
+ category: ServiceAlertTemplateCategory;
96
+ severity: ServiceAlertTemplateSeverity;
97
+ monitorType: MonitorType;
98
+ /*
99
+ * The runtime this template is for. Undefined means "every service" — the
100
+ * RED signals and the process-level ones, which come from spans, exceptions
101
+ * and the host-metrics instrumentation rather than from any runtime.
102
+ */
103
+ language?: ServiceLanguage | undefined;
104
+ getMonitorStep: (args: ServiceAlertTemplateArgs) => MonitorStep;
105
+ }
106
+
107
+ /*
108
+ * `EvaluateOverTimeType` names the window collapse, and its two members used
109
+ * here do NOT mean what their names suggest — worth stating once, here, rather
110
+ * than being rediscovered per template:
111
+ *
112
+ * AnyValue -> ANY bucket in the rolling window breaching is a breach.
113
+ * Right for spikes.
114
+ * AllValues -> EVERY bucket must breach. Right for sustained pressure, and
115
+ * what the comparators actually do for every member that is not
116
+ * `AnyValue` (they fall through to `.every()`; there is no
117
+ * averaging on this code path despite `Average` existing).
118
+ *
119
+ * A window with no samples at all never reaches the comparator — the
120
+ * evaluator's no-data guard returns "not breaching" under the default
121
+ * `NoDataPolicy.Ignore` — so `AllValues` cannot fire on an empty window.
122
+ *
123
+ * Applying `AllValues` to the healthy criteria too, as these templates do,
124
+ * buys hysteresis for free: a window where some buckets breach and others do
125
+ * not matches neither criteria, so the monitor holds its current status
126
+ * instead of flipping on every evaluation while a metric sits on the
127
+ * threshold. Alerting on a metric that hovers is the fastest way to teach a
128
+ * team to mute it.
129
+ */
130
+ const SUSTAINED: EvaluateOverTimeType = EvaluateOverTimeType.AllValues;
131
+
132
+ interface CountCriteriaArgs {
133
+ args: ServiceAlertTemplateArgs;
134
+ checkOn: CheckOn.SpanCount | CheckOn.ExceptionCount;
135
+ unhealthyFilterType: FilterType;
136
+ healthyFilterType: FilterType;
137
+ threshold: number;
138
+ unhealthyName: string;
139
+ unhealthyDescription: string;
140
+ incidentTitle: string;
141
+ incidentDescription: string;
142
+ }
143
+
144
+ interface MetricCriteriaArgs {
145
+ args: ServiceAlertTemplateArgs;
146
+ unhealthyFilterType: FilterType;
147
+ healthyFilterType: FilterType;
148
+ threshold: number;
149
+ metricAlias: string;
150
+ thresholdUnit?: string | undefined;
151
+ unhealthyName: string;
152
+ unhealthyDescription: string;
153
+ incidentTitle: string;
154
+ incidentDescription: string;
155
+ }
156
+
157
+ /*
158
+ * The unhealthy/healthy criteria pair every template ships.
159
+ *
160
+ * Both instances are always produced, with mirrored filter types, because a
161
+ * monitor with no healthy criteria never comes back online: it opens an
162
+ * incident, auto-resolves it (the incidents carry `autoResolveIncident`), and
163
+ * then sits in its offline status forever with nothing to move it back.
164
+ */
165
+ function buildCriteriaPair(data: {
166
+ args: ServiceAlertTemplateArgs;
167
+ checkOn: CheckOn;
168
+ unhealthyFilterType: FilterType;
169
+ healthyFilterType: FilterType;
170
+ threshold: number;
171
+ metricAlias?: string | undefined;
172
+ thresholdUnit?: string | undefined;
173
+ unhealthyName: string;
174
+ unhealthyDescription: string;
175
+ incidentTitle: string;
176
+ incidentDescription: string;
177
+ healthyDescription: string;
178
+ }): MonitorCriteria {
179
+ const metricMonitorOptions:
180
+ | {
181
+ metricAggregationType: EvaluateOverTimeType;
182
+ metricAlias: string;
183
+ thresholdUnit?: string | undefined;
184
+ }
185
+ | undefined = data.metricAlias
186
+ ? {
187
+ metricAggregationType: SUSTAINED,
188
+ metricAlias: data.metricAlias,
189
+ thresholdUnit: data.thresholdUnit,
190
+ }
191
+ : undefined;
192
+
193
+ const unhealthy: MonitorCriteriaInstance = new MonitorCriteriaInstance();
194
+
195
+ unhealthy.data = {
196
+ id: ObjectID.generate().toString(),
197
+ monitorStatusId: data.args.offlineMonitorStatusId,
198
+ filterCondition: FilterCondition.Any,
199
+ filters: [
200
+ {
201
+ checkOn: data.checkOn,
202
+ filterType: data.unhealthyFilterType,
203
+ value: data.threshold,
204
+ metricMonitorOptions: metricMonitorOptions,
205
+ },
206
+ ],
207
+ incidents: [
208
+ {
209
+ title: data.incidentTitle,
210
+ description: data.incidentDescription,
211
+ incidentSeverityId: data.args.defaultIncidentSeverityId,
212
+ autoResolveIncident: true,
213
+ id: ObjectID.generate().toString(),
214
+ onCallPolicyIds: [],
215
+ },
216
+ ],
217
+ alerts: [
218
+ {
219
+ title: data.incidentTitle,
220
+ description: data.incidentDescription,
221
+ alertSeverityId: data.args.defaultAlertSeverityId,
222
+ autoResolveAlert: true,
223
+ id: ObjectID.generate().toString(),
224
+ onCallPolicyIds: [],
225
+ },
226
+ ],
227
+ changeMonitorStatus: true,
228
+ createIncidents: true,
229
+ createAlerts: true,
230
+ name: data.unhealthyName,
231
+ description: data.unhealthyDescription,
232
+ };
233
+
234
+ const healthy: MonitorCriteriaInstance = new MonitorCriteriaInstance();
235
+
236
+ healthy.data = {
237
+ id: ObjectID.generate().toString(),
238
+ monitorStatusId: data.args.onlineMonitorStatusId,
239
+ filterCondition: FilterCondition.Any,
240
+ filters: [
241
+ {
242
+ checkOn: data.checkOn,
243
+ filterType: data.healthyFilterType,
244
+ value: data.threshold,
245
+ metricMonitorOptions: metricMonitorOptions,
246
+ },
247
+ ],
248
+ incidents: [],
249
+ alerts: [],
250
+ changeMonitorStatus: true,
251
+ createIncidents: false,
252
+ createAlerts: false,
253
+ name: "Healthy",
254
+ description: data.healthyDescription,
255
+ };
256
+
257
+ const criteria: MonitorCriteria = new MonitorCriteria();
258
+ criteria.data = {
259
+ monitorCriteriaInstanceArray: [unhealthy, healthy],
260
+ };
261
+
262
+ return criteria;
263
+ }
264
+
265
+ function buildCountCriteria(data: CountCriteriaArgs): MonitorCriteria {
266
+ return buildCriteriaPair({
267
+ args: data.args,
268
+ checkOn: data.checkOn,
269
+ unhealthyFilterType: data.unhealthyFilterType,
270
+ healthyFilterType: data.healthyFilterType,
271
+ threshold: data.threshold,
272
+ unhealthyName: data.unhealthyName,
273
+ unhealthyDescription: data.unhealthyDescription,
274
+ incidentTitle: data.incidentTitle,
275
+ incidentDescription: data.incidentDescription,
276
+ healthyDescription: `${data.args.monitorName} is within its recommended threshold.`,
277
+ });
278
+ }
279
+
280
+ function buildMetricCriteria(data: MetricCriteriaArgs): MonitorCriteria {
281
+ return buildCriteriaPair({
282
+ args: data.args,
283
+ checkOn: CheckOn.MetricValue,
284
+ unhealthyFilterType: data.unhealthyFilterType,
285
+ healthyFilterType: data.healthyFilterType,
286
+ threshold: data.threshold,
287
+ metricAlias: data.metricAlias,
288
+ thresholdUnit: data.thresholdUnit,
289
+ unhealthyName: data.unhealthyName,
290
+ unhealthyDescription: data.unhealthyDescription,
291
+ incidentTitle: data.incidentTitle,
292
+ incidentDescription: data.incidentDescription,
293
+ healthyDescription: `${data.args.monitorName} is within its recommended threshold.`,
294
+ });
295
+ }
296
+
297
+ /*
298
+ * One metric query.
299
+ *
300
+ * `metricVariable` is the alias the criteria filter names, and getting it
301
+ * wrong is silent: an alias that matches no query falls back to query result
302
+ * slot 0, so a two-query template with a typo'd alias would threshold the
303
+ * wrong series. Every alias below is passed to both the query config and its
304
+ * criteria from the same local constant for exactly that reason.
305
+ *
306
+ * `legendUnit` is what makes a threshold portable across SDKs that report the
307
+ * same metric in different units: the worker converts each sample from the
308
+ * metric's own reported unit into this one before comparing. Setting it to
309
+ * `"ms"` on a semconv duration histogram (reported in seconds) is what lets
310
+ * the threshold below be written as a readable millisecond count.
311
+ */
312
+ function buildQueryConfig(data: {
313
+ metricName: string;
314
+ metricAlias: string;
315
+ title: string;
316
+ aggregationType: MetricsAggregationType;
317
+ attributes?: Record<string, string> | undefined;
318
+ legendUnit?: string | undefined;
319
+ }): MetricQueryConfigData {
320
+ return {
321
+ metricAliasData: {
322
+ metricVariable: data.metricAlias,
323
+ title: data.title,
324
+ description: data.title,
325
+ legend: data.title,
326
+ legendUnit: data.legendUnit,
327
+ },
328
+ metricQueryData: {
329
+ filterData: {
330
+ metricName: data.metricName,
331
+ attributes: data.attributes || {},
332
+ aggegationType: data.aggregationType,
333
+ aggregateBy: {},
334
+ },
335
+ },
336
+ };
337
+ }
338
+
339
+ /*
340
+ * A single-metric threshold template: one query, one criteria pair.
341
+ */
342
+ function buildMetricTemplate(data: {
343
+ id: string;
344
+ name: string;
345
+ description: string;
346
+ category: ServiceAlertTemplateCategory;
347
+ severity: ServiceAlertTemplateSeverity;
348
+ language?: ServiceLanguage | undefined;
349
+ metricName: string;
350
+ metricAlias: string;
351
+ aggregationType: MetricsAggregationType;
352
+ attributes?: Record<string, string> | undefined;
353
+ legendUnit?: string | undefined;
354
+ threshold: number;
355
+ thresholdLabel: string;
356
+ rollingTime?: RollingTime | undefined;
357
+ unhealthyFilterType?: FilterType | undefined;
358
+ healthyFilterType?: FilterType | undefined;
359
+ incidentDescription: string;
360
+ }): ServiceAlertTemplate {
361
+ return {
362
+ id: data.id,
363
+ name: data.name,
364
+ description: data.description,
365
+ category: data.category,
366
+ severity: data.severity,
367
+ monitorType: MonitorType.Metrics,
368
+ language: data.language,
369
+ getMonitorStep: (args: ServiceAlertTemplateArgs): MonitorStep => {
370
+ const step: MonitorStep = MonitorStep.getDefaultMonitorStep({
371
+ monitorName: args.monitorName,
372
+ monitorType: MonitorType.Metrics,
373
+ onlineMonitorStatusId: args.onlineMonitorStatusId,
374
+ offlineMonitorStatusId: args.offlineMonitorStatusId,
375
+ defaultIncidentSeverityId: args.defaultIncidentSeverityId,
376
+ defaultAlertSeverityId: args.defaultAlertSeverityId,
377
+ });
378
+
379
+ step.setMetricMonitor({
380
+ telemetryServiceIds: [new ObjectID(args.serviceId)],
381
+ rollingTime: data.rollingTime || RollingTime.Past5Minutes,
382
+ metricViewConfig: {
383
+ queryConfigs: [
384
+ buildQueryConfig({
385
+ metricName: data.metricName,
386
+ metricAlias: data.metricAlias,
387
+ title: data.name,
388
+ aggregationType: data.aggregationType,
389
+ attributes: data.attributes,
390
+ legendUnit: data.legendUnit,
391
+ }),
392
+ ],
393
+ formulaConfigs: [],
394
+ },
395
+ });
396
+
397
+ step.setMonitorCriteria(
398
+ buildMetricCriteria({
399
+ args: args,
400
+ unhealthyFilterType:
401
+ data.unhealthyFilterType || FilterType.GreaterThanOrEqualTo,
402
+ healthyFilterType: data.healthyFilterType || FilterType.LessThan,
403
+ threshold: data.threshold,
404
+ metricAlias: data.metricAlias,
405
+ thresholdUnit: data.legendUnit,
406
+ unhealthyName: `${data.name} - ${data.thresholdLabel} or worse`,
407
+ unhealthyDescription: `Triggers when ${data.metricName} stays at ${data.thresholdLabel} or worse for the whole evaluation window.`,
408
+ incidentTitle: `[Service] ${data.name} - ${args.monitorName}`,
409
+ incidentDescription: data.incidentDescription,
410
+ }),
411
+ );
412
+
413
+ return step;
414
+ },
415
+ };
416
+ }
417
+
418
+ /*
419
+ * A ratio template: two queries and a formula, thresholded as a percentage.
420
+ *
421
+ * This is the shape worth reaching for whenever the runtime reports both a
422
+ * usage and its limit, because it is the only kind of memory threshold that is
423
+ * portable. "Heap above 2 GB" is a guess about someone else's `-Xmx`; "heap
424
+ * above 90% of the limit the JVM was actually given" is true on a 512 MB
425
+ * sidecar and a 64 GB batch node alike, with no tuning.
426
+ *
427
+ * The criteria filter names the FORMULA's alias, not either query's — the
428
+ * evaluator resolves an alias against the query configs first and the formula
429
+ * configs second, so the three aliases must all differ.
430
+ */
431
+ function buildRatioTemplate(data: {
432
+ id: string;
433
+ name: string;
434
+ description: string;
435
+ category: ServiceAlertTemplateCategory;
436
+ severity: ServiceAlertTemplateSeverity;
437
+ language?: ServiceLanguage | undefined;
438
+ numeratorMetricName: string;
439
+ numeratorAttributes?: Record<string, string> | undefined;
440
+ denominatorMetricName: string;
441
+ denominatorAttributes?: Record<string, string> | undefined;
442
+ numeratorAlias: string;
443
+ denominatorAlias: string;
444
+ resultAlias: string;
445
+ thresholdPercent: number;
446
+ incidentDescription: string;
447
+ }): ServiceAlertTemplate {
448
+ return {
449
+ id: data.id,
450
+ name: data.name,
451
+ description: data.description,
452
+ category: data.category,
453
+ severity: data.severity,
454
+ monitorType: MonitorType.Metrics,
455
+ language: data.language,
456
+ getMonitorStep: (args: ServiceAlertTemplateArgs): MonitorStep => {
457
+ const step: MonitorStep = MonitorStep.getDefaultMonitorStep({
458
+ monitorName: args.monitorName,
459
+ monitorType: MonitorType.Metrics,
460
+ onlineMonitorStatusId: args.onlineMonitorStatusId,
461
+ offlineMonitorStatusId: args.offlineMonitorStatusId,
462
+ defaultIncidentSeverityId: args.defaultIncidentSeverityId,
463
+ defaultAlertSeverityId: args.defaultAlertSeverityId,
464
+ });
465
+
466
+ const formulaConfig: MetricFormulaConfigData = {
467
+ metricAliasData: {
468
+ metricVariable: data.resultAlias,
469
+ title: data.name,
470
+ description: data.name,
471
+ legend: data.name,
472
+ legendUnit: "%",
473
+ },
474
+ metricFormulaData: {
475
+ metricFormula: `(${data.numeratorAlias} / ${data.denominatorAlias}) * 100`,
476
+ },
477
+ };
478
+
479
+ step.setMetricMonitor({
480
+ telemetryServiceIds: [new ObjectID(args.serviceId)],
481
+ rollingTime: RollingTime.Past5Minutes,
482
+ metricViewConfig: {
483
+ /*
484
+ * Both sides are summed rather than averaged. A runtime reports
485
+ * usage and limit per memory pool, so a service with three heap
486
+ * pools produces three series on each side; summing gives
487
+ * "total used / total limit", which is the number a human means by
488
+ * "heap is 90% full".
489
+ */
490
+ queryConfigs: [
491
+ buildQueryConfig({
492
+ metricName: data.numeratorMetricName,
493
+ metricAlias: data.numeratorAlias,
494
+ title: `${data.name} (used)`,
495
+ aggregationType: MetricsAggregationType.Sum,
496
+ attributes: data.numeratorAttributes,
497
+ }),
498
+ buildQueryConfig({
499
+ metricName: data.denominatorMetricName,
500
+ metricAlias: data.denominatorAlias,
501
+ title: `${data.name} (limit)`,
502
+ aggregationType: MetricsAggregationType.Sum,
503
+ attributes: data.denominatorAttributes,
504
+ }),
505
+ ],
506
+ formulaConfigs: [formulaConfig],
507
+ },
508
+ });
509
+
510
+ step.setMonitorCriteria(
511
+ buildMetricCriteria({
512
+ args: args,
513
+ unhealthyFilterType: FilterType.GreaterThanOrEqualTo,
514
+ healthyFilterType: FilterType.LessThan,
515
+ threshold: data.thresholdPercent,
516
+ metricAlias: data.resultAlias,
517
+ thresholdUnit: "%",
518
+ unhealthyName: `${data.name} - ${data.thresholdPercent}% or higher`,
519
+ unhealthyDescription: `Triggers when ${data.numeratorMetricName} stays at ${data.thresholdPercent}% or more of ${data.denominatorMetricName} for the whole evaluation window.`,
520
+ incidentTitle: `[Service] ${data.name} - ${args.monitorName}`,
521
+ incidentDescription: data.incidentDescription,
522
+ }),
523
+ );
524
+
525
+ return step;
526
+ },
527
+ };
528
+ }
529
+
530
+ /*
531
+ * A span-count template.
532
+ *
533
+ * `spanStatuses: [SpanStatus.Error]` is what turns "how busy is this service"
534
+ * into "how much of it is failing" — the count query filters to error-status
535
+ * spans server-side, so the threshold is a failure count and not a traffic
536
+ * count.
537
+ */
538
+ function buildSpanCountTemplate(data: {
539
+ id: string;
540
+ name: string;
541
+ description: string;
542
+ category: ServiceAlertTemplateCategory;
543
+ severity: ServiceAlertTemplateSeverity;
544
+ spanStatuses: Array<SpanStatus>;
545
+ lastXSecondsOfSpans: number;
546
+ threshold: number;
547
+ unhealthyFilterType: FilterType;
548
+ healthyFilterType: FilterType;
549
+ unhealthyName: string;
550
+ unhealthyDescription: string;
551
+ incidentDescription: string;
552
+ }): ServiceAlertTemplate {
553
+ return {
554
+ id: data.id,
555
+ name: data.name,
556
+ description: data.description,
557
+ category: data.category,
558
+ severity: data.severity,
559
+ monitorType: MonitorType.Traces,
560
+ getMonitorStep: (args: ServiceAlertTemplateArgs): MonitorStep => {
561
+ const step: MonitorStep = MonitorStep.getDefaultMonitorStep({
562
+ monitorName: args.monitorName,
563
+ monitorType: MonitorType.Traces,
564
+ onlineMonitorStatusId: args.onlineMonitorStatusId,
565
+ offlineMonitorStatusId: args.offlineMonitorStatusId,
566
+ defaultIncidentSeverityId: args.defaultIncidentSeverityId,
567
+ defaultAlertSeverityId: args.defaultAlertSeverityId,
568
+ });
569
+
570
+ step.setTraceMonitor({
571
+ attributes: {},
572
+ spanName: "",
573
+ spanStatuses: data.spanStatuses,
574
+ telemetryServiceIds: [new ObjectID(args.serviceId)],
575
+ entityKeys: [],
576
+ lastXSecondsOfSpans: data.lastXSecondsOfSpans,
577
+ });
578
+
579
+ step.setMonitorCriteria(
580
+ buildCountCriteria({
581
+ args: args,
582
+ checkOn: CheckOn.SpanCount,
583
+ unhealthyFilterType: data.unhealthyFilterType,
584
+ healthyFilterType: data.healthyFilterType,
585
+ threshold: data.threshold,
586
+ unhealthyName: data.unhealthyName,
587
+ unhealthyDescription: data.unhealthyDescription,
588
+ incidentTitle: `[Service] ${data.name} - ${args.monitorName}`,
589
+ incidentDescription: data.incidentDescription,
590
+ }),
591
+ );
592
+
593
+ return step;
594
+ },
595
+ };
596
+ }
597
+
598
+ // --- Language-agnostic templates ---
599
+
600
+ /*
601
+ * Ten minutes rather than the five the RUM catalog uses for its equivalent.
602
+ * A browser session is a few seconds long, so RUM wants the shortest window
603
+ * that can hold a signal; a backend service is judged over the window its SLO
604
+ * is written against, and ten minutes rides out the single transient blip that
605
+ * would otherwise page someone for a request that had already been retried
606
+ * successfully.
607
+ *
608
+ * It also keeps this template's coverage fingerprint distinct from
609
+ * `rum-failed-user-operations`, which is otherwise structurally identical to
610
+ * it — same config kind, same error-status filter, same threshold. The
611
+ * fingerprint separates them today by resource identifier (a Service row id
612
+ * can never equal a RumApplication row id), but the evaluation window is what
613
+ * separates them on their own terms. Shortening this back to 300 makes the two
614
+ * indistinguishable to anything that compares templates rather than monitors.
615
+ */
616
+ const failedOperationsTemplate: ServiceAlertTemplate = buildSpanCountTemplate({
617
+ id: "service-failed-operations",
618
+ name: "Failed Operations",
619
+ description:
620
+ "Alert when the service reports any error-status span in ten minutes. The earliest signal that requests are failing, whatever the cause.",
621
+ category: "Errors",
622
+ severity: "Warning",
623
+ spanStatuses: [SpanStatus.Error],
624
+ lastXSecondsOfSpans: 600,
625
+ threshold: 0,
626
+ unhealthyFilterType: FilterType.GreaterThan,
627
+ healthyFilterType: FilterType.LessThanOrEqualTo,
628
+ unhealthyName: "Error spans detected",
629
+ unhealthyDescription:
630
+ "Triggers when at least one error-status span is reported in ten minutes.",
631
+ incidentDescription:
632
+ "The service reported an error-status span. Open the trace to see which operation failed, what it was called by, and which downstream dependency it was waiting on.",
633
+ });
634
+
635
+ /*
636
+ * Warning above, Critical here, on the same signal at a much higher bar.
637
+ * A backend service that has never once returned an error is rare; one
638
+ * returning twenty-five in five minutes is having an incident. Shipping both
639
+ * lets a team pick the sensitivity that matches their traffic instead of
640
+ * picking between "pages constantly" and "nothing".
641
+ */
642
+ const errorBurstTemplate: ServiceAlertTemplate = buildSpanCountTemplate({
643
+ id: "service-error-burst",
644
+ name: "Error Burst",
645
+ description:
646
+ "Alert when error-status spans exceed 25 in five minutes — a failure rate well past routine noise.",
647
+ category: "Errors",
648
+ severity: "Critical",
649
+ spanStatuses: [SpanStatus.Error],
650
+ lastXSecondsOfSpans: 300,
651
+ threshold: 25,
652
+ unhealthyFilterType: FilterType.GreaterThan,
653
+ healthyFilterType: FilterType.LessThanOrEqualTo,
654
+ unhealthyName: "Error spans above 25 in five minutes",
655
+ unhealthyDescription:
656
+ "Triggers when more than 25 error-status spans are reported in five minutes.",
657
+ incidentDescription:
658
+ "The service is failing at a rate well above routine noise. Group the failing spans by operation and by downstream dependency to find the common factor, and check what deployed recently.",
659
+ });
660
+
661
+ /*
662
+ * Fires when a service goes quiet, so the healthy/unhealthy filter types are
663
+ * inverted relative to every other template here: below the threshold is the
664
+ * bad state.
665
+ *
666
+ * Fifteen minutes rather than five, and Warning rather than Critical, because
667
+ * plenty of legitimate services are idle at 3am. It is offered rather than
668
+ * assumed — for an always-on request path it is the cheapest possible
669
+ * liveness check, needing no health endpoint and no probe.
670
+ */
671
+ const trafficStoppedTemplate: ServiceAlertTemplate = buildSpanCountTemplate({
672
+ id: "service-traffic-stopped",
673
+ name: "Traffic Stopped",
674
+ description:
675
+ "Alert when the service produces no spans at all for fifteen minutes. Best on an always-on request path; a service with idle periods will trigger it legitimately.",
676
+ category: "Throughput",
677
+ severity: "Warning",
678
+ spanStatuses: [],
679
+ lastXSecondsOfSpans: 900,
680
+ threshold: 1,
681
+ unhealthyFilterType: FilterType.LessThan,
682
+ healthyFilterType: FilterType.GreaterThanOrEqualTo,
683
+ unhealthyName: "No spans in fifteen minutes",
684
+ unhealthyDescription:
685
+ "Triggers when the service reports no spans at all over fifteen minutes.",
686
+ incidentDescription:
687
+ "The service has stopped producing telemetry. Either it is down, it lost its route to the collector, or its callers stopped calling — check the process, then the exporter, then upstream traffic.",
688
+ });
689
+
690
+ const unhandledExceptionsTemplate: ServiceAlertTemplate = {
691
+ id: "service-unhandled-exceptions",
692
+ name: "Unhandled Exceptions",
693
+ description:
694
+ "Alert when the service reports any unresolved exception in ten minutes. Exceptions already marked resolved or archived are excluded, so acknowledging one closes the alert.",
695
+ category: "Errors",
696
+ severity: "Critical",
697
+ monitorType: MonitorType.Exceptions,
698
+ getMonitorStep: (args: ServiceAlertTemplateArgs): MonitorStep => {
699
+ const step: MonitorStep = MonitorStep.getDefaultMonitorStep({
700
+ monitorName: args.monitorName,
701
+ monitorType: MonitorType.Exceptions,
702
+ onlineMonitorStatusId: args.onlineMonitorStatusId,
703
+ offlineMonitorStatusId: args.offlineMonitorStatusId,
704
+ defaultIncidentSeverityId: args.defaultIncidentSeverityId,
705
+ defaultAlertSeverityId: args.defaultAlertSeverityId,
706
+ });
707
+
708
+ step.setExceptionMonitor({
709
+ telemetryServiceIds: [new ObjectID(args.serviceId)],
710
+ entityKeys: [],
711
+ exceptionTypes: [],
712
+ message: "",
713
+ includeResolved: false,
714
+ includeArchived: false,
715
+ // Ten minutes, for the reasons given on failedOperationsTemplate.
716
+ lastXSecondsOfExceptions: 600,
717
+ });
718
+
719
+ step.setMonitorCriteria(
720
+ buildCountCriteria({
721
+ args: args,
722
+ checkOn: CheckOn.ExceptionCount,
723
+ unhealthyFilterType: FilterType.GreaterThan,
724
+ healthyFilterType: FilterType.LessThanOrEqualTo,
725
+ threshold: 0,
726
+ unhealthyName: "Unhandled exception detected",
727
+ unhealthyDescription:
728
+ "Triggers when at least one unresolved exception is reported in ten minutes.",
729
+ incidentTitle: `[Service] Unhandled exception - ${args.monitorName}`,
730
+ incidentDescription:
731
+ "The service reported an unresolved exception. Inspect the exception group, its stack trace, the release it appeared in, and how many distinct requests it affected.",
732
+ }),
733
+ );
734
+
735
+ return step;
736
+ },
737
+ };
738
+
739
+ /*
740
+ * Latency has to be a metric monitor: trace monitors can only count spans,
741
+ * and no span-duration criterion exists anywhere in the evaluator.
742
+ *
743
+ * `http.server.request.duration` is the histogram the percentile path was
744
+ * built for — the metric service fans its buckets into weighted samples and
745
+ * runs a real quantile rather than taking a percentile of per-row sums.
746
+ *
747
+ * `legendUnit: "ms"` normalizes the two units this metric arrives in: the
748
+ * semantic convention says seconds, while OneUptime's own instrumentation
749
+ * reports milliseconds. Converting both to milliseconds before comparing is
750
+ * what makes one threshold correct for both.
751
+ */
752
+ const latencyP95Template: ServiceAlertTemplate = buildMetricTemplate({
753
+ id: "service-latency-p95",
754
+ name: "High Request Latency (p95)",
755
+ description:
756
+ "Alert when the 95th-percentile server request duration stays at or above one second. Reads http.server.request.duration; older SDKs report this as http.server.duration.",
757
+ category: "Latency",
758
+ severity: "Warning",
759
+ metricName: "http.server.request.duration",
760
+ metricAlias: "service_latency_p95",
761
+ aggregationType: MetricsAggregationType.P95,
762
+ legendUnit: "ms",
763
+ threshold: 1000,
764
+ thresholdLabel: "1,000 ms",
765
+ incidentDescription:
766
+ "One in twenty requests is now taking a second or more. Break the latency down by route and by downstream call to find where the time is going.",
767
+ });
768
+
769
+ const latencyP99Template: ServiceAlertTemplate = buildMetricTemplate({
770
+ id: "service-latency-p99",
771
+ name: "Severe Request Latency (p99)",
772
+ description:
773
+ "Alert when the 99th-percentile server request duration stays at or above 2.5 seconds — the tail users abandon.",
774
+ category: "Latency",
775
+ severity: "Critical",
776
+ metricName: "http.server.request.duration",
777
+ metricAlias: "service_latency_p99",
778
+ aggregationType: MetricsAggregationType.P99,
779
+ legendUnit: "ms",
780
+ threshold: 2500,
781
+ thresholdLabel: "2,500 ms",
782
+ incidentDescription:
783
+ "The slowest one percent of requests are taking 2.5 seconds or more. Look for a saturated dependency, a lock, or a slow query on the affected route.",
784
+ });
785
+
786
+ /*
787
+ * `process.cpu.utilization` and `process.memory.usage` come from the
788
+ * host-metrics / system-metrics instrumentation rather than from any language
789
+ * runtime, which is why they sit in the agnostic set. They are opt-in in every
790
+ * SDK: a service that has not enabled that instrumentation gets a monitor that
791
+ * never fires, which is why both say so in their descriptions.
792
+ *
793
+ * The threshold is a raw [0, 1] ratio, matching how the host templates already
794
+ * express utilization (`0.85 == 85%`) and how the SDKs report it. It is
795
+ * deliberately NOT converted to a percentage: that conversion depends on the
796
+ * metric's declared unit being present, and a missing unit would silently turn
797
+ * "85%" into a threshold no fraction can ever reach.
798
+ */
799
+ const processCpuTemplate: ServiceAlertTemplate = buildMetricTemplate({
800
+ id: "service-process-cpu-saturation",
801
+ name: "Process CPU Saturation",
802
+ description:
803
+ "Alert when the process stays above 85% CPU utilization. Needs the host-metrics instrumentation enabled in the service's SDK.",
804
+ category: "Saturation",
805
+ severity: "Warning",
806
+ metricName: "process.cpu.utilization",
807
+ metricAlias: "service_process_cpu",
808
+ aggregationType: MetricsAggregationType.Avg,
809
+ threshold: 0.85,
810
+ thresholdLabel: "85%",
811
+ unhealthyFilterType: FilterType.GreaterThan,
812
+ healthyFilterType: FilterType.LessThanOrEqualTo,
813
+ incidentDescription:
814
+ "The process has been pinned above 85% CPU for the whole window. Requests will be queueing behind it — check for a hot loop, a retry storm, or a workload that outgrew its CPU allocation.",
815
+ });
816
+
817
+ const processMemoryTemplate: ServiceAlertTemplate = buildMetricTemplate({
818
+ id: "service-process-memory-high",
819
+ name: "Process Memory High",
820
+ description:
821
+ "Alert when process resident memory stays above 1 GB. An absolute budget — retune it to your container limit. Needs the host-metrics instrumentation enabled.",
822
+ category: "Saturation",
823
+ severity: "Warning",
824
+ metricName: "process.memory.usage",
825
+ metricAlias: "service_process_memory",
826
+ aggregationType: MetricsAggregationType.Avg,
827
+ threshold: 1073741824,
828
+ thresholdLabel: "1 GB",
829
+ unhealthyFilterType: FilterType.GreaterThan,
830
+ healthyFilterType: FilterType.LessThanOrEqualTo,
831
+ incidentDescription:
832
+ "Process memory has stayed above its budget for the whole window. If it only ever climbs, it is a leak; if it climbs and drops, the workload has outgrown the container limit and will be OOM-killed next.",
833
+ });
834
+
835
+ // --- Java ---
836
+
837
+ const javaTemplates: Array<ServiceAlertTemplate> = [
838
+ buildRatioTemplate({
839
+ id: "service-java-heap-utilization",
840
+ name: "JVM Heap Utilization",
841
+ description:
842
+ "Alert when live heap stays at or above 90% of the JVM's configured heap limit — the leading indicator of GC thrashing and OutOfMemoryError.",
843
+ category: "JVM Runtime",
844
+ severity: "Critical",
845
+ language: "java",
846
+ numeratorMetricName: "jvm.memory.used",
847
+ numeratorAttributes: { "jvm.memory.type": "heap" },
848
+ denominatorMetricName: "jvm.memory.limit",
849
+ denominatorAttributes: { "jvm.memory.type": "heap" },
850
+ numeratorAlias: "jvm_heap_used",
851
+ denominatorAlias: "jvm_heap_limit",
852
+ resultAlias: "jvm_heap_percent",
853
+ thresholdPercent: 90,
854
+ incidentDescription:
855
+ "The JVM heap is at or above 90% of its limit. Expect long GC pauses next and an OutOfMemoryError after that. Take a heap dump before restarting — a restart clears the symptom and destroys the evidence.",
856
+ }),
857
+ buildMetricTemplate({
858
+ id: "service-java-live-heap-after-gc",
859
+ name: "Live Heap After GC",
860
+ description:
861
+ "Alert when the bytes still live after the last collection stay above 2 GB. Unlike heap used, this ignores allocation churn, so a rise here is a genuine leak. Retune to roughly 85% of your -Xmx.",
862
+ category: "JVM Runtime",
863
+ severity: "Warning",
864
+ language: "java",
865
+ metricName: "jvm.memory.used_after_last_gc",
866
+ metricAlias: "jvm_heap_after_gc",
867
+ aggregationType: MetricsAggregationType.Max,
868
+ attributes: { "jvm.memory.type": "heap" },
869
+ threshold: 2147483648,
870
+ thresholdLabel: "2 GB",
871
+ unhealthyFilterType: FilterType.GreaterThan,
872
+ healthyFilterType: FilterType.LessThanOrEqualTo,
873
+ incidentDescription:
874
+ "Memory that survives every collection keeps growing, which is what a leak looks like from the outside. Compare heap dumps taken an hour apart and look at which retained set grew.",
875
+ }),
876
+ buildMetricTemplate({
877
+ id: "service-java-gc-pause-p99",
878
+ name: "Long GC Pauses",
879
+ description:
880
+ "Alert when the 99th-percentile garbage collection pause stays at or above one second. Reads jvm.gc.duration; older agents report process.runtime.jvm.gc.duration.",
881
+ category: "JVM Runtime",
882
+ severity: "Critical",
883
+ language: "java",
884
+ metricName: "jvm.gc.duration",
885
+ metricAlias: "jvm_gc_pause_p99",
886
+ aggregationType: MetricsAggregationType.P99,
887
+ legendUnit: "ms",
888
+ threshold: 1000,
889
+ thresholdLabel: "1,000 ms",
890
+ incidentDescription:
891
+ "The JVM is stopping the world for a second or more at the tail. Every request in flight during a pause pays for it, so this shows up to callers as latency with no slow query behind it. Check heap headroom and collector choice.",
892
+ }),
893
+ buildMetricTemplate({
894
+ id: "service-java-cpu-saturation",
895
+ name: "JVM CPU Saturation",
896
+ description:
897
+ "Alert when the JVM's recent CPU utilization stays above 85%. Reads jvm.cpu.recent_utilization; older agents report process.runtime.jvm.cpu.utilization.",
898
+ category: "JVM Runtime",
899
+ severity: "Warning",
900
+ language: "java",
901
+ metricName: "jvm.cpu.recent_utilization",
902
+ metricAlias: "jvm_cpu",
903
+ aggregationType: MetricsAggregationType.Avg,
904
+ threshold: 0.85,
905
+ thresholdLabel: "85%",
906
+ unhealthyFilterType: FilterType.GreaterThan,
907
+ healthyFilterType: FilterType.LessThanOrEqualTo,
908
+ incidentDescription:
909
+ "The JVM has been pinned above 85% CPU for the whole window. If GC pause time rose with it, this is GC thrashing rather than application work — check heap utilization first.",
910
+ }),
911
+ buildMetricTemplate({
912
+ id: "service-java-thread-explosion",
913
+ name: "Thread Count Explosion",
914
+ description:
915
+ "Alert when live thread count stays above 500 — an unbounded pool, a leaked executor, or thread-per-request under load. Reads jvm.thread.count; older agents report process.runtime.jvm.threads.count.",
916
+ category: "JVM Runtime",
917
+ severity: "Warning",
918
+ language: "java",
919
+ metricName: "jvm.thread.count",
920
+ metricAlias: "jvm_threads",
921
+ aggregationType: MetricsAggregationType.Max,
922
+ threshold: 500,
923
+ thresholdLabel: "500 threads",
924
+ unhealthyFilterType: FilterType.GreaterThan,
925
+ healthyFilterType: FilterType.LessThanOrEqualTo,
926
+ incidentDescription:
927
+ "Live threads have stayed above 500. Each one costs stack memory and scheduler time, and a count that only climbs means threads are being created faster than they finish. Take a thread dump and look for the pool that has no bound.",
928
+ }),
929
+ buildMetricTemplate({
930
+ id: "service-java-non-heap-growth",
931
+ name: "Non-Heap Memory Growth",
932
+ description:
933
+ "Alert when non-heap memory — metaspace, code cache, compressed class space — stays above 512 MB. Usually a classloader leak, dynamic proxy churn, or an agent gone wrong.",
934
+ category: "JVM Runtime",
935
+ severity: "Warning",
936
+ language: "java",
937
+ metricName: "jvm.memory.used",
938
+ metricAlias: "jvm_non_heap_used",
939
+ aggregationType: MetricsAggregationType.Sum,
940
+ attributes: { "jvm.memory.type": "non_heap" },
941
+ threshold: 536870912,
942
+ thresholdLabel: "512 MB",
943
+ unhealthyFilterType: FilterType.GreaterThan,
944
+ healthyFilterType: FilterType.LessThanOrEqualTo,
945
+ incidentDescription:
946
+ "Non-heap memory has grown past its budget. Heap dumps will not show this — look at loaded class count over time, and at anything generating classes at runtime.",
947
+ }),
948
+ ];
949
+
950
+ // --- .NET ---
951
+
952
+ const dotnetTemplates: Array<ServiceAlertTemplate> = [
953
+ buildMetricTemplate({
954
+ id: "service-dotnet-threadpool-starvation",
955
+ name: "Thread Pool Starvation",
956
+ description:
957
+ "Alert when work items queue behind a saturated thread pool. The classic sync-over-async death spiral, and it presents as latency everywhere at once.",
958
+ category: ".NET Runtime",
959
+ severity: "Critical",
960
+ language: "dotnet",
961
+ metricName: "dotnet.thread_pool.queue.length",
962
+ metricAlias: "dotnet_threadpool_queue",
963
+ aggregationType: MetricsAggregationType.Max,
964
+ threshold: 50,
965
+ thresholdLabel: "50 queued work items",
966
+ unhealthyFilterType: FilterType.GreaterThan,
967
+ healthyFilterType: FilterType.LessThanOrEqualTo,
968
+ incidentDescription:
969
+ "Work is queueing because no pool thread is free. Look for blocking calls on pool threads — .Result, .Wait(), or a synchronous I/O call inside an async path — rather than for slow work.",
970
+ }),
971
+ buildMetricTemplate({
972
+ id: "service-dotnet-threadpool-growth",
973
+ name: "Thread Pool Growth",
974
+ description:
975
+ "Alert when the runtime has injected far more pool threads than the machine has cores, which it only does when existing threads are blocked.",
976
+ category: ".NET Runtime",
977
+ severity: "Warning",
978
+ language: "dotnet",
979
+ metricName: "dotnet.thread_pool.thread.count",
980
+ metricAlias: "dotnet_threadpool_threads",
981
+ aggregationType: MetricsAggregationType.Max,
982
+ threshold: 200,
983
+ thresholdLabel: "200 threads",
984
+ unhealthyFilterType: FilterType.GreaterThan,
985
+ healthyFilterType: FilterType.LessThanOrEqualTo,
986
+ incidentDescription:
987
+ "The thread pool has injected threads well past core count, which the runtime only does to work around blocked threads. Pairs with queue length — this one rises first.",
988
+ }),
989
+ buildMetricTemplate({
990
+ id: "service-dotnet-gen2-heap-growth",
991
+ name: "Gen 2 Heap Growth",
992
+ description:
993
+ "Alert when the gen 2 heap after the last collection stays above 1 GB. Objects that survive into gen 2 and stay there are the .NET leak signature.",
994
+ category: ".NET Runtime",
995
+ severity: "Warning",
996
+ language: "dotnet",
997
+ metricName: "dotnet.gc.last_collection.heap.size",
998
+ metricAlias: "dotnet_gen2_heap",
999
+ aggregationType: MetricsAggregationType.Avg,
1000
+ attributes: { "dotnet.gc.heap.generation": "gen2" },
1001
+ threshold: 1073741824,
1002
+ thresholdLabel: "1 GB",
1003
+ unhealthyFilterType: FilterType.GreaterThan,
1004
+ healthyFilterType: FilterType.LessThanOrEqualTo,
1005
+ incidentDescription:
1006
+ "The gen 2 heap keeps growing across collections. Take two dumps an hour apart and diff the object graph — a static collection or an event handler that is never unsubscribed is the usual cause.",
1007
+ }),
1008
+ buildMetricTemplate({
1009
+ id: "service-dotnet-working-set",
1010
+ name: "Working Set High",
1011
+ description:
1012
+ "Alert when the process working set stays above 1.5 GB. An absolute budget — retune it to your container limit.",
1013
+ category: ".NET Runtime",
1014
+ severity: "Warning",
1015
+ language: "dotnet",
1016
+ metricName: "dotnet.process.memory.working_set",
1017
+ metricAlias: "dotnet_working_set",
1018
+ aggregationType: MetricsAggregationType.Avg,
1019
+ threshold: 1610612736,
1020
+ thresholdLabel: "1.5 GB",
1021
+ unhealthyFilterType: FilterType.GreaterThan,
1022
+ healthyFilterType: FilterType.LessThanOrEqualTo,
1023
+ incidentDescription:
1024
+ "The working set has stayed above its budget. If gen 2 heap is flat, the growth is unmanaged — native handles, pinned buffers, or a native library holding memory.",
1025
+ }),
1026
+ ];
1027
+
1028
+ // --- Node.js ---
1029
+
1030
+ const nodejsTemplates: Array<ServiceAlertTemplate> = [
1031
+ buildMetricTemplate({
1032
+ id: "service-nodejs-event-loop-saturated",
1033
+ name: "Event Loop Saturated",
1034
+ description:
1035
+ "Alert when the event loop is busy more than 90% of wall time. The single best saturation signal Node has: past this point every new request just queues.",
1036
+ category: "Node.js Runtime",
1037
+ severity: "Critical",
1038
+ language: "nodejs",
1039
+ metricName: "nodejs.eventloop.utilization",
1040
+ metricAlias: "nodejs_eventloop_utilization",
1041
+ aggregationType: MetricsAggregationType.Avg,
1042
+ threshold: 0.9,
1043
+ thresholdLabel: "90%",
1044
+ unhealthyFilterType: FilterType.GreaterThan,
1045
+ healthyFilterType: FilterType.LessThanOrEqualTo,
1046
+ incidentDescription:
1047
+ "The event loop has almost no idle time left. One process cannot serve more than this — find the synchronous work blocking the loop (JSON of a huge payload, crypto, a tight loop) or add instances.",
1048
+ }),
1049
+ buildMetricTemplate({
1050
+ id: "service-nodejs-event-loop-lag",
1051
+ name: "Event Loop Lag",
1052
+ description:
1053
+ "Alert when 99th-percentile event-loop scheduling lag stays at or above 200 ms, meaning callbacks and timers are being starved.",
1054
+ category: "Node.js Runtime",
1055
+ severity: "Warning",
1056
+ language: "nodejs",
1057
+ metricName: "nodejs.eventloop.delay.p99",
1058
+ metricAlias: "nodejs_eventloop_delay",
1059
+ aggregationType: MetricsAggregationType.Avg,
1060
+ legendUnit: "ms",
1061
+ threshold: 200,
1062
+ thresholdLabel: "200 ms",
1063
+ incidentDescription:
1064
+ "Callbacks are waiting 200 ms or more just to be scheduled. Every response pays that on top of its real work — look for a synchronous block on the main thread.",
1065
+ }),
1066
+ /*
1067
+ * Deliberately unfiltered by heap space, on both sides.
1068
+ *
1069
+ * `v8js.memory.heap.used` is reported per heap space (the service overview's
1070
+ * own chart labels it "avg across V8 heap spaces"), so the obvious version
1071
+ * of this template filters both queries to `old_space` — that is the space
1072
+ * that actually kills a Node process. Summing every space instead is correct
1073
+ * whichever way the SDK reports it: if the series are split, the sums are
1074
+ * total used over total limit; if they are not, the values pass through
1075
+ * unchanged. Filtering on an attribute the SDK turns out not to emit
1076
+ * produces no data, and a metric monitor with no data never fires — a
1077
+ * silent, permanently-green monitor is a worse outcome than a slightly
1078
+ * blunter one.
1079
+ */
1080
+ buildRatioTemplate({
1081
+ id: "service-nodejs-heap-pressure",
1082
+ name: "V8 Heap Pressure",
1083
+ description:
1084
+ "Alert when the V8 heap stays at or above 90% of its hard limit — the last warning before a fatal, unrecoverable out-of-memory crash.",
1085
+ category: "Node.js Runtime",
1086
+ severity: "Critical",
1087
+ language: "nodejs",
1088
+ numeratorMetricName: "v8js.memory.heap.used",
1089
+ denominatorMetricName: "v8js.memory.heap.limit",
1090
+ numeratorAlias: "v8_heap_used",
1091
+ denominatorAlias: "v8_heap_limit",
1092
+ resultAlias: "v8_heap_percent",
1093
+ thresholdPercent: 90,
1094
+ incidentDescription:
1095
+ "The V8 heap is nearly full. Node does not degrade here — it aborts the process. Capture a heap snapshot now, and raise --max-old-space-size only as a stopgap.",
1096
+ }),
1097
+ /*
1098
+ * Unfiltered by GC type for the same reason as the template above: a p99
1099
+ * across every collection is dominated by the major ones anyway, and it
1100
+ * cannot be silenced by an attribute the SDK does not emit.
1101
+ */
1102
+ buildMetricTemplate({
1103
+ id: "service-nodejs-gc-pause-p99",
1104
+ name: "Long GC Pauses",
1105
+ description:
1106
+ "Alert when the 99th-percentile garbage collection pause stays at or above 200 ms. On a single-threaded event loop, a GC pause blocks everything.",
1107
+ category: "Node.js Runtime",
1108
+ severity: "Warning",
1109
+ language: "nodejs",
1110
+ metricName: "v8js.gc.duration",
1111
+ metricAlias: "v8_gc_pause_p99",
1112
+ aggregationType: MetricsAggregationType.P99,
1113
+ legendUnit: "ms",
1114
+ threshold: 200,
1115
+ thresholdLabel: "200 ms",
1116
+ incidentDescription:
1117
+ "Collections are pausing the loop for 200 ms or more at the tail. Usually the tail end of heap pressure — check heap utilization before tuning the collector.",
1118
+ }),
1119
+ ];
1120
+
1121
+ // --- Python ---
1122
+
1123
+ const pythonTemplates: Array<ServiceAlertTemplate> = [
1124
+ /*
1125
+ * The only genuinely default-on Python metric here: the ASGI and WSGI
1126
+ * instrumentations emit it, so FastAPI, Django and Flask services get it
1127
+ * without any extra package. The other two need
1128
+ * `opentelemetry-instrumentation-system-metrics`.
1129
+ */
1130
+ buildMetricTemplate({
1131
+ id: "service-python-request-concurrency",
1132
+ name: "Request Concurrency Saturated",
1133
+ description:
1134
+ "Alert when in-flight requests reach the worker budget, meaning new requests are queueing. Retune the threshold to your workers × threads.",
1135
+ category: "Python Runtime",
1136
+ severity: "Critical",
1137
+ language: "python",
1138
+ metricName: "http.server.active_requests",
1139
+ metricAlias: "python_active_requests",
1140
+ aggregationType: MetricsAggregationType.Max,
1141
+ threshold: 40,
1142
+ thresholdLabel: "40 in-flight requests",
1143
+ incidentDescription:
1144
+ "Every worker is busy and requests are queueing behind them. With a GIL, adding threads rarely helps — look at what the workers are blocked on, and at process count.",
1145
+ }),
1146
+ buildMetricTemplate({
1147
+ id: "service-python-rss-memory",
1148
+ name: "Resident Memory High",
1149
+ description:
1150
+ "Alert when resident memory stays above 1 GB per worker — the signal that precedes an OOM kill. Needs the system-metrics instrumentation enabled.",
1151
+ category: "Python Runtime",
1152
+ severity: "Warning",
1153
+ language: "python",
1154
+ metricName: "process.runtime.cpython.memory",
1155
+ metricAlias: "python_rss",
1156
+ aggregationType: MetricsAggregationType.Avg,
1157
+ attributes: { type: "rss" },
1158
+ threshold: 1073741824,
1159
+ thresholdLabel: "1 GB",
1160
+ unhealthyFilterType: FilterType.GreaterThan,
1161
+ healthyFilterType: FilterType.LessThanOrEqualTo,
1162
+ incidentDescription:
1163
+ "A worker's resident memory has stayed above its budget. Python rarely returns memory to the OS, so a plateau after a spike is normal and a steady climb is not — look for an unbounded cache or a growing module-level structure.",
1164
+ }),
1165
+ buildMetricTemplate({
1166
+ id: "service-python-thread-growth",
1167
+ name: "Thread Count Growth",
1168
+ description:
1169
+ "Alert when thread count stays above 200, which usually means a leaked executor or an unclosed client pool. Needs the system-metrics instrumentation enabled.",
1170
+ category: "Python Runtime",
1171
+ severity: "Warning",
1172
+ language: "python",
1173
+ metricName: "process.runtime.cpython.thread_count",
1174
+ metricAlias: "python_threads",
1175
+ aggregationType: MetricsAggregationType.Max,
1176
+ threshold: 200,
1177
+ thresholdLabel: "200 threads",
1178
+ unhealthyFilterType: FilterType.GreaterThan,
1179
+ healthyFilterType: FilterType.LessThanOrEqualTo,
1180
+ incidentDescription:
1181
+ "Thread count has stayed above 200. Under a GIL these mostly wait rather than work, so a climbing count is a leak — look for ThreadPoolExecutors created per request and clients never closed.",
1182
+ }),
1183
+ ];
1184
+
1185
+ // --- Go ---
1186
+
1187
+ const goTemplates: Array<ServiceAlertTemplate> = [
1188
+ buildMetricTemplate({
1189
+ id: "service-go-goroutine-leak",
1190
+ name: "Goroutine Leak",
1191
+ description:
1192
+ "Alert when live goroutines stay above 10,000 — leaked contexts, unbounded fan-out, or sends on a channel nobody reads. Reads go.goroutine.count; older builds report process.runtime.go.goroutines.",
1193
+ category: "Go Runtime",
1194
+ severity: "Warning",
1195
+ language: "go",
1196
+ metricName: "go.goroutine.count",
1197
+ metricAlias: "go_goroutines",
1198
+ aggregationType: MetricsAggregationType.Max,
1199
+ threshold: 10000,
1200
+ thresholdLabel: "10,000 goroutines",
1201
+ unhealthyFilterType: FilterType.GreaterThan,
1202
+ healthyFilterType: FilterType.LessThanOrEqualTo,
1203
+ incidentDescription:
1204
+ "Goroutine count has stayed above 10,000. A count that only climbs is a leak: take a goroutine profile and look at the top stack — it is almost always a channel send or receive with no timeout and no cancelled context.",
1205
+ }),
1206
+ buildMetricTemplate({
1207
+ id: "service-go-heap-memory",
1208
+ name: "Runtime Memory High",
1209
+ description:
1210
+ "Alert when non-stack runtime memory stays above 1 GB. An absolute budget — retune it to your container limit.",
1211
+ category: "Go Runtime",
1212
+ severity: "Warning",
1213
+ language: "go",
1214
+ metricName: "go.memory.used",
1215
+ metricAlias: "go_heap_memory",
1216
+ aggregationType: MetricsAggregationType.Avg,
1217
+ /*
1218
+ * The filter is load-bearing, not a refinement: `go.memory.used` splits
1219
+ * into `stack` and `other` series, so an unfiltered average of the two
1220
+ * reports roughly half the heap and the threshold would be wrong by that
1221
+ * factor. "other" is the heap-dominated half.
1222
+ */
1223
+ attributes: { "go.memory.type": "other" },
1224
+ threshold: 1073741824,
1225
+ thresholdLabel: "1 GB",
1226
+ unhealthyFilterType: FilterType.GreaterThan,
1227
+ healthyFilterType: FilterType.LessThanOrEqualTo,
1228
+ incidentDescription:
1229
+ "Heap memory has stayed above its budget. Take a heap profile and compare allocation sites — in Go this is usually a slice or map that is appended to and never bounded.",
1230
+ }),
1231
+ buildMetricTemplate({
1232
+ id: "service-go-stack-memory",
1233
+ name: "Goroutine Stack Memory",
1234
+ description:
1235
+ "Alert when total goroutine stack memory stays above 256 MB. An independent read on the same leak goroutine count catches, and it often moves first.",
1236
+ category: "Go Runtime",
1237
+ severity: "Warning",
1238
+ language: "go",
1239
+ metricName: "go.memory.used",
1240
+ metricAlias: "go_stack_memory",
1241
+ aggregationType: MetricsAggregationType.Avg,
1242
+ attributes: { "go.memory.type": "stack" },
1243
+ threshold: 268435456,
1244
+ thresholdLabel: "256 MB",
1245
+ unhealthyFilterType: FilterType.GreaterThan,
1246
+ healthyFilterType: FilterType.LessThanOrEqualTo,
1247
+ incidentDescription:
1248
+ "Stack memory has grown past its budget. Either there are far too many goroutines, or some of them recurse deeply — the goroutine profile answers which.",
1249
+ }),
1250
+ buildMetricTemplate({
1251
+ id: "service-go-scheduler-latency-p99",
1252
+ name: "Scheduler Latency",
1253
+ description:
1254
+ "Alert when the 99th-percentile time a runnable goroutine waits to be scheduled stays at or above 50 ms — P contention or a GOMAXPROCS set below the real CPU budget.",
1255
+ category: "Go Runtime",
1256
+ severity: "Warning",
1257
+ language: "go",
1258
+ metricName: "go.schedule.duration",
1259
+ metricAlias: "go_schedule_p99",
1260
+ aggregationType: MetricsAggregationType.P99,
1261
+ legendUnit: "ms",
1262
+ threshold: 50,
1263
+ thresholdLabel: "50 ms",
1264
+ incidentDescription:
1265
+ "Runnable goroutines are waiting 50 ms or more for a processor. Check GOMAXPROCS against the container's real CPU limit, and look for CPU-bound work starving the scheduler.",
1266
+ }),
1267
+ ];
1268
+
1269
+ /*
1270
+ * Declaration order is the display order — the recommendations page renders
1271
+ * category sections in the order the templates first mention them, so the
1272
+ * agnostic RED signals come first and the runtime section lands underneath.
1273
+ */
1274
+ const ALL_SERVICE_ALERT_TEMPLATES: Array<ServiceAlertTemplate> = [
1275
+ failedOperationsTemplate,
1276
+ errorBurstTemplate,
1277
+ unhandledExceptionsTemplate,
1278
+ latencyP95Template,
1279
+ latencyP99Template,
1280
+ trafficStoppedTemplate,
1281
+ processCpuTemplate,
1282
+ processMemoryTemplate,
1283
+ ...javaTemplates,
1284
+ ...dotnetTemplates,
1285
+ ...nodejsTemplates,
1286
+ ...pythonTemplates,
1287
+ ...goTemplates,
1288
+ ];
1289
+
1290
+ export function getAllServiceAlertTemplates(): Array<ServiceAlertTemplate> {
1291
+ return [...ALL_SERVICE_ALERT_TEMPLATES];
1292
+ }
1293
+
1294
+ /*
1295
+ * The templates to offer ONE service, given its detected runtime.
1296
+ *
1297
+ * Always the agnostic set, plus the runtime's own set when there is one. The
1298
+ * two rules that matter:
1299
+ *
1300
+ * - An unknown language (null/undefined) yields the agnostic set, never an
1301
+ * empty list and never a guess. A service whose SDK has not reported
1302
+ * `telemetry.sdk.language` yet still has spans and exceptions, so the RED
1303
+ * recommendations are all valid for it.
1304
+ *
1305
+ * - A known language with no templates of its own — Ruby, PHP, Rust, and the
1306
+ * rest — also yields exactly the agnostic set. That is a deliberate
1307
+ * omission rather than a gap to be filled later with plausible-looking
1308
+ * entries: those ecosystems have no default OpenTelemetry runtime-metrics
1309
+ * instrumentation, so any runtime template written for them would query a
1310
+ * metric name nobody emits. A shorter honest list beats a longer list of
1311
+ * monitors that can never fire.
1312
+ */
1313
+ export function getServiceAlertTemplates(
1314
+ language?: ServiceLanguage | null | undefined,
1315
+ ): Array<ServiceAlertTemplate> {
1316
+ return ALL_SERVICE_ALERT_TEMPLATES.filter(
1317
+ (template: ServiceAlertTemplate) => {
1318
+ if (!template.language) {
1319
+ return true;
1320
+ }
1321
+
1322
+ return Boolean(language) && template.language === language;
1323
+ },
1324
+ );
1325
+ }
1326
+
1327
+ export function getServiceAlertTemplateById(
1328
+ id: string,
1329
+ ): ServiceAlertTemplate | undefined {
1330
+ return ALL_SERVICE_ALERT_TEMPLATES.find((template: ServiceAlertTemplate) => {
1331
+ return template.id === id;
1332
+ });
1333
+ }
1334
+
1335
+ /*
1336
+ * The languages this module ships runtime templates for.
1337
+ *
1338
+ * Derived from the templates rather than hand-listed, so it cannot drift: a
1339
+ * new runtime template makes its language appear here automatically, and the
1340
+ * tests assert the two agree.
1341
+ */
1342
+ export function getLanguagesWithServiceAlertTemplates(): Array<ServiceLanguage> {
1343
+ const languages: Array<ServiceLanguage> = [];
1344
+
1345
+ for (const template of ALL_SERVICE_ALERT_TEMPLATES) {
1346
+ if (template.language && !languages.includes(template.language)) {
1347
+ languages.push(template.language);
1348
+ }
1349
+ }
1350
+
1351
+ return languages;
1352
+ }