@oneuptime/common 14.0.0 → 14.0.1

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 (196) hide show
  1. package/Models/DatabaseModels/Index.ts +4 -0
  2. package/Models/DatabaseModels/ServiceLevelObjectiveFeed.ts +2 -0
  3. package/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.ts +522 -0
  4. package/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.ts +38 -4
  5. package/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.ts +603 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.ts +248 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.ts +19 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  9. package/Server/Services/DatabaseService.ts +10 -0
  10. package/Server/Services/Index.ts +4 -0
  11. package/Server/Services/MonitorService.ts +3 -2
  12. package/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.ts +385 -0
  13. package/Server/Services/ServiceLevelObjectiveLabelRuleService.ts +51 -0
  14. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +92 -16
  15. package/Server/Services/ServiceLevelObjectiveMonitorRuleService.ts +36 -1
  16. package/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.ts +521 -0
  17. package/Server/Services/ServiceLevelObjectiveOwnerRuleService.ts +51 -0
  18. package/Server/Services/ServiceLevelObjectiveService.ts +28 -0
  19. package/Server/Types/Workflow/Components/Telegram/SendMessageToChat.ts +27 -1
  20. package/Server/Utils/AI/Toolbox/SloTools.ts +1 -0
  21. package/Server/Utils/Monitor/AffectedResourceList.ts +31 -73
  22. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +1 -1
  23. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +1 -1
  24. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +181 -136
  25. package/Server/Utils/Monitor/RootCauseList.ts +139 -0
  26. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +28 -0
  27. package/Server/Utils/Slo/SloRulePatternValidator.ts +42 -0
  28. package/Tests/App/Dashboard/FeedFilterSortIntegration.test.tsx +1632 -0
  29. package/Tests/App/Dashboard/FeedOptionsLocales.test.ts +250 -0
  30. package/Tests/App/Dashboard/FeedOrdering.test.ts +279 -9
  31. package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +60 -1
  32. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +594 -3
  33. package/Tests/App/Dashboard/ProductOrientation.test.tsx +646 -0
  34. package/Tests/App/Dashboard/ResourceFeed.test.tsx +612 -2
  35. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +30 -0
  36. package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +1202 -0
  37. package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +52 -1
  38. package/Tests/App/Dashboard/SloFeed.test.tsx +106 -1
  39. package/Tests/App/Dashboard/SloMonitorRuleCriteria.test.tsx +566 -0
  40. package/Tests/App/Dashboard/SloNavigationWiring.test.tsx +6 -4
  41. package/Tests/App/Dashboard/SloNoticeBanner.test.tsx +186 -0
  42. package/Tests/App/Dashboard/SloOverviewKpiStrip.test.tsx +388 -44
  43. package/Tests/App/Dashboard/SloOverviewLayout.test.tsx +613 -0
  44. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +0 -8
  45. package/Tests/Models/DatabaseModels/SloLabelAndOwnerRuleModels.test.ts +404 -0
  46. package/Tests/Models/DatabaseModels/SloOverhaulModels.test.ts +17 -1
  47. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
  48. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
  49. package/Tests/Server/Infrastructure/Postgres/AddSloLabelAndOwnerRulesMigration.test.ts +407 -0
  50. package/Tests/Server/Infrastructure/Postgres/AddSloMonitorRuleMonitorTypeMigration.test.ts +171 -0
  51. package/Tests/Server/Infrastructure/Postgres/SloProductOverhaulMigration.test.ts +88 -4
  52. package/Tests/Server/Services/DatabaseServiceRuleCriteriaValidation.test.ts +104 -0
  53. package/Tests/Server/Services/RuleRun/LabelRuleEnginesGroupBRun.test.ts +48 -1
  54. package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +30 -0
  55. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleEngines.test.ts +1170 -0
  56. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleServices.test.ts +172 -0
  57. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +323 -0
  58. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleService.test.ts +115 -0
  59. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleTriggers.test.ts +21 -2
  60. package/Tests/Server/Services/ServiceLevelObjectiveMonitorTypePostgres.test.ts +480 -0
  61. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +162 -0
  62. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +45 -6
  63. package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +745 -0
  64. package/Tests/Server/Types/Markdown.test.ts +2 -1
  65. package/Tests/Server/Types/Workflow/Components/ApiComponentSsrf.test.ts +49 -0
  66. package/Tests/Server/Types/Workflow/Components/TelegramSendMessageToChat.test.ts +331 -0
  67. package/Tests/Server/Utils/AI/SloTools.test.ts +10 -0
  68. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +1 -1
  69. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +405 -0
  70. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorBreachingSamples.test.ts +1101 -0
  71. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +106 -80
  72. package/Tests/Server/Utils/Monitor/RootCauseList.test.ts +252 -0
  73. package/Tests/Server/Utils/Rules/RuleRun/RuleRunPermission.test.ts +154 -0
  74. package/Tests/Server/Utils/SSRFProtection.test.ts +111 -0
  75. package/Tests/Server/Utils/Slo/SloRulePatternValidator.test.ts +67 -0
  76. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +213 -0
  77. package/Tests/Types/API/Route.test.ts +56 -0
  78. package/Tests/Types/API/URLSchemeLikePath.test.ts +304 -0
  79. package/Tests/UI/Components/FeedOptions.test.ts +1404 -0
  80. package/Tests/UI/Components/FeedOptionsButton.test.tsx +2603 -0
  81. package/Tests/UI/Components/Icon.test.tsx +26 -0
  82. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  83. package/Tests/UI/Components/useFeedItemsViewKey.test.tsx +1017 -0
  84. package/Tests/UI/Components/useFeedOptions.test.tsx +1107 -0
  85. package/Tests/Utils/LabelRuleImportExport.test.ts +102 -0
  86. package/Tests/Utils/Slo/SloDuration.test.ts +115 -24
  87. package/Tests/Utils/Slo/SloHealth.test.ts +93 -10
  88. package/Tests/Utils/Slo/SloMonitorRuleCriteria.test.ts +109 -0
  89. package/Tests/Utils/Slo/SloOverviewText.test.ts +64 -0
  90. package/Types/API/Route.ts +19 -2
  91. package/Types/API/URL.ts +26 -2
  92. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +6 -5
  93. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +9 -0
  94. package/Types/Permission.ts +90 -0
  95. package/Types/Rules/RuleCriteriaFieldRegistry.ts +11 -0
  96. package/Types/Rules/RuleRun.ts +4 -0
  97. package/UI/Components/Feed/FeedOptions.ts +427 -0
  98. package/UI/Components/Feed/FeedOptionsButton.tsx +692 -0
  99. package/UI/Components/Feed/useFeedItems.ts +28 -9
  100. package/UI/Components/Feed/useFeedOptions.ts +225 -0
  101. package/UI/Components/Icon/Icon.tsx +1 -1
  102. package/Utils/LabelRuleImportExport.ts +27 -9
  103. package/Utils/Monitor/MetricValueFormatter.ts +2 -2
  104. package/Utils/Rules/MonitorTypeRuleCriteria.ts +31 -0
  105. package/Utils/Slo/SloDuration.ts +22 -6
  106. package/Utils/Slo/SloHealth.ts +8 -7
  107. package/Utils/Slo/SloMonitorRuleCriteria.ts +33 -1
  108. package/Utils/Slo/SloOverviewText.ts +33 -0
  109. package/Utils/Slo/SloProjection.ts +4 -3
  110. package/build/dist/Models/DatabaseModels/Index.js +4 -0
  111. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  112. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js +2 -0
  113. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js.map +1 -1
  114. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js +530 -0
  115. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js.map +1 -0
  116. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js +39 -4
  117. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js.map +1 -1
  118. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js +612 -0
  119. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js.map +1 -0
  120. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js +93 -0
  121. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js.map +1 -0
  122. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js +12 -0
  123. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js.map +1 -0
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  126. package/build/dist/Server/Services/DatabaseService.js +7 -0
  127. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  128. package/build/dist/Server/Services/Index.js +4 -0
  129. package/build/dist/Server/Services/Index.js.map +1 -1
  130. package/build/dist/Server/Services/MonitorService.js +3 -2
  131. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  132. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js +295 -0
  133. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js.map +1 -0
  134. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js +56 -0
  135. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js.map +1 -0
  136. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +59 -14
  137. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -1
  138. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js +22 -1
  139. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js.map +1 -1
  140. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js +400 -0
  141. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js.map +1 -0
  142. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js +56 -0
  143. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js.map +1 -0
  144. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +21 -0
  145. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  146. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js +20 -1
  147. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js.map +1 -1
  148. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +1 -0
  149. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -1
  150. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js +22 -51
  151. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js.map +1 -1
  152. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +1 -1
  153. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
  154. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +139 -109
  155. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  156. package/build/dist/Server/Utils/Monitor/RootCauseList.js +101 -0
  157. package/build/dist/Server/Utils/Monitor/RootCauseList.js.map +1 -0
  158. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +28 -0
  159. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
  160. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js +28 -0
  161. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js.map +1 -0
  162. package/build/dist/Types/API/Route.js +18 -2
  163. package/build/dist/Types/API/Route.js.map +1 -1
  164. package/build/dist/Types/API/URL.js +23 -2
  165. package/build/dist/Types/API/URL.js.map +1 -1
  166. package/build/dist/Types/Permission.js +84 -0
  167. package/build/dist/Types/Permission.js.map +1 -1
  168. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +11 -0
  169. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  170. package/build/dist/Types/Rules/RuleRun.js +4 -0
  171. package/build/dist/Types/Rules/RuleRun.js.map +1 -1
  172. package/build/dist/UI/Components/Feed/FeedOptions.js +235 -0
  173. package/build/dist/UI/Components/Feed/FeedOptions.js.map +1 -0
  174. package/build/dist/UI/Components/Feed/FeedOptionsButton.js +346 -0
  175. package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -0
  176. package/build/dist/UI/Components/Feed/useFeedItems.js +17 -7
  177. package/build/dist/UI/Components/Feed/useFeedItems.js.map +1 -1
  178. package/build/dist/UI/Components/Feed/useFeedOptions.js +130 -0
  179. package/build/dist/UI/Components/Feed/useFeedOptions.js.map +1 -0
  180. package/build/dist/UI/Components/Icon/Icon.js +1 -1
  181. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  182. package/build/dist/Utils/LabelRuleImportExport.js +22 -6
  183. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
  184. package/build/dist/Utils/Monitor/MetricValueFormatter.js +2 -2
  185. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js +18 -0
  186. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js.map +1 -0
  187. package/build/dist/Utils/Slo/SloDuration.js +13 -5
  188. package/build/dist/Utils/Slo/SloDuration.js.map +1 -1
  189. package/build/dist/Utils/Slo/SloHealth.js +8 -7
  190. package/build/dist/Utils/Slo/SloHealth.js.map +1 -1
  191. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js +16 -1
  192. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js.map +1 -1
  193. package/build/dist/Utils/Slo/SloOverviewText.js +16 -0
  194. package/build/dist/Utils/Slo/SloOverviewText.js.map +1 -1
  195. package/build/dist/Utils/Slo/SloProjection.js.map +1 -1
  196. package/package.json +1 -1
@@ -0,0 +1,385 @@
1
+ import Label from "../../Models/DatabaseModels/Label";
2
+ import ServiceLevelObjective from "../../Models/DatabaseModels/ServiceLevelObjective";
3
+ import { ServiceLevelObjectiveFeedEventType } from "../../Models/DatabaseModels/ServiceLevelObjectiveFeed";
4
+ import ServiceLevelObjectiveLabelRule from "../../Models/DatabaseModels/ServiceLevelObjectiveLabelRule";
5
+ import { Purple500 } from "../../Types/BrandColors";
6
+ import ObjectID from "../../Types/ObjectID";
7
+ import { escapeMarkdownInline } from "../../Utils/Markdown/MarkdownEscape";
8
+ import { MAX_RULES_EVALUATED_PER_PROJECT } from "../../Utils/Rules/RuleEngineLimits";
9
+ import { RuleCriteriaMatcher } from "../../Utils/Rules/RuleCriteriaMatcher";
10
+ import RulePatternMatchUtil from "../../Utils/Rules/RulePatternMatchUtil";
11
+ import Select from "../Types/Database/Select";
12
+ import logger, { LogAttributes } from "../Utils/Logger";
13
+ import logIfRuleReadWasTruncated from "../Utils/Rules/RuleEngineRuleRead";
14
+ import {
15
+ ApplyRulesToExistingResourceData,
16
+ RuleApplicationResult,
17
+ RuleApplicationResultUtil,
18
+ RuleRunEngine,
19
+ } from "../Utils/Rules/RuleRun/RuleApplication";
20
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
21
+ import ServiceLevelObjectiveFeedService from "./ServiceLevelObjectiveFeedService";
22
+ import ServiceLevelObjectiveLabelRuleService from "./ServiceLevelObjectiveLabelRuleService";
23
+ import ServiceLevelObjectiveService from "./ServiceLevelObjectiveService";
24
+
25
+ class ServiceLevelObjectiveLabelRuleEngineServiceClass
26
+ implements
27
+ RuleRunEngine<ServiceLevelObjective, ServiceLevelObjectiveLabelRule>
28
+ {
29
+ public readonly ruleSelect: Select<ServiceLevelObjectiveLabelRule> = {
30
+ _id: true,
31
+ name: true,
32
+ criteria: true,
33
+ serviceLevelObjectiveLabels: { _id: true },
34
+ serviceLevelObjectiveNamePattern: true,
35
+ serviceLevelObjectiveDescriptionPattern: true,
36
+ labelsToAdd: { _id: true },
37
+ };
38
+
39
+ // Evaluation re-reads the SLO, so a run only has to name it.
40
+ public readonly resourceSelectForRuleRun: Select<ServiceLevelObjective> = {
41
+ _id: true,
42
+ projectId: true,
43
+ };
44
+
45
+ /**
46
+ * Evaluates ServiceLevelObjectiveLabelRule rows for the given SLO and
47
+ * attaches matched labels to it. The union is deduped against labels already
48
+ * on the SLO before insert to avoid PK conflicts on the join table.
49
+ */
50
+ @CaptureSpan()
51
+ public async applyRulesToServiceLevelObjective(
52
+ serviceLevelObjective: ServiceLevelObjective,
53
+ ): Promise<void> {
54
+ if (!serviceLevelObjective.id || !serviceLevelObjective.projectId) {
55
+ return;
56
+ }
57
+
58
+ try {
59
+ const rules: Array<ServiceLevelObjectiveLabelRule> =
60
+ await ServiceLevelObjectiveLabelRuleService.findBy({
61
+ query: {
62
+ projectId: serviceLevelObjective.projectId,
63
+ isEnabled: true,
64
+ },
65
+ props: { isRoot: true },
66
+ select: this.ruleSelect,
67
+ limit: MAX_RULES_EVALUATED_PER_PROJECT,
68
+ skip: 0,
69
+ });
70
+
71
+ logIfRuleReadWasTruncated({
72
+ ruleKind: "ServiceLevelObjectiveLabelRule",
73
+ projectId: serviceLevelObjective.projectId,
74
+ rulesRead: rules.length,
75
+ });
76
+
77
+ if (rules.length === 0) {
78
+ return;
79
+ }
80
+
81
+ await this.applyRules({
82
+ serviceLevelObjective: serviceLevelObjective,
83
+ rules: rules,
84
+ });
85
+ } catch (error) {
86
+ logger.error(`Error applying SLO label rules: ${error}`, {
87
+ projectId: serviceLevelObjective.projectId?.toString(),
88
+ serviceLevelObjectiveId: serviceLevelObjective.id?.toString(),
89
+ } as LogAttributes);
90
+ }
91
+ }
92
+
93
+ /*
94
+ * "Run now": the same evaluation, for an SLO that already exists and only
95
+ * the rules being run.
96
+ */
97
+ @CaptureSpan()
98
+ public async applyRulesToExistingResource(
99
+ data: ApplyRulesToExistingResourceData<
100
+ ServiceLevelObjective,
101
+ ServiceLevelObjectiveLabelRule
102
+ >,
103
+ ): Promise<RuleApplicationResult> {
104
+ try {
105
+ return await this.applyRules({
106
+ serviceLevelObjective: data.resource,
107
+ rules: data.rules,
108
+ });
109
+ } catch (error) {
110
+ logger.error(`Error running SLO label rules: ${error}`, {
111
+ projectId: data.resource.projectId?.toString(),
112
+ serviceLevelObjectiveId: data.resource.id?.toString(),
113
+ } as LogAttributes);
114
+
115
+ return RuleApplicationResultUtil.failed();
116
+ }
117
+ }
118
+
119
+ private async applyRules(data: {
120
+ serviceLevelObjective: ServiceLevelObjective;
121
+ rules: Array<ServiceLevelObjectiveLabelRule>;
122
+ }): Promise<RuleApplicationResult> {
123
+ const { serviceLevelObjective, rules } = data;
124
+
125
+ if (
126
+ !serviceLevelObjective.id ||
127
+ !serviceLevelObjective.projectId ||
128
+ rules.length === 0
129
+ ) {
130
+ return RuleApplicationResultUtil.noMatch();
131
+ }
132
+
133
+ const sloWithDetails: ServiceLevelObjective | null =
134
+ await ServiceLevelObjectiveService.findOneById({
135
+ id: serviceLevelObjective.id,
136
+ select: {
137
+ name: true,
138
+ description: true,
139
+ labels: { _id: true },
140
+ },
141
+ props: { isRoot: true },
142
+ });
143
+
144
+ if (!sloWithDetails) {
145
+ return RuleApplicationResultUtil.noMatch();
146
+ }
147
+
148
+ const labelIdsToAdd: Set<string> = new Set();
149
+ const matchedRuleNames: Array<string> = [];
150
+ let matchedAnyRule: boolean = false;
151
+
152
+ for (const rule of rules) {
153
+ if (!this.doesServiceLevelObjectiveMatchRule(sloWithDetails, rule)) {
154
+ continue;
155
+ }
156
+ matchedAnyRule = true;
157
+ if ((rule.labelsToAdd || []).length > 0) {
158
+ matchedRuleNames.push(
159
+ rule.name || rule.id?.toString() || "Unnamed rule",
160
+ );
161
+ }
162
+ for (const label of rule.labelsToAdd || []) {
163
+ if (label.id) {
164
+ labelIdsToAdd.add(label.id.toString());
165
+ }
166
+ }
167
+ }
168
+
169
+ if (!matchedAnyRule) {
170
+ return RuleApplicationResultUtil.noMatch();
171
+ }
172
+
173
+ if (labelIdsToAdd.size === 0) {
174
+ return RuleApplicationResultUtil.alreadyApplied();
175
+ }
176
+
177
+ const existingLabelIds: Set<string> = new Set(
178
+ (sloWithDetails.labels || [])
179
+ .map((l: Label) => {
180
+ return l.id?.toString() || "";
181
+ })
182
+ .filter((id: string) => {
183
+ return id !== "";
184
+ }),
185
+ );
186
+
187
+ const newLabelIds: Array<string> = Array.from(labelIdsToAdd).filter(
188
+ (id: string) => {
189
+ return !existingLabelIds.has(id);
190
+ },
191
+ );
192
+ if (newLabelIds.length === 0) {
193
+ return RuleApplicationResultUtil.alreadyApplied();
194
+ }
195
+
196
+ await ServiceLevelObjectiveService.getRepository()
197
+ .createQueryBuilder()
198
+ .relation(ServiceLevelObjective, "labels")
199
+ .of(serviceLevelObjective.id.toString())
200
+ .add(newLabelIds);
201
+
202
+ /*
203
+ * Sync in-memory labels so the owner-rule engine that runs next in the
204
+ * same onCreateSuccess chain can match on rule-added labels.
205
+ */
206
+ const mergedLabelIds: Set<string> = new Set([
207
+ ...existingLabelIds,
208
+ ...newLabelIds,
209
+ ]);
210
+ serviceLevelObjective.labels = Array.from(mergedLabelIds).map(
211
+ (id: string) => {
212
+ const label: Label = new Label();
213
+ label.id = new ObjectID(id);
214
+ return label;
215
+ },
216
+ );
217
+
218
+ logger.debug(
219
+ `ServiceLevelObjectiveLabelRuleEngine attached ${newLabelIds.length} labels to SLO ${serviceLevelObjective.id}`,
220
+ {
221
+ projectId: serviceLevelObjective.projectId.toString(),
222
+ } as LogAttributes,
223
+ );
224
+
225
+ await this.postLabelRuleFeedItem({
226
+ serviceLevelObjectiveId: serviceLevelObjective.id,
227
+ projectId: serviceLevelObjective.projectId,
228
+ sloName: sloWithDetails.name,
229
+ labelsAdded: newLabelIds.length,
230
+ matchedRuleNames: matchedRuleNames,
231
+ });
232
+
233
+ return RuleApplicationResultUtil.updated(newLabelIds.length);
234
+ }
235
+
236
+ /*
237
+ * Labels arriving from a rule rather than from a person is exactly what the
238
+ * SLO's feed cannot otherwise explain, so it records which rules did it.
239
+ * The labels are already attached by now: a feed failure is logged, never
240
+ * thrown, or a run would report an SLO it changed as one it failed on.
241
+ */
242
+ private async postLabelRuleFeedItem(data: {
243
+ serviceLevelObjectiveId: ObjectID;
244
+ projectId: ObjectID;
245
+ sloName: string | undefined;
246
+ labelsAdded: number;
247
+ matchedRuleNames: Array<string>;
248
+ }): Promise<void> {
249
+ try {
250
+ const sloLink: string =
251
+ await ServiceLevelObjectiveService.getSloMarkdownLink({
252
+ projectId: data.projectId,
253
+ sloId: data.serviceLevelObjectiveId,
254
+ sloName: data.sloName,
255
+ });
256
+
257
+ await ServiceLevelObjectiveFeedService.createServiceLevelObjectiveFeedItem(
258
+ {
259
+ serviceLevelObjectiveId: data.serviceLevelObjectiveId,
260
+ projectId: data.projectId,
261
+ serviceLevelObjectiveFeedEventType:
262
+ ServiceLevelObjectiveFeedEventType.LabelRuleExecuted,
263
+ displayColor: Purple500,
264
+ feedInfoInMarkdown: `🏷️ ${data.labelsAdded} label(s) were attached to ${sloLink} by label ${data.matchedRuleNames.length === 1 ? "rule" : "rules"}.`,
265
+ moreInformationInMarkdown: `**Label rules that matched**: ${data.matchedRuleNames
266
+ .map((name: string): string => {
267
+ // Rule names are user-controlled and the feed renders markdown.
268
+ return `**${escapeMarkdownInline(name)}**`;
269
+ })
270
+ .join(", ")}`,
271
+ },
272
+ );
273
+ } catch (error) {
274
+ logger.error(
275
+ `Error writing the label rule feed item for SLO ${data.serviceLevelObjectiveId.toString()}: ${error}`,
276
+ { projectId: data.projectId.toString() } as LogAttributes,
277
+ );
278
+ }
279
+ }
280
+
281
+ private doesServiceLevelObjectiveMatchRule(
282
+ serviceLevelObjective: ServiceLevelObjective,
283
+ rule: ServiceLevelObjectiveLabelRule,
284
+ ): boolean {
285
+ return RuleCriteriaMatcher.matchesWithLegacySync({
286
+ rule,
287
+ legacyFields: [
288
+ "serviceLevelObjectiveLabels",
289
+ "serviceLevelObjectiveNamePattern",
290
+ "serviceLevelObjectiveDescriptionPattern",
291
+ ],
292
+ emptyResult: true,
293
+ matchesLegacyRule: (
294
+ legacyRule: ServiceLevelObjectiveLabelRule,
295
+ ): boolean => {
296
+ return this.doesServiceLevelObjectiveMatchLegacyRule(
297
+ serviceLevelObjective,
298
+ legacyRule,
299
+ );
300
+ },
301
+ });
302
+ }
303
+
304
+ private doesServiceLevelObjectiveMatchLegacyRule(
305
+ serviceLevelObjective: ServiceLevelObjective,
306
+ rule: ServiceLevelObjectiveLabelRule,
307
+ ): boolean {
308
+ if (
309
+ rule.serviceLevelObjectiveLabels &&
310
+ rule.serviceLevelObjectiveLabels.length > 0
311
+ ) {
312
+ if (
313
+ !serviceLevelObjective.labels ||
314
+ serviceLevelObjective.labels.length === 0
315
+ ) {
316
+ return false;
317
+ }
318
+ const ruleLabelIds: Array<string> = rule.serviceLevelObjectiveLabels.map(
319
+ (l: Label) => {
320
+ return l.id?.toString() || "";
321
+ },
322
+ );
323
+ const labelIds: Array<string> = serviceLevelObjective.labels.map(
324
+ (l: Label) => {
325
+ return l.id?.toString() || "";
326
+ },
327
+ );
328
+ if (
329
+ !ruleLabelIds.some((id: string) => {
330
+ return labelIds.includes(id);
331
+ })
332
+ ) {
333
+ return false;
334
+ }
335
+ }
336
+
337
+ if (
338
+ rule.serviceLevelObjectiveNamePattern &&
339
+ (!serviceLevelObjective.name ||
340
+ !this.testPattern(
341
+ rule.serviceLevelObjectiveNamePattern,
342
+ serviceLevelObjective.name,
343
+ rule,
344
+ ))
345
+ ) {
346
+ return false;
347
+ }
348
+
349
+ if (
350
+ rule.serviceLevelObjectiveDescriptionPattern &&
351
+ (!serviceLevelObjective.description ||
352
+ !this.testPattern(
353
+ rule.serviceLevelObjectiveDescriptionPattern,
354
+ serviceLevelObjective.description,
355
+ rule,
356
+ ))
357
+ ) {
358
+ return false;
359
+ }
360
+
361
+ return true;
362
+ }
363
+
364
+ /*
365
+ * Patterns are regexes, with a '*' wildcard fallback - the same two syntaxes
366
+ * the SLO monitor rules take. SloRulePatternValidator rejects anything else
367
+ * on save, so this only warns about a rule written before that check.
368
+ */
369
+ private testPattern(
370
+ pattern: string,
371
+ value: string,
372
+ rule: ServiceLevelObjectiveLabelRule,
373
+ ): boolean {
374
+ if (!RulePatternMatchUtil.isSupportedPattern(pattern)) {
375
+ logger.warn(
376
+ `Invalid pattern in SLO label rule ${rule.id}: ${pattern}. It is neither a valid regular expression nor a wildcard pattern, so it will never match.`,
377
+ );
378
+ return false;
379
+ }
380
+
381
+ return RulePatternMatchUtil.matches(value, pattern);
382
+ }
383
+ }
384
+
385
+ export default new ServiceLevelObjectiveLabelRuleEngineServiceClass();
@@ -0,0 +1,51 @@
1
+ import DatabaseService from "./DatabaseService";
2
+ import Model from "../../Models/DatabaseModels/ServiceLevelObjectiveLabelRule";
3
+ import CreateBy from "../Types/Database/CreateBy";
4
+ import { OnCreate, OnUpdate } from "../Types/Database/Hooks";
5
+ import UpdateBy from "../Types/Database/UpdateBy";
6
+ import SloRulePatternValidator from "../Utils/Slo/SloRulePatternValidator";
7
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
8
+
9
+ /*
10
+ * No hard-delete retention: a rule is configuration, not an event log, so it
11
+ * stays until somebody deletes it.
12
+ */
13
+ export class Service extends DatabaseService<Model> {
14
+ public constructor() {
15
+ super(Model);
16
+ }
17
+
18
+ /*
19
+ * A pattern that is neither a valid regex nor a wildcard can only ever match
20
+ * nothing, and the rule engine has no way to say so - it just quietly stops
21
+ * labelling. Reject it while the user is still looking at the form.
22
+ */
23
+ @CaptureSpan()
24
+ protected override async onBeforeCreate(
25
+ createBy: CreateBy<Model>,
26
+ ): Promise<OnCreate<Model>> {
27
+ SloRulePatternValidator.validate({
28
+ namePattern: createBy.data.serviceLevelObjectiveNamePattern,
29
+ descriptionPattern: createBy.data.serviceLevelObjectiveDescriptionPattern,
30
+ });
31
+
32
+ return { createBy: createBy, carryForward: null };
33
+ }
34
+
35
+ @CaptureSpan()
36
+ protected override async onBeforeUpdate(
37
+ updateBy: UpdateBy<Model>,
38
+ ): Promise<OnUpdate<Model>> {
39
+ SloRulePatternValidator.validate({
40
+ namePattern: updateBy.data.serviceLevelObjectiveNamePattern as
41
+ | string
42
+ | undefined,
43
+ descriptionPattern: updateBy.data
44
+ .serviceLevelObjectiveDescriptionPattern as string | undefined,
45
+ });
46
+
47
+ return { updateBy: updateBy, carryForward: null };
48
+ }
49
+ }
50
+
51
+ export default new Service();
@@ -7,8 +7,19 @@ import { Gray500, Green500 } from "../../Types/BrandColors";
7
7
  import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
8
8
  import PartialEntity from "../../Types/Database/PartialEntity";
9
9
  import ObjectID from "../../Types/ObjectID";
10
+ import FilterCondition from "../../Types/Filter/FilterCondition";
11
+ import RuleCriteria, {
12
+ RuleCriteriaFilter,
13
+ RuleCriteriaOperator,
14
+ } from "../../Types/Rules/RuleCriteria";
15
+ import {
16
+ getMonitorTypeCriteriaValidationError,
17
+ isMonitorTypeCriteriaValue,
18
+ } from "../../Utils/Rules/MonitorTypeRuleCriteria";
10
19
  import { escapeMarkdownInline } from "../../Utils/Markdown/MarkdownEscape";
11
- import RuleCriteriaMatcher from "../../Utils/Rules/RuleCriteriaMatcher";
20
+ import RuleCriteriaMatcher, {
21
+ isValidRuleCriteria,
22
+ } from "../../Utils/Rules/RuleCriteriaMatcher";
12
23
  import { MAX_RULES_EVALUATED_PER_PROJECT } from "../../Utils/Rules/RuleEngineLimits";
13
24
  import RulePatternMatchUtil from "../../Utils/Rules/RulePatternMatchUtil";
14
25
  import QueryHelper from "../Types/Database/QueryHelper";
@@ -96,6 +107,7 @@ const RULE_SELECT: Select<ServiceLevelObjectiveMonitorRule> = {
96
107
  serviceLevelObjectiveId: true,
97
108
  isEnabled: true,
98
109
  monitorLabels: { _id: true },
110
+ monitorType: true,
99
111
  monitorNamePattern: true,
100
112
  monitorDescriptionPattern: true,
101
113
  criteria: true,
@@ -122,7 +134,7 @@ const MAX_MONITORS_LISTED_IN_FEED_DETAILS: number = 50;
122
134
  *
123
135
  * - the SLO side (one of its rules was created, edited, disabled or
124
136
  * deleted) -> syncMonitorsForSlo
125
- * - the monitor side (its labels, name or description changed, or it was
137
+ * - the monitor side (its labels, type, name or description changed, or it was
126
138
  * just created) -> syncSlosForMonitor
127
139
  *
128
140
  * Both write through ServiceLevelObjective.autoAddedMonitors, which records
@@ -215,6 +227,7 @@ export class ServiceLevelObjectiveMonitorRuleEngineServiceClass {
215
227
  projectId: true,
216
228
  name: true,
217
229
  description: true,
230
+ monitorType: true,
218
231
  labels: {
219
232
  _id: true,
220
233
  },
@@ -479,20 +492,64 @@ export class ServiceLevelObjectiveMonitorRuleEngineServiceClass {
479
492
  monitor: Monitor;
480
493
  rule: ServiceLevelObjectiveMonitorRule;
481
494
  }): boolean {
482
- return RuleCriteriaMatcher.matchesWithLegacySync({
483
- rule: data.rule,
484
- legacyFields: [
485
- "monitorLabels",
486
- "monitorNamePattern",
487
- "monitorDescriptionPattern",
488
- ],
495
+ const criteria: RuleCriteria | null | undefined = data.rule.criteria;
496
+
497
+ if (criteria === undefined || criteria === null) {
498
+ return this.doesMonitorMatchLegacyRule(data);
499
+ }
500
+
501
+ // A malformed type condition must fail the whole rule, even under Match any.
502
+ if (
503
+ !isValidRuleCriteria(criteria) ||
504
+ criteria.filters.some((filter: RuleCriteriaFilter): boolean => {
505
+ return (
506
+ filter.field === "monitorType" &&
507
+ getMonitorTypeCriteriaValidationError(filter) !== null
508
+ );
509
+ })
510
+ ) {
511
+ return false;
512
+ }
513
+
514
+ return RuleCriteriaMatcher.matchesSync({
515
+ criteria: criteria,
489
516
  emptyResult: false,
490
- matchesLegacyRule: (
491
- legacyRule: ServiceLevelObjectiveMonitorRule,
492
- ): boolean => {
493
- return this.doesMonitorMatchLegacyRule({
494
- monitor: data.monitor,
495
- rule: legacyRule,
517
+ matchesFilter: (filter: RuleCriteriaFilter): boolean => {
518
+ if (filter.field === "monitorType") {
519
+ if (!isMonitorTypeCriteriaValue(data.monitor.monitorType)) {
520
+ return false;
521
+ }
522
+
523
+ return filter.operator === RuleCriteriaOperator.Equals
524
+ ? data.monitor.monitorType === filter.value
525
+ : data.monitor.monitorType !== filter.value;
526
+ }
527
+
528
+ // Existing text and label conditions retain their legacy-compatible semantics.
529
+ return RuleCriteriaMatcher.matchesWithLegacySync({
530
+ rule: {
531
+ ...data.rule,
532
+ criteria: {
533
+ ...criteria,
534
+ filterCondition: FilterCondition.All,
535
+ filters: [filter],
536
+ },
537
+ } as ServiceLevelObjectiveMonitorRule,
538
+ legacyFields: [
539
+ "monitorLabels",
540
+ "monitorType",
541
+ "monitorNamePattern",
542
+ "monitorDescriptionPattern",
543
+ ],
544
+ emptyResult: false,
545
+ matchesLegacyRule: (
546
+ legacyRule: ServiceLevelObjectiveMonitorRule,
547
+ ): boolean => {
548
+ return this.doesMonitorMatchLegacyRule({
549
+ monitor: data.monitor,
550
+ rule: legacyRule,
551
+ });
552
+ },
496
553
  });
497
554
  },
498
555
  });
@@ -512,7 +569,24 @@ export class ServiceLevelObjectiveMonitorRuleEngineServiceClass {
512
569
  rule.monitorDescriptionPattern,
513
570
  );
514
571
 
515
- if (!hasLabelCriteria && !hasNameCriteria && !hasDescriptionCriteria) {
572
+ const hasTypeCriteria: boolean =
573
+ rule.monitorType !== undefined && rule.monitorType !== null;
574
+
575
+ if (
576
+ !hasLabelCriteria &&
577
+ !hasNameCriteria &&
578
+ !hasDescriptionCriteria &&
579
+ !hasTypeCriteria
580
+ ) {
581
+ return false;
582
+ }
583
+
584
+ if (
585
+ hasTypeCriteria &&
586
+ (!isMonitorTypeCriteriaValue(rule.monitorType) ||
587
+ !isMonitorTypeCriteriaValue(monitor.monitorType) ||
588
+ monitor.monitorType !== rule.monitorType)
589
+ ) {
516
590
  return false;
517
591
  }
518
592
 
@@ -638,6 +712,7 @@ export class ServiceLevelObjectiveMonitorRuleEngineServiceClass {
638
712
  _id: true,
639
713
  name: true,
640
714
  description: true,
715
+ monitorType: true,
641
716
  labels: {
642
717
  _id: true,
643
718
  },
@@ -663,6 +738,7 @@ export class ServiceLevelObjectiveMonitorRuleEngineServiceClass {
663
738
 
664
739
  return Boolean(
665
740
  (rule.monitorLabels && rule.monitorLabels.length > 0) ||
741
+ rule.monitorType ||
666
742
  rule.monitorNamePattern ||
667
743
  rule.monitorDescriptionPattern,
668
744
  );
@@ -11,6 +11,11 @@ import Color from "../../Types/Color";
11
11
  import LIMIT_MAX, { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
12
12
  import BadDataException from "../../Types/Exception/BadDataException";
13
13
  import ObjectID from "../../Types/ObjectID";
14
+ import MonitorType from "../../Types/Monitor/MonitorType";
15
+ import {
16
+ getMonitorTypeCriteriaValidationError,
17
+ isMonitorTypeCriteriaValue,
18
+ } from "../../Utils/Rules/MonitorTypeRuleCriteria";
14
19
  import { escapeMarkdownInline } from "../../Utils/Markdown/MarkdownEscape";
15
20
  import {
16
21
  getRuleCriteriaValidationError,
@@ -51,6 +56,7 @@ const RULE_SNAPSHOT_SELECT: Select<Model> = {
51
56
  _id: true,
52
57
  name: true,
53
58
  },
59
+ monitorType: true,
54
60
  monitorNamePattern: true,
55
61
  monitorDescriptionPattern: true,
56
62
  criteria: true,
@@ -92,6 +98,7 @@ export class Service extends DatabaseService<Model> {
92
98
 
93
99
  this.assertHasMatchCriteria({
94
100
  monitorLabelCount: (createBy.data.monitorLabels || []).length,
101
+ monitorType: createBy.data.monitorType,
95
102
  monitorNamePattern: createBy.data.monitorNamePattern,
96
103
  monitorDescriptionPattern: createBy.data.monitorDescriptionPattern,
97
104
  criteria: createBy.data.criteria,
@@ -188,6 +195,7 @@ export class Service extends DatabaseService<Model> {
188
195
 
189
196
  const touchesCriteria: boolean =
190
197
  updateBy.data.monitorLabels !== undefined ||
198
+ updateBy.data.monitorType !== undefined ||
191
199
  updateBy.data.monitorNamePattern !== undefined ||
192
200
  updateBy.data.monitorDescriptionPattern !== undefined ||
193
201
  updateBy.data.criteria !== undefined;
@@ -656,6 +664,7 @@ export class Service extends DatabaseService<Model> {
656
664
  */
657
665
  private assertHasMatchCriteria(data: {
658
666
  monitorLabelCount: number;
667
+ monitorType?: MonitorType | undefined;
659
668
  monitorNamePattern?: string | undefined;
660
669
  monitorDescriptionPattern?: string | undefined;
661
670
  criteria?: Model["criteria"];
@@ -673,6 +682,17 @@ export class Service extends DatabaseService<Model> {
673
682
  isValidRuleCriteria(data.criteria) &&
674
683
  data.criteria.filters.length > 0
675
684
  ) {
685
+ for (const filter of data.criteria.filters) {
686
+ if (filter.field === "monitorType") {
687
+ const typeError: string | null =
688
+ getMonitorTypeCriteriaValidationError(filter);
689
+
690
+ if (typeError) {
691
+ throw new BadDataException(typeError);
692
+ }
693
+ }
694
+ }
695
+
676
696
  return;
677
697
  }
678
698
 
@@ -681,13 +701,24 @@ export class Service extends DatabaseService<Model> {
681
701
  );
682
702
  }
683
703
 
704
+ if (
705
+ data.monitorType !== undefined &&
706
+ data.monitorType !== null &&
707
+ !isMonitorTypeCriteriaValue(data.monitorType)
708
+ ) {
709
+ throw new BadDataException(
710
+ "Monitor type criteria require a valid monitor type.",
711
+ );
712
+ }
713
+
684
714
  if (
685
715
  data.monitorLabelCount === 0 &&
716
+ !data.monitorType &&
686
717
  !data.monitorNamePattern &&
687
718
  !data.monitorDescriptionPattern
688
719
  ) {
689
720
  throw new BadDataException(
690
- "An SLO monitor rule needs at least one match criterion: monitor labels, a monitor name pattern, or a monitor description pattern. Use .* as the name pattern to match every monitor in the project.",
721
+ "An SLO monitor rule needs at least one match criterion: monitor labels, a monitor type, a monitor name pattern, or a monitor description pattern. Use .* as the name pattern to match every monitor in the project.",
691
722
  );
692
723
  }
693
724
  }
@@ -744,6 +775,10 @@ export class Service extends DatabaseService<Model> {
744
775
  ? rule.monitorLabels || []
745
776
  : nextLabels || []
746
777
  ).length,
778
+ monitorType:
779
+ updateBy.data.monitorType === undefined
780
+ ? rule.monitorType
781
+ : (updateBy.data.monitorType as MonitorType | undefined),
747
782
  monitorNamePattern:
748
783
  updateBy.data.monitorNamePattern === undefined
749
784
  ? rule.monitorNamePattern