@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,521 @@
1
+ import Label from "../../Models/DatabaseModels/Label";
2
+ import ServiceLevelObjective from "../../Models/DatabaseModels/ServiceLevelObjective";
3
+ import { ServiceLevelObjectiveFeedEventType } from "../../Models/DatabaseModels/ServiceLevelObjectiveFeed";
4
+ import ServiceLevelObjectiveOwnerRule from "../../Models/DatabaseModels/ServiceLevelObjectiveOwnerRule";
5
+ import ServiceLevelObjectiveOwnerTeam from "../../Models/DatabaseModels/ServiceLevelObjectiveOwnerTeam";
6
+ import ServiceLevelObjectiveOwnerUser from "../../Models/DatabaseModels/ServiceLevelObjectiveOwnerUser";
7
+ import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
8
+ import { Purple500 } from "../../Types/BrandColors";
9
+ import BadDataException from "../../Types/Exception/BadDataException";
10
+ import ObjectID from "../../Types/ObjectID";
11
+ import { escapeMarkdownInline } from "../../Utils/Markdown/MarkdownEscape";
12
+ import { MAX_RULES_EVALUATED_PER_PROJECT } from "../../Utils/Rules/RuleEngineLimits";
13
+ import { RuleCriteriaMatcher } from "../../Utils/Rules/RuleCriteriaMatcher";
14
+ import RulePatternMatchUtil from "../../Utils/Rules/RulePatternMatchUtil";
15
+ import Select from "../Types/Database/Select";
16
+ import logger, { LogAttributes } from "../Utils/Logger";
17
+ import OwnerRuleAssignment, {
18
+ OwnersToAssign,
19
+ } from "../Utils/Rules/OwnerRuleAssignment";
20
+ import logIfRuleReadWasTruncated from "../Utils/Rules/RuleEngineRuleRead";
21
+ import {
22
+ ApplyRulesToExistingResourceData,
23
+ RuleApplicationResult,
24
+ RuleApplicationResultUtil,
25
+ RuleRunEngine,
26
+ } from "../Utils/Rules/RuleRun/RuleApplication";
27
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
28
+ import DatabaseService from "./DatabaseService";
29
+ import ServiceLevelObjectiveFeedService from "./ServiceLevelObjectiveFeedService";
30
+ import ServiceLevelObjectiveOwnerRuleService from "./ServiceLevelObjectiveOwnerRuleService";
31
+ import ServiceLevelObjectiveOwnerTeamService from "./ServiceLevelObjectiveOwnerTeamService";
32
+ import ServiceLevelObjectiveOwnerUserService from "./ServiceLevelObjectiveOwnerUserService";
33
+ import ServiceLevelObjectiveService from "./ServiceLevelObjectiveService";
34
+
35
+ // What adding one owner row did.
36
+ enum OwnerWriteOutcome {
37
+ Added = "Added",
38
+ AlreadyOwner = "AlreadyOwner",
39
+ Rejected = "Rejected",
40
+ }
41
+
42
+ class ServiceLevelObjectiveOwnerRuleEngineServiceClass
43
+ implements
44
+ RuleRunEngine<ServiceLevelObjective, ServiceLevelObjectiveOwnerRule>
45
+ {
46
+ public readonly ruleSelect: Select<ServiceLevelObjectiveOwnerRule> = {
47
+ _id: true,
48
+ name: true,
49
+ criteria: true,
50
+ notifyOwners: true,
51
+ serviceLevelObjectiveLabels: { _id: true },
52
+ serviceLevelObjectiveNamePattern: true,
53
+ serviceLevelObjectiveDescriptionPattern: true,
54
+ ownerUsers: { _id: true },
55
+ ownerTeams: { _id: true },
56
+ };
57
+
58
+ // Evaluation re-reads the SLO, so a run only has to name it.
59
+ public readonly resourceSelectForRuleRun: Select<ServiceLevelObjective> = {
60
+ _id: true,
61
+ projectId: true,
62
+ };
63
+
64
+ /**
65
+ * Evaluates ServiceLevelObjectiveOwnerRule rows for the given SLO and adds
66
+ * matched owner users / teams through the SLO owner services, so each one
67
+ * gets the usual owner-added feed item and notification. Rules with
68
+ * notifyOwners set notify the added owners; rules with notifyOwners off add
69
+ * silently.
70
+ */
71
+ @CaptureSpan()
72
+ public async applyRulesToServiceLevelObjective(
73
+ serviceLevelObjective: ServiceLevelObjective,
74
+ ): Promise<void> {
75
+ if (!serviceLevelObjective.id || !serviceLevelObjective.projectId) {
76
+ return;
77
+ }
78
+
79
+ try {
80
+ const rules: Array<ServiceLevelObjectiveOwnerRule> =
81
+ await ServiceLevelObjectiveOwnerRuleService.findBy({
82
+ query: {
83
+ projectId: serviceLevelObjective.projectId,
84
+ isEnabled: true,
85
+ },
86
+ props: { isRoot: true },
87
+ select: this.ruleSelect,
88
+ limit: MAX_RULES_EVALUATED_PER_PROJECT,
89
+ skip: 0,
90
+ });
91
+
92
+ logIfRuleReadWasTruncated({
93
+ ruleKind: "ServiceLevelObjectiveOwnerRule",
94
+ projectId: serviceLevelObjective.projectId,
95
+ rulesRead: rules.length,
96
+ });
97
+
98
+ if (rules.length === 0) {
99
+ return;
100
+ }
101
+
102
+ await this.applyRules({
103
+ serviceLevelObjective: serviceLevelObjective,
104
+ rules: rules,
105
+ allowOwnerNotification: true,
106
+ });
107
+ } catch (error) {
108
+ logger.error(`Error applying SLO owner rules: ${error}`, {
109
+ projectId: serviceLevelObjective.projectId?.toString(),
110
+ serviceLevelObjectiveId: serviceLevelObjective.id?.toString(),
111
+ } as LogAttributes);
112
+ }
113
+ }
114
+
115
+ /*
116
+ * "Run now": the same evaluation, for an SLO that already exists and only
117
+ * the rules being run.
118
+ */
119
+ @CaptureSpan()
120
+ public async applyRulesToExistingResource(
121
+ data: ApplyRulesToExistingResourceData<
122
+ ServiceLevelObjective,
123
+ ServiceLevelObjectiveOwnerRule
124
+ >,
125
+ ): Promise<RuleApplicationResult> {
126
+ try {
127
+ return await this.applyRules({
128
+ serviceLevelObjective: data.resource,
129
+ rules: data.rules,
130
+ allowOwnerNotification: data.allowOwnerNotification,
131
+ });
132
+ } catch (error) {
133
+ logger.error(`Error running SLO owner rules: ${error}`, {
134
+ projectId: data.resource.projectId?.toString(),
135
+ serviceLevelObjectiveId: data.resource.id?.toString(),
136
+ } as LogAttributes);
137
+
138
+ return RuleApplicationResultUtil.failed();
139
+ }
140
+ }
141
+
142
+ private async applyRules(data: {
143
+ serviceLevelObjective: ServiceLevelObjective;
144
+ rules: Array<ServiceLevelObjectiveOwnerRule>;
145
+ allowOwnerNotification: boolean;
146
+ }): Promise<RuleApplicationResult> {
147
+ const { serviceLevelObjective, rules } = data;
148
+
149
+ if (
150
+ !serviceLevelObjective.id ||
151
+ !serviceLevelObjective.projectId ||
152
+ rules.length === 0
153
+ ) {
154
+ return RuleApplicationResultUtil.noMatch();
155
+ }
156
+
157
+ const sloWithDetails: ServiceLevelObjective | null =
158
+ await ServiceLevelObjectiveService.findOneById({
159
+ id: serviceLevelObjective.id,
160
+ select: {
161
+ name: true,
162
+ description: true,
163
+ labels: { _id: true },
164
+ },
165
+ props: { isRoot: true },
166
+ });
167
+
168
+ if (!sloWithDetails) {
169
+ return RuleApplicationResultUtil.noMatch();
170
+ }
171
+
172
+ const usersByNotify: Map<boolean, Set<string>> = new Map([
173
+ [true, new Set()],
174
+ [false, new Set()],
175
+ ]);
176
+ const teamsByNotify: Map<boolean, Set<string>> = new Map([
177
+ [true, new Set()],
178
+ [false, new Set()],
179
+ ]);
180
+
181
+ const matchedRules: Array<ServiceLevelObjectiveOwnerRule> = [];
182
+ const allUserIds: Set<string> = new Set();
183
+ const allTeamIds: Set<string> = new Set();
184
+ let anyRuleMatched: boolean = false;
185
+
186
+ for (const rule of rules) {
187
+ if (!this.doesServiceLevelObjectiveMatchRule(sloWithDetails, rule)) {
188
+ continue;
189
+ }
190
+ anyRuleMatched = true;
191
+ let ruleAddedAny: boolean = false;
192
+ const notify: boolean =
193
+ rule.notifyOwners !== false && data.allowOwnerNotification;
194
+ for (const user of rule.ownerUsers || []) {
195
+ if (user.id) {
196
+ usersByNotify.get(notify)!.add(user.id.toString());
197
+ allUserIds.add(user.id.toString());
198
+ ruleAddedAny = true;
199
+ }
200
+ }
201
+ for (const team of rule.ownerTeams || []) {
202
+ if (team.id) {
203
+ teamsByNotify.get(notify)!.add(team.id.toString());
204
+ allTeamIds.add(team.id.toString());
205
+ ruleAddedAny = true;
206
+ }
207
+ }
208
+ if (ruleAddedAny) {
209
+ matchedRules.push(rule);
210
+ }
211
+ }
212
+
213
+ if (!anyRuleMatched) {
214
+ return RuleApplicationResultUtil.noMatch();
215
+ }
216
+
217
+ // The rules that matched name no owners, so there is nothing to add.
218
+ if (matchedRules.length === 0) {
219
+ return RuleApplicationResultUtil.alreadyApplied();
220
+ }
221
+
222
+ // Owners already on the SLO are skipped rather than duplicated.
223
+ const notYetAssigned: OwnersToAssign =
224
+ await OwnerRuleAssignment.getOwnersNotYetAssigned({
225
+ ownerUserService: ServiceLevelObjectiveOwnerUserService,
226
+ ownerTeamService: ServiceLevelObjectiveOwnerTeamService,
227
+ resourceIdColumn: "serviceLevelObjectiveId",
228
+ resourceId: serviceLevelObjective.id,
229
+ userIds: Array.from(allUserIds),
230
+ teamIds: Array.from(allTeamIds),
231
+ });
232
+
233
+ const userIdsToAdd: Set<string> = new Set(
234
+ notYetAssigned.userIds.map((id: ObjectID): string => {
235
+ return id.toString();
236
+ }),
237
+ );
238
+ const teamIdsToAdd: Set<string> = new Set(
239
+ notYetAssigned.teamIds.map((id: ObjectID): string => {
240
+ return id.toString();
241
+ }),
242
+ );
243
+
244
+ let ownersAdded: number = 0;
245
+ let ownersRejected: number = 0;
246
+
247
+ const count: (outcome: OwnerWriteOutcome) => void = (
248
+ outcome: OwnerWriteOutcome,
249
+ ): void => {
250
+ if (outcome === OwnerWriteOutcome.Added) {
251
+ ownersAdded++;
252
+ } else if (outcome === OwnerWriteOutcome.Rejected) {
253
+ ownersRejected++;
254
+ }
255
+ };
256
+
257
+ /*
258
+ * The notifying set goes first, so an owner two matching rules disagree
259
+ * about is added once, and notified.
260
+ */
261
+ for (const notify of [true, false]) {
262
+ const userIds: Array<string> = Array.from(
263
+ usersByNotify.get(notify)!,
264
+ ).filter((id: string): boolean => {
265
+ return userIdsToAdd.delete(id);
266
+ });
267
+ const teamIds: Array<string> = Array.from(
268
+ teamsByNotify.get(notify)!,
269
+ ).filter((id: string): boolean => {
270
+ return teamIdsToAdd.delete(id);
271
+ });
272
+
273
+ for (const userId of userIds) {
274
+ const owner: ServiceLevelObjectiveOwnerUser =
275
+ new ServiceLevelObjectiveOwnerUser();
276
+ owner.serviceLevelObjectiveId = serviceLevelObjective.id;
277
+ owner.projectId = serviceLevelObjective.projectId;
278
+ owner.userId = new ObjectID(userId);
279
+ owner.isOwnerNotified = !notify;
280
+ count(
281
+ await this.addOwner({
282
+ ownerService: ServiceLevelObjectiveOwnerUserService,
283
+ owner: owner,
284
+ serviceLevelObjective: serviceLevelObjective,
285
+ }),
286
+ );
287
+ }
288
+
289
+ for (const teamId of teamIds) {
290
+ const owner: ServiceLevelObjectiveOwnerTeam =
291
+ new ServiceLevelObjectiveOwnerTeam();
292
+ owner.serviceLevelObjectiveId = serviceLevelObjective.id;
293
+ owner.projectId = serviceLevelObjective.projectId;
294
+ owner.teamId = new ObjectID(teamId);
295
+ owner.isOwnerNotified = !notify;
296
+ count(
297
+ await this.addOwner({
298
+ ownerService: ServiceLevelObjectiveOwnerTeamService,
299
+ owner: owner,
300
+ serviceLevelObjective: serviceLevelObjective,
301
+ }),
302
+ );
303
+ }
304
+ }
305
+
306
+ if (ownersAdded === 0) {
307
+ /*
308
+ * Every owner the rules name was refused - a user who has since left the
309
+ * project, say. Nothing was added, and "already applied" would claim the
310
+ * SLO has owners it does not, so report the SLO as one the run could not
311
+ * update.
312
+ */
313
+ return ownersRejected > 0
314
+ ? RuleApplicationResultUtil.failed()
315
+ : RuleApplicationResultUtil.alreadyApplied();
316
+ }
317
+
318
+ logger.debug(
319
+ `ServiceLevelObjectiveOwnerRuleEngine added owners to SLO ${serviceLevelObjective.id}`,
320
+ {
321
+ projectId: serviceLevelObjective.projectId.toString(),
322
+ } as LogAttributes,
323
+ );
324
+
325
+ await this.postOwnerRuleFeedItem({
326
+ serviceLevelObjectiveId: serviceLevelObjective.id,
327
+ projectId: serviceLevelObjective.projectId,
328
+ sloName: sloWithDetails.name,
329
+ matchedRules: matchedRules,
330
+ });
331
+
332
+ return RuleApplicationResultUtil.updated(ownersAdded);
333
+ }
334
+
335
+ /*
336
+ * SLO owner services refuse a user who is not a member of the project, or a
337
+ * team from another one (see SloOwnerReferenceValidator) - which a rule can
338
+ * still name after the user left, or when it was written through the API.
339
+ * That owner is skipped so the rule's other owners are still added; any
340
+ * other failure is thrown as before.
341
+ */
342
+ private async addOwner<TOwner extends BaseModel>(data: {
343
+ ownerService: DatabaseService<TOwner>;
344
+ owner: TOwner;
345
+ serviceLevelObjective: ServiceLevelObjective;
346
+ }): Promise<OwnerWriteOutcome> {
347
+ try {
348
+ const added: boolean = await OwnerRuleAssignment.createOwner({
349
+ ownerService: data.ownerService,
350
+ owner: data.owner,
351
+ props: { isRoot: true },
352
+ });
353
+
354
+ return added ? OwnerWriteOutcome.Added : OwnerWriteOutcome.AlreadyOwner;
355
+ } catch (error) {
356
+ if (!(error instanceof BadDataException)) {
357
+ throw error;
358
+ }
359
+
360
+ logger.warn(
361
+ `SLO owner rule could not add an owner to SLO ${data.serviceLevelObjective.id?.toString()}: ${error.message}`,
362
+ {
363
+ projectId: data.serviceLevelObjective.projectId?.toString(),
364
+ } as LogAttributes,
365
+ );
366
+
367
+ return OwnerWriteOutcome.Rejected;
368
+ }
369
+ }
370
+
371
+ /*
372
+ * The individual OwnerUserAdded / OwnerTeamAdded items say who was added;
373
+ * this one says which rule is responsible, which is what somebody asking
374
+ * "why am I on the hook for this SLO?" actually needs. The owners are
375
+ * already added by now, so a feed failure is logged, never thrown.
376
+ */
377
+ private async postOwnerRuleFeedItem(data: {
378
+ serviceLevelObjectiveId: ObjectID;
379
+ projectId: ObjectID;
380
+ sloName: string | undefined;
381
+ matchedRules: Array<ServiceLevelObjectiveOwnerRule>;
382
+ }): Promise<void> {
383
+ try {
384
+ const sloLink: string =
385
+ await ServiceLevelObjectiveService.getSloMarkdownLink({
386
+ projectId: data.projectId,
387
+ sloId: data.serviceLevelObjectiveId,
388
+ sloName: data.sloName,
389
+ });
390
+
391
+ await ServiceLevelObjectiveFeedService.createServiceLevelObjectiveFeedItem(
392
+ {
393
+ serviceLevelObjectiveId: data.serviceLevelObjectiveId,
394
+ projectId: data.projectId,
395
+ serviceLevelObjectiveFeedEventType:
396
+ ServiceLevelObjectiveFeedEventType.OwnerRuleExecuted,
397
+ displayColor: Purple500,
398
+ feedInfoInMarkdown: `👥 Owners were added to ${sloLink} by ${data.matchedRules.length} owner ${data.matchedRules.length === 1 ? "rule" : "rules"}.`,
399
+ moreInformationInMarkdown: `**Owner rules that matched**: ${data.matchedRules
400
+ .map((rule: ServiceLevelObjectiveOwnerRule): string => {
401
+ // Rule names are user-controlled and the feed renders markdown.
402
+ return `**${escapeMarkdownInline(
403
+ rule.name || rule.id?.toString() || "Unnamed rule",
404
+ )}**`;
405
+ })
406
+ .join(", ")}`,
407
+ },
408
+ );
409
+ } catch (error) {
410
+ logger.error(
411
+ `Error writing the owner rule feed item for SLO ${data.serviceLevelObjectiveId.toString()}: ${error}`,
412
+ { projectId: data.projectId.toString() } as LogAttributes,
413
+ );
414
+ }
415
+ }
416
+
417
+ private doesServiceLevelObjectiveMatchRule(
418
+ serviceLevelObjective: ServiceLevelObjective,
419
+ rule: ServiceLevelObjectiveOwnerRule,
420
+ ): boolean {
421
+ return RuleCriteriaMatcher.matchesWithLegacySync({
422
+ rule,
423
+ legacyFields: [
424
+ "serviceLevelObjectiveLabels",
425
+ "serviceLevelObjectiveNamePattern",
426
+ "serviceLevelObjectiveDescriptionPattern",
427
+ ],
428
+ emptyResult: true,
429
+ matchesLegacyRule: (
430
+ legacyRule: ServiceLevelObjectiveOwnerRule,
431
+ ): boolean => {
432
+ return this.doesServiceLevelObjectiveMatchLegacyRule(
433
+ serviceLevelObjective,
434
+ legacyRule,
435
+ );
436
+ },
437
+ });
438
+ }
439
+
440
+ private doesServiceLevelObjectiveMatchLegacyRule(
441
+ serviceLevelObjective: ServiceLevelObjective,
442
+ rule: ServiceLevelObjectiveOwnerRule,
443
+ ): boolean {
444
+ if (
445
+ rule.serviceLevelObjectiveLabels &&
446
+ rule.serviceLevelObjectiveLabels.length > 0
447
+ ) {
448
+ if (
449
+ !serviceLevelObjective.labels ||
450
+ serviceLevelObjective.labels.length === 0
451
+ ) {
452
+ return false;
453
+ }
454
+ const ruleLabelIds: Array<string> = rule.serviceLevelObjectiveLabels.map(
455
+ (l: Label) => {
456
+ return l.id?.toString() || "";
457
+ },
458
+ );
459
+ const labelIds: Array<string> = serviceLevelObjective.labels.map(
460
+ (l: Label) => {
461
+ return l.id?.toString() || "";
462
+ },
463
+ );
464
+ if (
465
+ !ruleLabelIds.some((id: string) => {
466
+ return labelIds.includes(id);
467
+ })
468
+ ) {
469
+ return false;
470
+ }
471
+ }
472
+
473
+ if (
474
+ rule.serviceLevelObjectiveNamePattern &&
475
+ (!serviceLevelObjective.name ||
476
+ !this.testPattern(
477
+ rule.serviceLevelObjectiveNamePattern,
478
+ serviceLevelObjective.name,
479
+ rule,
480
+ ))
481
+ ) {
482
+ return false;
483
+ }
484
+
485
+ if (
486
+ rule.serviceLevelObjectiveDescriptionPattern &&
487
+ (!serviceLevelObjective.description ||
488
+ !this.testPattern(
489
+ rule.serviceLevelObjectiveDescriptionPattern,
490
+ serviceLevelObjective.description,
491
+ rule,
492
+ ))
493
+ ) {
494
+ return false;
495
+ }
496
+
497
+ return true;
498
+ }
499
+
500
+ /*
501
+ * Patterns are regexes, with a '*' wildcard fallback - the same two syntaxes
502
+ * the SLO monitor rules take. SloRulePatternValidator rejects anything else
503
+ * on save, so this only warns about a rule written before that check.
504
+ */
505
+ private testPattern(
506
+ pattern: string,
507
+ value: string,
508
+ rule: ServiceLevelObjectiveOwnerRule,
509
+ ): boolean {
510
+ if (!RulePatternMatchUtil.isSupportedPattern(pattern)) {
511
+ logger.warn(
512
+ `Invalid pattern in SLO owner rule ${rule.id}: ${pattern}. It is neither a valid regular expression nor a wildcard pattern, so it will never match.`,
513
+ );
514
+ return false;
515
+ }
516
+
517
+ return RulePatternMatchUtil.matches(value, pattern);
518
+ }
519
+ }
520
+
521
+ export default new ServiceLevelObjectiveOwnerRuleEngineServiceClass();
@@ -0,0 +1,51 @@
1
+ import DatabaseService from "./DatabaseService";
2
+ import Model from "../../Models/DatabaseModels/ServiceLevelObjectiveOwnerRule";
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
+ * assigning owners. 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();
@@ -27,6 +27,8 @@ import DatabaseService from "./DatabaseService";
27
27
  import MonitorStatusService from "./MonitorStatusService";
28
28
  import ProjectService from "./ProjectService";
29
29
  import ServiceLevelObjectiveBurnRateRuleService from "./ServiceLevelObjectiveBurnRateRuleService";
30
+ import ServiceLevelObjectiveLabelRuleEngineService from "./ServiceLevelObjectiveLabelRuleEngineService";
31
+ import ServiceLevelObjectiveOwnerRuleEngineService from "./ServiceLevelObjectiveOwnerRuleEngineService";
30
32
  import ServiceLevelObjectiveMonitorRuleEngineService from "./ServiceLevelObjectiveMonitorRuleEngineService";
31
33
  import ServiceLevelObjectiveMonitorRuleService from "./ServiceLevelObjectiveMonitorRuleService";
32
34
  import ServiceLevelObjectiveOwnerTeamService from "./ServiceLevelObjectiveOwnerTeamService";
@@ -287,6 +289,32 @@ export class Service extends DatabaseService<Model> {
287
289
  );
288
290
  }
289
291
 
292
+ /*
293
+ * Label rules first, so the labels they attach are persisted - and on
294
+ * createdItem - before owner rules run: an owner rule can then key on a
295
+ * rule-added label. Fire-and-forget, like every other resource's rules: a
296
+ * rule must never slow down or fail the create.
297
+ */
298
+ if (createdItem.projectId && createdItem.id) {
299
+ Promise.resolve()
300
+ .then(async () => {
301
+ await ServiceLevelObjectiveLabelRuleEngineService.applyRulesToServiceLevelObjective(
302
+ createdItem,
303
+ );
304
+ })
305
+ .then(async () => {
306
+ await ServiceLevelObjectiveOwnerRuleEngineService.applyRulesToServiceLevelObjective(
307
+ createdItem,
308
+ );
309
+ })
310
+ .catch((err: Error) => {
311
+ logger.error(
312
+ `Error applying the label and owner rules for SLO ${createdItem.id?.toString()}: ${err}`,
313
+ { projectId: createdItem.projectId?.toString() } as LogAttributes,
314
+ );
315
+ });
316
+ }
317
+
290
318
  /*
291
319
  * Fire-and-forget, like ServiceService's created item: the SLO exists
292
320
  * whether or not its feed item does, and the create request should not
@@ -13,7 +13,17 @@ import TelegramComponents from "../../../../../Types/Workflow/Components/Telegra
13
13
  import API from "../../../../../Utils/API";
14
14
  import CaptureSpan from "../../../../Utils/Telemetry/CaptureSpan";
15
15
 
16
+ /*
17
+ * https://core.telegram.org/bots/api#authorizing-your-bot — a numeric bot id,
18
+ * a ":", then the secret, e.g. "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11".
19
+ */
20
+ const TELEGRAM_BOT_TOKEN_REGEX: RegExp = /^\d+:[A-Za-z0-9_-]+$/;
21
+
16
22
  export default class SendMessageToChat extends ComponentCode {
23
+ public static isValidBotToken(botToken: string): boolean {
24
+ return TELEGRAM_BOT_TOKEN_REGEX.test(botToken);
25
+ }
26
+
17
27
  public constructor() {
18
28
  super();
19
29
 
@@ -69,10 +79,26 @@ export default class SendMessageToChat extends ComponentCode {
69
79
  throw options.onError(new BadDataException("Telegram message not found"));
70
80
  }
71
81
 
72
- const botToken: string = args["bot-token"]?.toString() as string;
82
+ // A token pasted or stored with a trailing newline is still the token.
83
+ const botToken: string = (args["bot-token"]?.toString() || "").trim();
73
84
  const chatId: string = args["chat-id"]?.toString() as string;
74
85
  const text: string = args["text"]?.toString() as string;
75
86
 
87
+ /*
88
+ * The token becomes a path segment of the request URL, so anything other
89
+ * than Telegram's "<bot id>:<secret>" shape is refused before it gets
90
+ * there. The message deliberately does not quote the value: the step's
91
+ * error is written to the run log, and the token is a credential. Before
92
+ * this check, a URL parse failure put the whole token into the log.
93
+ */
94
+ if (!SendMessageToChat.isValidBotToken(botToken)) {
95
+ throw options.onError(
96
+ new BadDataException(
97
+ "Telegram Bot Token is not in a valid format. It should look like 123456789:ABCdefGhIJKlmnoPQRstuVWxyz, exactly as BotFather issued it.",
98
+ ),
99
+ );
100
+ }
101
+
76
102
  const telegramApiUrl: URL = URL.fromString(
77
103
  `https://api.telegram.org/bot${botToken}/sendMessage`,
78
104
  );
@@ -382,6 +382,7 @@ export const QuerySlosTool: ObservabilityTool = {
382
382
  _id: true,
383
383
  name: true,
384
384
  },
385
+ monitorType: true,
385
386
  monitorNamePattern: true,
386
387
  monitorDescriptionPattern: true,
387
388
  criteria: true,