@oneuptime/common 10.0.65 → 10.0.66

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 (210) hide show
  1. package/Models/DatabaseModels/DockerHostOwnerTeam.ts +464 -0
  2. package/Models/DatabaseModels/DockerHostOwnerUser.ts +463 -0
  3. package/Models/DatabaseModels/Index.ts +24 -0
  4. package/Models/DatabaseModels/KubernetesClusterOwnerTeam.ts +464 -0
  5. package/Models/DatabaseModels/KubernetesClusterOwnerUser.ts +463 -0
  6. package/Models/DatabaseModels/KubernetesResource.ts +548 -0
  7. package/Models/DatabaseModels/MetricPipelineRule.ts +804 -0
  8. package/Models/DatabaseModels/MetricRecordingRule.ts +470 -0
  9. package/Models/DatabaseModels/Monitor.ts +2 -0
  10. package/Models/DatabaseModels/Project.ts +53 -0
  11. package/Models/DatabaseModels/Service.ts +79 -0
  12. package/Models/DatabaseModels/TraceDropFilter.ts +508 -0
  13. package/Models/DatabaseModels/TracePipeline.ts +436 -0
  14. package/Models/DatabaseModels/TracePipelineProcessor.ts +454 -0
  15. package/Models/DatabaseModels/TraceRecordingRule.ts +470 -0
  16. package/Models/DatabaseModels/TraceScrubRule.ts +546 -0
  17. package/Server/API/KubernetesResourceAPI.ts +129 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1776504277320-MigrationName.ts +399 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/1776505976155-AddTracePipelineTables.ts +205 -0
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/1776509413763-MigrationName.ts +335 -0
  21. package/Server/Infrastructure/Postgres/SchemaMigrations/1776541018853-MigrationName.ts +29 -0
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1776544084793-MigrationName.ts +53 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -1
  24. package/Server/Services/DockerHostOwnerTeamService.ts +10 -0
  25. package/Server/Services/DockerHostOwnerUserService.ts +10 -0
  26. package/Server/Services/KubernetesClusterOwnerTeamService.ts +10 -0
  27. package/Server/Services/KubernetesClusterOwnerUserService.ts +10 -0
  28. package/Server/Services/KubernetesResourceService.ts +351 -0
  29. package/Server/Services/MetricPipelineRuleService.ts +10 -0
  30. package/Server/Services/MetricRecordingRuleService.ts +10 -0
  31. package/Server/Services/TraceDropFilterService.ts +10 -0
  32. package/Server/Services/TracePipelineProcessorService.ts +10 -0
  33. package/Server/Services/TracePipelineService.ts +10 -0
  34. package/Server/Services/TraceRecordingRuleService.ts +10 -0
  35. package/Server/Services/TraceScrubRuleService.ts +10 -0
  36. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +71 -9
  37. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +483 -75
  38. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +379 -6
  39. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +502 -0
  40. package/Tests/Utils/MetricUnitUtil.test.ts +216 -0
  41. package/Tests/Utils/Metrics/MetricFormulaEvaluator.test.ts +269 -0
  42. package/Tests/Utils/Metrics/MetricResultUnitConverter.test.ts +231 -0
  43. package/Tests/Utils/RecordingRuleExpression.test.ts +177 -0
  44. package/Types/Kubernetes/KubernetesInventoryExtractor.ts +327 -0
  45. package/Types/Kubernetes/KubernetesObjectParser.ts +1949 -0
  46. package/Types/Metrics/MetricDownsamplingRetentionDays.ts +49 -0
  47. package/Types/Metrics/MetricFormulaConfigData.ts +4 -0
  48. package/Types/Metrics/MetricPipelineRuleFilterCondition.ts +136 -0
  49. package/Types/Metrics/MetricPipelineRuleType.ts +27 -0
  50. package/Types/Metrics/RecordingRuleDefinition.ts +180 -0
  51. package/Types/Monitor/CriteriaFilter.ts +43 -0
  52. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +70 -0
  53. package/Types/Permission.ts +520 -0
  54. package/Types/Trace/TraceAggregationType.ts +17 -0
  55. package/Types/Trace/TraceDropFilterAction.ts +6 -0
  56. package/Types/Trace/TracePipelineProcessorType.ts +56 -0
  57. package/Types/Trace/TraceRecordingRuleDefinition.ts +218 -0
  58. package/Types/Trace/TraceScrubAction.ts +7 -0
  59. package/Types/Trace/TraceScrubField.ts +8 -0
  60. package/Types/Trace/TraceScrubPatternType.ts +10 -0
  61. package/UI/Components/CardSelect/CardSelect.tsx +9 -1
  62. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +6 -10
  63. package/UI/Components/Forms/Fields/FormField.tsx +1 -0
  64. package/UI/Components/Forms/Types/Field.ts +1 -0
  65. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +57 -0
  66. package/UI/Components/Page/Page.tsx +6 -0
  67. package/Utils/MetricUnitUtil.ts +289 -0
  68. package/Utils/Metrics/MetricFormulaEvaluator.ts +610 -0
  69. package/Utils/Metrics/MetricResultUnitConverter.ts +91 -0
  70. package/Utils/Metrics/RecordingRuleExpression.ts +359 -0
  71. package/Utils/ValueFormatter.ts +137 -13
  72. package/build/dist/Models/DatabaseModels/DockerHostOwnerTeam.js +480 -0
  73. package/build/dist/Models/DatabaseModels/DockerHostOwnerTeam.js.map +1 -0
  74. package/build/dist/Models/DatabaseModels/DockerHostOwnerUser.js +479 -0
  75. package/build/dist/Models/DatabaseModels/DockerHostOwnerUser.js.map +1 -0
  76. package/build/dist/Models/DatabaseModels/Index.js +24 -0
  77. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  78. package/build/dist/Models/DatabaseModels/KubernetesClusterOwnerTeam.js +480 -0
  79. package/build/dist/Models/DatabaseModels/KubernetesClusterOwnerTeam.js.map +1 -0
  80. package/build/dist/Models/DatabaseModels/KubernetesClusterOwnerUser.js +479 -0
  81. package/build/dist/Models/DatabaseModels/KubernetesClusterOwnerUser.js.map +1 -0
  82. package/build/dist/Models/DatabaseModels/KubernetesResource.js +590 -0
  83. package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -0
  84. package/build/dist/Models/DatabaseModels/MetricPipelineRule.js +836 -0
  85. package/build/dist/Models/DatabaseModels/MetricPipelineRule.js.map +1 -0
  86. package/build/dist/Models/DatabaseModels/MetricRecordingRule.js +497 -0
  87. package/build/dist/Models/DatabaseModels/MetricRecordingRule.js.map +1 -0
  88. package/build/dist/Models/DatabaseModels/Monitor.js +2 -0
  89. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  90. package/build/dist/Models/DatabaseModels/Project.js +53 -0
  91. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  92. package/build/dist/Models/DatabaseModels/Service.js +79 -0
  93. package/build/dist/Models/DatabaseModels/Service.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/TraceDropFilter.js +536 -0
  95. package/build/dist/Models/DatabaseModels/TraceDropFilter.js.map +1 -0
  96. package/build/dist/Models/DatabaseModels/TracePipeline.js +462 -0
  97. package/build/dist/Models/DatabaseModels/TracePipeline.js.map +1 -0
  98. package/build/dist/Models/DatabaseModels/TracePipelineProcessor.js +476 -0
  99. package/build/dist/Models/DatabaseModels/TracePipelineProcessor.js.map +1 -0
  100. package/build/dist/Models/DatabaseModels/TraceRecordingRule.js +497 -0
  101. package/build/dist/Models/DatabaseModels/TraceRecordingRule.js.map +1 -0
  102. package/build/dist/Models/DatabaseModels/TraceScrubRule.js +575 -0
  103. package/build/dist/Models/DatabaseModels/TraceScrubRule.js.map +1 -0
  104. package/build/dist/Server/API/KubernetesResourceAPI.js +98 -0
  105. package/build/dist/Server/API/KubernetesResourceAPI.js.map +1 -0
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776504277320-MigrationName.js +144 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776504277320-MigrationName.js.map +1 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776505976155-AddTracePipelineTables.js +82 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776505976155-AddTracePipelineTables.js.map +1 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776509413763-MigrationName.js +118 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776509413763-MigrationName.js.map +1 -0
  112. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776541018853-MigrationName.js +16 -0
  113. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776541018853-MigrationName.js.map +1 -0
  114. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776544084793-MigrationName.js +24 -0
  115. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776544084793-MigrationName.js.map +1 -0
  116. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
  117. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  118. package/build/dist/Server/Services/DockerHostOwnerTeamService.js +9 -0
  119. package/build/dist/Server/Services/DockerHostOwnerTeamService.js.map +1 -0
  120. package/build/dist/Server/Services/DockerHostOwnerUserService.js +9 -0
  121. package/build/dist/Server/Services/DockerHostOwnerUserService.js.map +1 -0
  122. package/build/dist/Server/Services/KubernetesClusterOwnerTeamService.js +9 -0
  123. package/build/dist/Server/Services/KubernetesClusterOwnerTeamService.js.map +1 -0
  124. package/build/dist/Server/Services/KubernetesClusterOwnerUserService.js +9 -0
  125. package/build/dist/Server/Services/KubernetesClusterOwnerUserService.js.map +1 -0
  126. package/build/dist/Server/Services/KubernetesResourceService.js +237 -0
  127. package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -0
  128. package/build/dist/Server/Services/MetricPipelineRuleService.js +9 -0
  129. package/build/dist/Server/Services/MetricPipelineRuleService.js.map +1 -0
  130. package/build/dist/Server/Services/MetricRecordingRuleService.js +9 -0
  131. package/build/dist/Server/Services/MetricRecordingRuleService.js.map +1 -0
  132. package/build/dist/Server/Services/TraceDropFilterService.js +9 -0
  133. package/build/dist/Server/Services/TraceDropFilterService.js.map +1 -0
  134. package/build/dist/Server/Services/TracePipelineProcessorService.js +9 -0
  135. package/build/dist/Server/Services/TracePipelineProcessorService.js.map +1 -0
  136. package/build/dist/Server/Services/TracePipelineService.js +9 -0
  137. package/build/dist/Server/Services/TracePipelineService.js.map +1 -0
  138. package/build/dist/Server/Services/TraceRecordingRuleService.js +9 -0
  139. package/build/dist/Server/Services/TraceRecordingRuleService.js.map +1 -0
  140. package/build/dist/Server/Services/TraceScrubRuleService.js +9 -0
  141. package/build/dist/Server/Services/TraceScrubRuleService.js.map +1 -0
  142. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +56 -9
  143. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  144. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +335 -53
  145. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  146. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +277 -5
  147. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  148. package/build/dist/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.js +407 -0
  149. package/build/dist/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.js.map +1 -0
  150. package/build/dist/Tests/Utils/MetricUnitUtil.test.js +159 -0
  151. package/build/dist/Tests/Utils/MetricUnitUtil.test.js.map +1 -0
  152. package/build/dist/Tests/Utils/Metrics/MetricFormulaEvaluator.test.js +224 -0
  153. package/build/dist/Tests/Utils/Metrics/MetricFormulaEvaluator.test.js.map +1 -0
  154. package/build/dist/Tests/Utils/Metrics/MetricResultUnitConverter.test.js +180 -0
  155. package/build/dist/Tests/Utils/Metrics/MetricResultUnitConverter.test.js.map +1 -0
  156. package/build/dist/Tests/Utils/RecordingRuleExpression.test.js +142 -0
  157. package/build/dist/Tests/Utils/RecordingRuleExpression.test.js.map +1 -0
  158. package/build/dist/Types/Kubernetes/KubernetesInventoryExtractor.js +200 -0
  159. package/build/dist/Types/Kubernetes/KubernetesInventoryExtractor.js.map +1 -0
  160. package/build/dist/Types/Kubernetes/KubernetesObjectParser.js +1205 -0
  161. package/build/dist/Types/Kubernetes/KubernetesObjectParser.js.map +1 -0
  162. package/build/dist/Types/Metrics/MetricDownsamplingRetentionDays.js +32 -0
  163. package/build/dist/Types/Metrics/MetricDownsamplingRetentionDays.js.map +1 -0
  164. package/build/dist/Types/Metrics/MetricPipelineRuleFilterCondition.js +103 -0
  165. package/build/dist/Types/Metrics/MetricPipelineRuleFilterCondition.js.map +1 -0
  166. package/build/dist/Types/Metrics/MetricPipelineRuleType.js +27 -0
  167. package/build/dist/Types/Metrics/MetricPipelineRuleType.js.map +1 -0
  168. package/build/dist/Types/Metrics/RecordingRuleDefinition.js +110 -0
  169. package/build/dist/Types/Metrics/RecordingRuleDefinition.js.map +1 -0
  170. package/build/dist/Types/Monitor/CriteriaFilter.js +22 -0
  171. package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
  172. package/build/dist/Types/Monitor/MetricMonitor/MetricCriteriaContext.js +2 -0
  173. package/build/dist/Types/Monitor/MetricMonitor/MetricCriteriaContext.js.map +1 -0
  174. package/build/dist/Types/Permission.js +454 -0
  175. package/build/dist/Types/Permission.js.map +1 -1
  176. package/build/dist/Types/Trace/TraceAggregationType.js +18 -0
  177. package/build/dist/Types/Trace/TraceAggregationType.js.map +1 -0
  178. package/build/dist/Types/Trace/TraceDropFilterAction.js +7 -0
  179. package/build/dist/Types/Trace/TraceDropFilterAction.js.map +1 -0
  180. package/build/dist/Types/Trace/TracePipelineProcessorType.js +10 -0
  181. package/build/dist/Types/Trace/TracePipelineProcessorType.js.map +1 -0
  182. package/build/dist/Types/Trace/TraceRecordingRuleDefinition.js +145 -0
  183. package/build/dist/Types/Trace/TraceRecordingRuleDefinition.js.map +1 -0
  184. package/build/dist/Types/Trace/TraceScrubAction.js +8 -0
  185. package/build/dist/Types/Trace/TraceScrubAction.js.map +1 -0
  186. package/build/dist/Types/Trace/TraceScrubField.js +9 -0
  187. package/build/dist/Types/Trace/TraceScrubField.js.map +1 -0
  188. package/build/dist/Types/Trace/TraceScrubPatternType.js +11 -0
  189. package/build/dist/Types/Trace/TraceScrubPatternType.js.map +1 -0
  190. package/build/dist/UI/Components/CardSelect/CardSelect.js +3 -1
  191. package/build/dist/UI/Components/CardSelect/CardSelect.js.map +1 -1
  192. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +6 -9
  193. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  194. package/build/dist/UI/Components/Forms/Fields/FormField.js +1 -1
  195. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  196. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +30 -0
  197. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  198. package/build/dist/UI/Components/Page/Page.js +1 -0
  199. package/build/dist/UI/Components/Page/Page.js.map +1 -1
  200. package/build/dist/Utils/MetricUnitUtil.js +232 -0
  201. package/build/dist/Utils/MetricUnitUtil.js.map +1 -0
  202. package/build/dist/Utils/Metrics/MetricFormulaEvaluator.js +453 -0
  203. package/build/dist/Utils/Metrics/MetricFormulaEvaluator.js.map +1 -0
  204. package/build/dist/Utils/Metrics/MetricResultUnitConverter.js +61 -0
  205. package/build/dist/Utils/Metrics/MetricResultUnitConverter.js.map +1 -0
  206. package/build/dist/Utils/Metrics/RecordingRuleExpression.js +298 -0
  207. package/build/dist/Utils/Metrics/RecordingRuleExpression.js.map +1 -0
  208. package/build/dist/Utils/ValueFormatter.js +123 -13
  209. package/build/dist/Utils/ValueFormatter.js.map +1 -1
  210. package/package.json +1 -1
@@ -0,0 +1,610 @@
1
+ import BadDataException from "../../Types/Exception/BadDataException";
2
+ import AggregatedModel from "../../Types/BaseDatabase/AggregatedModel";
3
+ import AggregatedResult from "../../Types/BaseDatabase/AggregatedResult";
4
+ import MetricFormulaConfigData from "../../Types/Metrics/MetricFormulaConfigData";
5
+ import MetricQueryConfigData from "../../Types/Metrics/MetricQueryConfigData";
6
+
7
+ /**
8
+ * Shunting-yard based evaluator for metric formulas such as
9
+ * "$A + $B * 2" or "(a - b) / 100". Variables are matched by metric
10
+ * alias (case-insensitive, with or without a leading "$"). Evaluates
11
+ * the formula point-by-point against a time-aligned series from the
12
+ * referenced queries/formulas.
13
+ */
14
+
15
+ enum TokenType {
16
+ Number = "number",
17
+ Variable = "variable",
18
+ Operator = "operator",
19
+ LeftParen = "lparen",
20
+ RightParen = "rparen",
21
+ }
22
+
23
+ interface Token {
24
+ type: TokenType;
25
+ value: string;
26
+ }
27
+
28
+ type UnaryOperator = "u-" | "u+";
29
+ type BinaryOperator = "+" | "-" | "*" | "/" | "%" | "^";
30
+ type Operator = UnaryOperator | BinaryOperator;
31
+
32
+ const OPERATOR_PRECEDENCE: Record<Operator, number> = {
33
+ "u-": 4,
34
+ "u+": 4,
35
+ "^": 3,
36
+ "*": 2,
37
+ "/": 2,
38
+ "%": 2,
39
+ "+": 1,
40
+ "-": 1,
41
+ };
42
+
43
+ const RIGHT_ASSOCIATIVE: Set<Operator> = new Set<Operator>(["^", "u-", "u+"]);
44
+
45
+ export interface FormulaPoint {
46
+ timestamp: Date | string;
47
+ value: number;
48
+ }
49
+
50
+ export default class MetricFormulaEvaluator {
51
+ /**
52
+ * Evaluate a formula against the provided query/formula results and
53
+ * return a synthetic AggregatedResult whose timestamps are the union
54
+ * of all referenced series.
55
+ */
56
+ public static evaluateFormula(input: {
57
+ formula: string;
58
+ queryConfigs: Array<MetricQueryConfigData>;
59
+ formulaConfigs: Array<MetricFormulaConfigData>;
60
+ results: Array<AggregatedResult>;
61
+ }): AggregatedResult {
62
+ const trimmedFormula: string = (input.formula || "").trim();
63
+ if (!trimmedFormula) {
64
+ return { data: [] };
65
+ }
66
+
67
+ const rpn: Array<Token> = MetricFormulaEvaluator.toRpn(trimmedFormula);
68
+
69
+ const variableResults: Record<string, AggregatedResult> =
70
+ MetricFormulaEvaluator.buildVariableResultMap({
71
+ queryConfigs: input.queryConfigs,
72
+ formulaConfigs: input.formulaConfigs,
73
+ results: input.results,
74
+ });
75
+
76
+ /*
77
+ * Validate every variable referenced in the formula actually resolves
78
+ * to a result series. Failing loudly is better than silently returning
79
+ * NaN when a user typos an alias.
80
+ */
81
+ const referencedVariables: Array<string> =
82
+ MetricFormulaEvaluator.collectVariableNames(rpn);
83
+
84
+ for (const variableName of referencedVariables) {
85
+ if (!variableResults[variableName]) {
86
+ throw new BadDataException(
87
+ `Formula references unknown variable "$${variableName}". Define a metric query with that alias first.`,
88
+ );
89
+ }
90
+ }
91
+
92
+ const timestampIndex: Map<
93
+ string,
94
+ Record<string, number>
95
+ > = MetricFormulaEvaluator.buildTimestampIndex(
96
+ referencedVariables,
97
+ variableResults,
98
+ );
99
+
100
+ const sortedTimestamps: Array<string> = Array.from(
101
+ timestampIndex.keys(),
102
+ ).sort();
103
+
104
+ const resultData: Array<AggregatedModel> = [];
105
+
106
+ for (const timestampString of sortedTimestamps) {
107
+ const values: Record<string, number> =
108
+ timestampIndex.get(timestampString) || {};
109
+
110
+ /*
111
+ * Skip points where any referenced variable is missing a value. A
112
+ * partial join would produce misleading numbers (e.g. treating a
113
+ * gap as zero silently when using subtraction).
114
+ */
115
+ const hasAllValues: boolean = referencedVariables.every(
116
+ (variable: string) => {
117
+ return typeof values[variable] === "number";
118
+ },
119
+ );
120
+
121
+ if (!hasAllValues) {
122
+ continue;
123
+ }
124
+
125
+ let evaluated: number;
126
+ try {
127
+ evaluated = MetricFormulaEvaluator.evaluateRpn(rpn, values);
128
+ } catch {
129
+ continue;
130
+ }
131
+
132
+ if (!Number.isFinite(evaluated)) {
133
+ continue;
134
+ }
135
+
136
+ resultData.push({
137
+ timestamp: new Date(timestampString),
138
+ value: evaluated,
139
+ });
140
+ }
141
+
142
+ return { data: resultData };
143
+ }
144
+
145
+ /**
146
+ * Return the set of variables referenced by a formula, preserving the
147
+ * order of first appearance. Variable names are lower-cased to match
148
+ * the evaluator's case-insensitive lookup. Invalid formulas return an
149
+ * empty list rather than throwing, so callers rendering UI don't have
150
+ * to add defensive try/catch.
151
+ */
152
+ public static getReferencedVariables(formula: string): Array<string> {
153
+ const trimmedFormula: string = (formula || "").trim();
154
+ if (!trimmedFormula) {
155
+ return [];
156
+ }
157
+ try {
158
+ const rpn: Array<Token> = MetricFormulaEvaluator.toRpn(trimmedFormula);
159
+ return MetricFormulaEvaluator.collectVariableNames(rpn);
160
+ } catch {
161
+ return [];
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Validate a formula's syntax without evaluating it. Returns `null`
167
+ * when valid, otherwise a human-readable error message.
168
+ */
169
+ public static validateFormula(input: {
170
+ formula: string;
171
+ availableVariables: Array<string>;
172
+ }): string | null {
173
+ const trimmedFormula: string = (input.formula || "").trim();
174
+ if (!trimmedFormula) {
175
+ return "Formula is required.";
176
+ }
177
+
178
+ let rpn: Array<Token>;
179
+ try {
180
+ rpn = MetricFormulaEvaluator.toRpn(trimmedFormula);
181
+ } catch (err: unknown) {
182
+ return (err as Error).message || "Invalid formula.";
183
+ }
184
+
185
+ const referenced: Array<string> =
186
+ MetricFormulaEvaluator.collectVariableNames(rpn);
187
+ const available: Set<string> = new Set<string>(
188
+ input.availableVariables.map((v: string) => {
189
+ return v.toLowerCase();
190
+ }),
191
+ );
192
+
193
+ for (const variable of referenced) {
194
+ if (!available.has(variable.toLowerCase())) {
195
+ return `Formula references unknown variable "$${variable}".`;
196
+ }
197
+ }
198
+
199
+ return null;
200
+ }
201
+
202
+ private static buildVariableResultMap(input: {
203
+ queryConfigs: Array<MetricQueryConfigData>;
204
+ formulaConfigs: Array<MetricFormulaConfigData>;
205
+ results: Array<AggregatedResult>;
206
+ }): Record<string, AggregatedResult> {
207
+ const variableMap: Record<string, AggregatedResult> = {};
208
+
209
+ const totalSeries: number =
210
+ input.queryConfigs.length + input.formulaConfigs.length;
211
+
212
+ for (let index: number = 0; index < totalSeries; index++) {
213
+ const result: AggregatedResult | undefined = input.results[index];
214
+ if (!result) {
215
+ continue;
216
+ }
217
+
218
+ let alias: string | undefined;
219
+ if (index < input.queryConfigs.length) {
220
+ alias =
221
+ input.queryConfigs[index]?.metricAliasData?.metricVariable ||
222
+ undefined;
223
+ } else {
224
+ const formulaIndex: number = index - input.queryConfigs.length;
225
+ alias =
226
+ input.formulaConfigs[formulaIndex]?.metricAliasData?.metricVariable ||
227
+ undefined;
228
+ }
229
+
230
+ if (!alias) {
231
+ continue;
232
+ }
233
+
234
+ variableMap[alias.toLowerCase()] = result;
235
+ }
236
+
237
+ return variableMap;
238
+ }
239
+
240
+ private static buildTimestampIndex(
241
+ variables: Array<string>,
242
+ variableResults: Record<string, AggregatedResult>,
243
+ ): Map<string, Record<string, number>> {
244
+ const index: Map<string, Record<string, number>> = new Map();
245
+
246
+ for (const variable of variables) {
247
+ const series: AggregatedResult | undefined = variableResults[variable];
248
+ if (!series) {
249
+ continue;
250
+ }
251
+
252
+ for (const sample of series.data) {
253
+ const timestampKey: string = MetricFormulaEvaluator.normalizeTimestamp(
254
+ sample.timestamp,
255
+ );
256
+
257
+ if (!index.has(timestampKey)) {
258
+ index.set(timestampKey, {});
259
+ }
260
+
261
+ const bucket: Record<string, number> = index.get(timestampKey) || {};
262
+ bucket[variable] = sample.value;
263
+ }
264
+ }
265
+
266
+ return index;
267
+ }
268
+
269
+ private static normalizeTimestamp(timestamp: Date | string): string {
270
+ if (timestamp instanceof Date) {
271
+ return timestamp.toISOString();
272
+ }
273
+
274
+ /*
275
+ * ClickHouse sometimes returns timestamps at varying precisions
276
+ * ("2024-01-01T00:00:00" vs "2024-01-01T00:00:00.000Z"). Normalize
277
+ * by parsing through Date so differently formatted strings for the
278
+ * same instant align correctly.
279
+ */
280
+ const asDate: Date = new Date(timestamp);
281
+ if (!isNaN(asDate.getTime())) {
282
+ return asDate.toISOString();
283
+ }
284
+ return String(timestamp);
285
+ }
286
+
287
+ private static collectVariableNames(rpn: Array<Token>): Array<string> {
288
+ const seen: Set<string> = new Set<string>();
289
+ const result: Array<string> = [];
290
+ for (const token of rpn) {
291
+ if (token.type === TokenType.Variable) {
292
+ const normalized: string = token.value.toLowerCase();
293
+ if (!seen.has(normalized)) {
294
+ seen.add(normalized);
295
+ result.push(normalized);
296
+ }
297
+ }
298
+ }
299
+ return result;
300
+ }
301
+
302
+ private static tokenize(expression: string): Array<Token> {
303
+ const tokens: Array<Token> = [];
304
+ let position: number = 0;
305
+
306
+ while (position < expression.length) {
307
+ const char: string = expression[position]!;
308
+
309
+ if (char === " " || char === "\t" || char === "\n") {
310
+ position++;
311
+ continue;
312
+ }
313
+
314
+ // Number literal (with optional decimal and exponent)
315
+ if (char >= "0" && char <= "9") {
316
+ let numberBuffer: string = "";
317
+ while (
318
+ position < expression.length &&
319
+ MetricFormulaEvaluator.isNumberChar(
320
+ expression[position]!,
321
+ numberBuffer,
322
+ )
323
+ ) {
324
+ numberBuffer += expression[position];
325
+ position++;
326
+ }
327
+ tokens.push({ type: TokenType.Number, value: numberBuffer });
328
+ continue;
329
+ }
330
+
331
+ // Decimal starting without leading 0 (e.g. ".5")
332
+ if (
333
+ char === "." &&
334
+ position + 1 < expression.length &&
335
+ expression[position + 1]! >= "0" &&
336
+ expression[position + 1]! <= "9"
337
+ ) {
338
+ let numberBuffer: string = "";
339
+ while (
340
+ position < expression.length &&
341
+ MetricFormulaEvaluator.isNumberChar(
342
+ expression[position]!,
343
+ numberBuffer,
344
+ )
345
+ ) {
346
+ numberBuffer += expression[position];
347
+ position++;
348
+ }
349
+ tokens.push({ type: TokenType.Number, value: numberBuffer });
350
+ continue;
351
+ }
352
+
353
+ // Variable — may be prefixed with "$" or bare ("a", "b1", etc.)
354
+ if (char === "$" || MetricFormulaEvaluator.isIdentifierStart(char)) {
355
+ if (char === "$") {
356
+ position++;
357
+ }
358
+ let identifier: string = "";
359
+ while (
360
+ position < expression.length &&
361
+ MetricFormulaEvaluator.isIdentifierPart(expression[position]!)
362
+ ) {
363
+ identifier += expression[position];
364
+ position++;
365
+ }
366
+
367
+ if (!identifier) {
368
+ throw new BadDataException(
369
+ `Unexpected character "$" without a variable name.`,
370
+ );
371
+ }
372
+
373
+ tokens.push({ type: TokenType.Variable, value: identifier });
374
+ continue;
375
+ }
376
+
377
+ if (char === "(") {
378
+ tokens.push({ type: TokenType.LeftParen, value: char });
379
+ position++;
380
+ continue;
381
+ }
382
+
383
+ if (char === ")") {
384
+ tokens.push({ type: TokenType.RightParen, value: char });
385
+ position++;
386
+ continue;
387
+ }
388
+
389
+ if (
390
+ char === "+" ||
391
+ char === "-" ||
392
+ char === "*" ||
393
+ char === "/" ||
394
+ char === "%" ||
395
+ char === "^"
396
+ ) {
397
+ tokens.push({ type: TokenType.Operator, value: char });
398
+ position++;
399
+ continue;
400
+ }
401
+
402
+ throw new BadDataException(
403
+ `Unexpected character "${char}" at position ${position}.`,
404
+ );
405
+ }
406
+
407
+ return tokens;
408
+ }
409
+
410
+ private static isNumberChar(char: string, currentBuffer: string): boolean {
411
+ if (char >= "0" && char <= "9") {
412
+ return true;
413
+ }
414
+ if (char === "." && !currentBuffer.includes(".")) {
415
+ return true;
416
+ }
417
+ if (
418
+ (char === "e" || char === "E") &&
419
+ !currentBuffer.toLowerCase().includes("e") &&
420
+ currentBuffer.length > 0
421
+ ) {
422
+ return true;
423
+ }
424
+ if (
425
+ (char === "+" || char === "-") &&
426
+ currentBuffer.length > 0 &&
427
+ (currentBuffer[currentBuffer.length - 1] === "e" ||
428
+ currentBuffer[currentBuffer.length - 1] === "E")
429
+ ) {
430
+ return true;
431
+ }
432
+ return false;
433
+ }
434
+
435
+ private static isIdentifierStart(char: string): boolean {
436
+ return (
437
+ (char >= "a" && char <= "z") ||
438
+ (char >= "A" && char <= "Z") ||
439
+ char === "_"
440
+ );
441
+ }
442
+
443
+ private static isIdentifierPart(char: string): boolean {
444
+ return (
445
+ MetricFormulaEvaluator.isIdentifierStart(char) ||
446
+ (char >= "0" && char <= "9")
447
+ );
448
+ }
449
+
450
+ private static toRpn(expression: string): Array<Token> {
451
+ const tokens: Array<Token> = MetricFormulaEvaluator.tokenize(expression);
452
+ const output: Array<Token> = [];
453
+ const operatorStack: Array<Token> = [];
454
+
455
+ let previousToken: Token | null = null;
456
+
457
+ for (const token of tokens) {
458
+ if (token.type === TokenType.Number) {
459
+ output.push(token);
460
+ } else if (token.type === TokenType.Variable) {
461
+ output.push(token);
462
+ } else if (token.type === TokenType.Operator) {
463
+ let operatorValue: Operator = token.value as Operator;
464
+ const isUnary: boolean =
465
+ (operatorValue === "+" || operatorValue === "-") &&
466
+ (previousToken === null ||
467
+ previousToken.type === TokenType.Operator ||
468
+ previousToken.type === TokenType.LeftParen);
469
+
470
+ if (isUnary) {
471
+ operatorValue = operatorValue === "-" ? "u-" : "u+";
472
+ }
473
+
474
+ while (operatorStack.length > 0) {
475
+ const top: Token = operatorStack[operatorStack.length - 1]!;
476
+ if (top.type !== TokenType.Operator) {
477
+ break;
478
+ }
479
+ const topPrecedence: number =
480
+ OPERATOR_PRECEDENCE[top.value as Operator];
481
+ const currentPrecedence: number = OPERATOR_PRECEDENCE[operatorValue];
482
+ const isRightAssociative: boolean =
483
+ RIGHT_ASSOCIATIVE.has(operatorValue);
484
+
485
+ if (
486
+ topPrecedence > currentPrecedence ||
487
+ (topPrecedence === currentPrecedence && !isRightAssociative)
488
+ ) {
489
+ output.push(operatorStack.pop()!);
490
+ } else {
491
+ break;
492
+ }
493
+ }
494
+
495
+ operatorStack.push({ type: TokenType.Operator, value: operatorValue });
496
+ } else if (token.type === TokenType.LeftParen) {
497
+ operatorStack.push(token);
498
+ } else if (token.type === TokenType.RightParen) {
499
+ let foundLeftParen: boolean = false;
500
+ while (operatorStack.length > 0) {
501
+ const top: Token = operatorStack.pop()!;
502
+ if (top.type === TokenType.LeftParen) {
503
+ foundLeftParen = true;
504
+ break;
505
+ }
506
+ output.push(top);
507
+ }
508
+ if (!foundLeftParen) {
509
+ throw new BadDataException("Mismatched parentheses in formula.");
510
+ }
511
+ }
512
+
513
+ previousToken = token;
514
+ }
515
+
516
+ while (operatorStack.length > 0) {
517
+ const top: Token = operatorStack.pop()!;
518
+ if (
519
+ top.type === TokenType.LeftParen ||
520
+ top.type === TokenType.RightParen
521
+ ) {
522
+ throw new BadDataException("Mismatched parentheses in formula.");
523
+ }
524
+ output.push(top);
525
+ }
526
+
527
+ return output;
528
+ }
529
+
530
+ private static evaluateRpn(
531
+ rpn: Array<Token>,
532
+ variableValues: Record<string, number>,
533
+ ): number {
534
+ const stack: Array<number> = [];
535
+
536
+ for (const token of rpn) {
537
+ if (token.type === TokenType.Number) {
538
+ stack.push(parseFloat(token.value));
539
+ continue;
540
+ }
541
+
542
+ if (token.type === TokenType.Variable) {
543
+ const variableName: string = token.value.toLowerCase();
544
+ const variableValue: number | undefined = variableValues[variableName];
545
+ if (typeof variableValue !== "number") {
546
+ throw new BadDataException(
547
+ `Missing value for variable "${variableName}".`,
548
+ );
549
+ }
550
+ stack.push(variableValue);
551
+ continue;
552
+ }
553
+
554
+ if (token.type === TokenType.Operator) {
555
+ const operatorValue: Operator = token.value as Operator;
556
+
557
+ if (operatorValue === "u-" || operatorValue === "u+") {
558
+ const operand: number | undefined = stack.pop();
559
+ if (typeof operand !== "number") {
560
+ throw new BadDataException("Invalid formula: missing operand.");
561
+ }
562
+ stack.push(operatorValue === "u-" ? -operand : operand);
563
+ continue;
564
+ }
565
+
566
+ const right: number | undefined = stack.pop();
567
+ const left: number | undefined = stack.pop();
568
+
569
+ if (typeof right !== "number" || typeof left !== "number") {
570
+ throw new BadDataException("Invalid formula: missing operands.");
571
+ }
572
+
573
+ switch (operatorValue) {
574
+ case "+":
575
+ stack.push(left + right);
576
+ break;
577
+ case "-":
578
+ stack.push(left - right);
579
+ break;
580
+ case "*":
581
+ stack.push(left * right);
582
+ break;
583
+ case "/":
584
+ if (right === 0) {
585
+ stack.push(NaN);
586
+ } else {
587
+ stack.push(left / right);
588
+ }
589
+ break;
590
+ case "%":
591
+ if (right === 0) {
592
+ stack.push(NaN);
593
+ } else {
594
+ stack.push(left % right);
595
+ }
596
+ break;
597
+ case "^":
598
+ stack.push(Math.pow(left, right));
599
+ break;
600
+ }
601
+ }
602
+ }
603
+
604
+ if (stack.length !== 1) {
605
+ throw new BadDataException("Invalid formula expression.");
606
+ }
607
+
608
+ return stack[0]!;
609
+ }
610
+ }
@@ -0,0 +1,91 @@
1
+ import AggregatedModel from "../../Types/BaseDatabase/AggregatedModel";
2
+ import AggregatedResult from "../../Types/BaseDatabase/AggregatedResult";
3
+ import MetricQueryConfigData from "../../Types/Metrics/MetricQueryConfigData";
4
+ import MetricUnitUtil from "../MetricUnitUtil";
5
+
6
+ /**
7
+ * Convert aggregated metric results from each metric's native unit
8
+ * (reported by OpenTelemetry / stored in MetricType) into the unit the
9
+ * user configured in the query's alias ("legendUnit"). This is what
10
+ * makes downstream consumers — charts, formulas, and threshold
11
+ * comparisons — all speak the same unit as the user picked.
12
+ *
13
+ * Called after results are fetched but before formulas are evaluated.
14
+ * When `legendUnit` is empty or matches the native unit (or the two
15
+ * aren't in the same unit family), the raw value is passed through
16
+ * unchanged, so this is always safe to call.
17
+ */
18
+ export default class MetricResultUnitConverter {
19
+ public static convertQueryResultsToDisplayUnit(input: {
20
+ queryConfigs: Array<MetricQueryConfigData>;
21
+ results: Array<AggregatedResult>;
22
+ nativeUnitByMetricName: Map<string, string>;
23
+ }): Array<AggregatedResult> {
24
+ const converted: Array<AggregatedResult> = [];
25
+
26
+ for (let index: number = 0; index < input.results.length; index++) {
27
+ const rawResult: AggregatedResult | undefined = input.results[index];
28
+ if (!rawResult) {
29
+ converted.push({ data: [] });
30
+ continue;
31
+ }
32
+
33
+ const queryConfig: MetricQueryConfigData | undefined =
34
+ input.queryConfigs[index];
35
+
36
+ if (!queryConfig) {
37
+ converted.push(rawResult);
38
+ continue;
39
+ }
40
+
41
+ const metricName: string | undefined =
42
+ queryConfig.metricQueryData?.filterData?.metricName?.toString();
43
+ const nativeUnit: string | undefined = metricName
44
+ ? input.nativeUnitByMetricName.get(metricName.toLowerCase())
45
+ : undefined;
46
+ const displayUnit: string | undefined =
47
+ queryConfig.metricAliasData?.legendUnit || undefined;
48
+
49
+ if (!nativeUnit || !displayUnit || nativeUnit === displayUnit) {
50
+ converted.push(rawResult);
51
+ continue;
52
+ }
53
+
54
+ converted.push(
55
+ MetricResultUnitConverter.convertResult({
56
+ result: rawResult,
57
+ fromUnit: nativeUnit,
58
+ toUnit: displayUnit,
59
+ }),
60
+ );
61
+ }
62
+
63
+ return converted;
64
+ }
65
+
66
+ public static convertResult(input: {
67
+ result: AggregatedResult;
68
+ fromUnit: string;
69
+ toUnit: string;
70
+ }): AggregatedResult {
71
+ if (input.fromUnit === input.toUnit) {
72
+ return input.result;
73
+ }
74
+
75
+ const convertedData: Array<AggregatedModel> = input.result.data.map(
76
+ (row: AggregatedModel) => {
77
+ const convertedValue: number = MetricUnitUtil.convertToMetricUnit({
78
+ value: row.value,
79
+ fromUnit: input.fromUnit,
80
+ metricUnit: input.toUnit,
81
+ });
82
+ return {
83
+ ...row,
84
+ value: convertedValue,
85
+ };
86
+ },
87
+ );
88
+
89
+ return { data: convertedData };
90
+ }
91
+ }