@oneuptime/common 10.0.54 → 10.0.56

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 (132) hide show
  1. package/Models/DatabaseModels/DockerHost.ts +662 -0
  2. package/Models/DatabaseModels/GlobalConfig.ts +112 -0
  3. package/Models/DatabaseModels/Index.ts +2 -0
  4. package/Server/API/TelemetryAPI.ts +352 -16
  5. package/Server/Infrastructure/ClickhouseConfig.ts +9 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/1774000000002-MigrationName.ts +76 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1775766676723-MigrationName.ts +133 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1775900000000-AddGlobalSmtpOAuth.ts +51 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  10. package/Server/Services/DockerHostService.ts +173 -0
  11. package/Server/Services/ExceptionAggregationService.ts +335 -0
  12. package/Server/Services/Index.ts +2 -0
  13. package/Server/Services/LogAggregationService.ts +17 -0
  14. package/Server/Services/MonitorProbeService.ts +42 -21
  15. package/Server/Services/MonitorService.ts +21 -21
  16. package/Server/Services/TraceAggregationService.ts +514 -0
  17. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +73 -1
  18. package/Tests/Server/Services/LogAggregationService.test.ts +2 -2
  19. package/Tests/__mocks__/mermaid.js +18 -0
  20. package/Tests/__mocks__/react-markdown.js +17 -0
  21. package/Tests/__mocks__/react-syntax-highlighter.js +19 -0
  22. package/Tests/__mocks__/remark-gfm.js +8 -0
  23. package/Types/Icon/IconProp.ts +1 -0
  24. package/Types/Monitor/DockerAlertTemplates.ts +507 -0
  25. package/Types/Monitor/DockerMetricCatalog.ts +226 -0
  26. package/Types/Monitor/MonitorStep.ts +33 -0
  27. package/Types/Monitor/MonitorStepDockerMonitor.ts +38 -0
  28. package/Types/Monitor/MonitorType.ts +15 -1
  29. package/Types/Permission.ts +38 -0
  30. package/UI/Components/Icon/Icon.tsx +87 -0
  31. package/UI/Components/Markdown.tsx/MarkdownEditor.tsx +7 -132
  32. package/UI/Components/ModelDetail/CardModelDetail.tsx +11 -1
  33. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +285 -0
  34. package/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.tsx +85 -0
  35. package/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.tsx +156 -0
  36. package/UI/Components/TelemetryViewer/components/TelemetryFacetSection.tsx +160 -0
  37. package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +85 -0
  38. package/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx +102 -0
  39. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +280 -0
  40. package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +125 -0
  41. package/UI/Components/TelemetryViewer/components/TelemetryPagination.tsx +114 -0
  42. package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +378 -0
  43. package/UI/Components/TelemetryViewer/components/TelemetrySearchHelp.tsx +78 -0
  44. package/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.tsx +64 -0
  45. package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +193 -0
  46. package/UI/Components/TelemetryViewer/types.ts +67 -0
  47. package/build/dist/Models/DatabaseModels/DockerHost.js +686 -0
  48. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -0
  49. package/build/dist/Models/DatabaseModels/GlobalConfig.js +117 -0
  50. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  51. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  52. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  53. package/build/dist/Server/API/TelemetryAPI.js +237 -16
  54. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  55. package/build/dist/Server/Infrastructure/ClickhouseConfig.js +9 -0
  56. package/build/dist/Server/Infrastructure/ClickhouseConfig.js.map +1 -1
  57. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1774000000002-MigrationName.js +35 -0
  58. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1774000000002-MigrationName.js.map +1 -0
  59. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1775766676723-MigrationName.js +52 -0
  60. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1775766676723-MigrationName.js.map +1 -0
  61. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1775900000000-AddGlobalSmtpOAuth.js +26 -0
  62. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1775900000000-AddGlobalSmtpOAuth.js.map +1 -0
  63. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  64. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  65. package/build/dist/Server/Services/DockerHostService.js +162 -0
  66. package/build/dist/Server/Services/DockerHostService.js.map +1 -0
  67. package/build/dist/Server/Services/ExceptionAggregationService.js +224 -0
  68. package/build/dist/Server/Services/ExceptionAggregationService.js.map +1 -0
  69. package/build/dist/Server/Services/Index.js +2 -0
  70. package/build/dist/Server/Services/Index.js.map +1 -1
  71. package/build/dist/Server/Services/LogAggregationService.js +11 -0
  72. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  73. package/build/dist/Server/Services/MonitorProbeService.js +28 -14
  74. package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
  75. package/build/dist/Server/Services/MonitorService.js +19 -17
  76. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  77. package/build/dist/Server/Services/TraceAggregationService.js +364 -0
  78. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -0
  79. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +46 -1
  80. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  81. package/build/dist/Tests/Server/Services/LogAggregationService.test.js +2 -2
  82. package/build/dist/Tests/Server/Services/LogAggregationService.test.js.map +1 -1
  83. package/build/dist/Types/Icon/IconProp.js +1 -0
  84. package/build/dist/Types/Icon/IconProp.js.map +1 -1
  85. package/build/dist/Types/Monitor/DockerAlertTemplates.js +410 -0
  86. package/build/dist/Types/Monitor/DockerAlertTemplates.js.map +1 -0
  87. package/build/dist/Types/Monitor/DockerMetricCatalog.js +192 -0
  88. package/build/dist/Types/Monitor/DockerMetricCatalog.js.map +1 -0
  89. package/build/dist/Types/Monitor/MonitorStep.js +23 -0
  90. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  91. package/build/dist/Types/Monitor/MonitorStepDockerMonitor.js +21 -0
  92. package/build/dist/Types/Monitor/MonitorStepDockerMonitor.js.map +1 -0
  93. package/build/dist/Types/Monitor/MonitorType.js +14 -1
  94. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  95. package/build/dist/Types/Permission.js +36 -0
  96. package/build/dist/Types/Permission.js.map +1 -1
  97. package/build/dist/UI/Components/Icon/Icon.js +13 -0
  98. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  99. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js +7 -75
  100. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js.map +1 -1
  101. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +8 -1
  102. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  103. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +71 -0
  104. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -0
  105. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js +39 -0
  106. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js.map +1 -0
  107. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js +61 -0
  108. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js.map +1 -0
  109. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js +66 -0
  110. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js.map +1 -0
  111. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +41 -0
  112. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -0
  113. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js +35 -0
  114. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js.map +1 -0
  115. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +132 -0
  116. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -0
  117. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +65 -0
  118. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -0
  119. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js +52 -0
  120. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js.map +1 -0
  121. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +224 -0
  122. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -0
  123. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchHelp.js +35 -0
  124. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchHelp.js.map +1 -0
  125. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js +27 -0
  126. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js.map +1 -0
  127. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +97 -0
  128. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -0
  129. package/build/dist/UI/Components/TelemetryViewer/types.js +6 -0
  130. package/build/dist/UI/Components/TelemetryViewer/types.js.map +1 -0
  131. package/jest.config.json +6 -1
  132. package/package.json +1 -1
@@ -0,0 +1,514 @@
1
+ import { SQL, Statement } from "../Utils/AnalyticsDatabase/Statement";
2
+ import SpanService from "./SpanService";
3
+ import TableColumnType from "../../Types/AnalyticsDatabase/TableColumnType";
4
+ import { JSONObject } from "../../Types/JSON";
5
+ import ObjectID from "../../Types/ObjectID";
6
+ import BadDataException from "../../Types/Exception/BadDataException";
7
+ import Includes from "../../Types/BaseDatabase/Includes";
8
+ import AnalyticsTableName from "../../Types/AnalyticsDatabase/AnalyticsTableName";
9
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
10
+ import { DbJSONResponse, Results } from "./AnalyticsDatabaseService";
11
+
12
+ export interface HistogramBucket {
13
+ time: string;
14
+ series: string;
15
+ count: number;
16
+ }
17
+
18
+ export interface TraceFilters {
19
+ serviceIds?: Array<ObjectID> | undefined;
20
+ statusCodes?: Array<number> | undefined;
21
+ spanKinds?: Array<string> | undefined;
22
+ spanNames?: Array<string> | undefined;
23
+ traceIds?: Array<string> | undefined;
24
+ nameSearchText?: string | undefined;
25
+ rootOnly?: boolean | undefined;
26
+ attributes?: Record<string, string> | undefined;
27
+ }
28
+
29
+ export interface HistogramRequest extends TraceFilters {
30
+ projectId: ObjectID;
31
+ startTime: Date;
32
+ endTime: Date;
33
+ bucketSizeInMinutes: number;
34
+ }
35
+
36
+ export interface FacetValue {
37
+ value: string;
38
+ count: number;
39
+ }
40
+
41
+ export interface FacetRequest extends TraceFilters {
42
+ projectId: ObjectID;
43
+ startTime: Date;
44
+ endTime: Date;
45
+ facetKey: string;
46
+ limit?: number | undefined;
47
+ }
48
+
49
+ export interface MultiFacetRequest extends TraceFilters {
50
+ projectId: ObjectID;
51
+ startTime: Date;
52
+ endTime: Date;
53
+ facetKeys: Array<string>;
54
+ limit?: number | undefined;
55
+ sampleSize?: number | undefined;
56
+ }
57
+
58
+ export class TraceAggregationService {
59
+ private static readonly DEFAULT_FACET_LIMIT: number = 500;
60
+ private static readonly TABLE_NAME: string = AnalyticsTableName.Span;
61
+ private static readonly TOP_LEVEL_COLUMNS: Set<string> = new Set([
62
+ "serviceId",
63
+ "traceId",
64
+ "spanId",
65
+ "parentSpanId",
66
+ "name",
67
+ "kind",
68
+ "statusCode",
69
+ ]);
70
+ private static readonly ATTRIBUTE_KEY_PATTERN: RegExp = /^[a-zA-Z0-9._:/-]+$/;
71
+ private static readonly MAX_FACET_KEY_LENGTH: number = 256;
72
+
73
+ @CaptureSpan()
74
+ public static async getHistogram(
75
+ request: HistogramRequest,
76
+ ): Promise<Array<HistogramBucket>> {
77
+ const statement: Statement =
78
+ TraceAggregationService.buildHistogramStatement(request);
79
+
80
+ const dbResult: Results = await SpanService.executeQuery(statement);
81
+ const response: DbJSONResponse = await dbResult.json<{
82
+ data?: Array<JSONObject>;
83
+ }>();
84
+
85
+ const rows: Array<JSONObject> = response.data || [];
86
+
87
+ return rows.map((row: JSONObject): HistogramBucket => {
88
+ return {
89
+ time: String(row["bucket"] || ""),
90
+ series: TraceAggregationService.mapStatusCodeToSeries(
91
+ Number(row["statusCode"] || 0),
92
+ ),
93
+ count: Number(row["cnt"] || 0),
94
+ };
95
+ });
96
+ }
97
+
98
+ @CaptureSpan()
99
+ public static async getFacetValues(
100
+ request: FacetRequest,
101
+ ): Promise<Array<FacetValue>> {
102
+ const statement: Statement =
103
+ TraceAggregationService.buildFacetStatement(request);
104
+
105
+ const dbResult: Results = await SpanService.executeQuery(statement);
106
+ const response: DbJSONResponse = await dbResult.json<{
107
+ data?: Array<JSONObject>;
108
+ }>();
109
+
110
+ const rows: Array<JSONObject> = response.data || [];
111
+
112
+ return rows
113
+ .map((row: JSONObject): FacetValue => {
114
+ return {
115
+ value: String(row["val"] || ""),
116
+ count: Number(row["cnt"] || 0),
117
+ };
118
+ })
119
+ .filter((facet: FacetValue): boolean => {
120
+ return facet.value.length > 0;
121
+ });
122
+ }
123
+
124
+ /*
125
+ * Sample-based facet computation. Runs a single sort-key aligned query
126
+ * (ORDER BY startTime DESC LIMIT sampleSize) and computes top-K per facet
127
+ * in Node.js. This avoids ClickHouse GROUP BY aggregations that can't
128
+ * return partial results under max_execution_time 'break' mode, and
129
+ * leverages the (projectId, startTime, ...) primary key for efficient
130
+ * backwards scans. Facet values reflect the most recent N root spans in
131
+ * the window — the most actionable view for filtering.
132
+ */
133
+ @CaptureSpan()
134
+ public static async getFacetValuesFromSample(
135
+ request: MultiFacetRequest,
136
+ ): Promise<Record<string, Array<FacetValue>>> {
137
+ const limit: number =
138
+ request.limit ?? TraceAggregationService.DEFAULT_FACET_LIMIT;
139
+ const sampleSize: number = request.sampleSize ?? 1000;
140
+
141
+ for (const facetKey of request.facetKeys) {
142
+ TraceAggregationService.validateFacetKey(facetKey);
143
+ }
144
+
145
+ const topLevelKeys: Array<string> = request.facetKeys.filter(
146
+ (key: string): boolean => {
147
+ return TraceAggregationService.isTopLevelColumn(key);
148
+ },
149
+ );
150
+ const attributeKeys: Array<string> = request.facetKeys.filter(
151
+ (key: string): boolean => {
152
+ return !TraceAggregationService.isTopLevelColumn(key);
153
+ },
154
+ );
155
+
156
+ const selectColumns: Array<string> = [];
157
+ if (topLevelKeys.length > 0) {
158
+ selectColumns.push(...topLevelKeys);
159
+ }
160
+ if (attributeKeys.length > 0) {
161
+ selectColumns.push("attributes");
162
+ }
163
+ if (selectColumns.length === 0) {
164
+ return {};
165
+ }
166
+
167
+ /*
168
+ * Safe to interpolate: top-level column names come from a hardcoded
169
+ * allowlist (TOP_LEVEL_COLUMNS) and "attributes" is a literal. TABLE_NAME
170
+ * is also a private constant.
171
+ */
172
+ const statement: Statement = new Statement();
173
+ statement.append(
174
+ `SELECT ${selectColumns.join(", ")} FROM ${TraceAggregationService.TABLE_NAME}`,
175
+ );
176
+ statement.append(
177
+ SQL` WHERE projectId = ${{
178
+ type: TableColumnType.ObjectID,
179
+ value: request.projectId,
180
+ }} AND startTime >= ${{
181
+ type: TableColumnType.Date,
182
+ value: request.startTime,
183
+ }} AND startTime <= ${{
184
+ type: TableColumnType.Date,
185
+ value: request.endTime,
186
+ }}`,
187
+ );
188
+
189
+ TraceAggregationService.appendCommonFilters(statement, request);
190
+
191
+ statement.append(
192
+ SQL` ORDER BY startTime DESC LIMIT ${{
193
+ type: TableColumnType.Number,
194
+ value: sampleSize,
195
+ }}`,
196
+ );
197
+
198
+ /*
199
+ * Defense in depth: the sample query is sort-key aligned and should
200
+ * return in well under a second, but cap runtime below nginx's 60s
201
+ * proxy_read_timeout regardless.
202
+ */
203
+ statement.append(
204
+ " SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break'",
205
+ );
206
+
207
+ const dbResult: Results = await SpanService.executeQuery(statement);
208
+ const response: DbJSONResponse = await dbResult.json<{
209
+ data?: Array<JSONObject>;
210
+ }>();
211
+
212
+ const rows: Array<JSONObject> = response.data || [];
213
+
214
+ const counts: Record<string, Map<string, number>> = {};
215
+ for (const key of request.facetKeys) {
216
+ counts[key] = new Map<string, number>();
217
+ }
218
+
219
+ for (const row of rows) {
220
+ for (const key of topLevelKeys) {
221
+ const raw: unknown = row[key];
222
+ if (raw === undefined || raw === null) {
223
+ continue;
224
+ }
225
+ const value: string = String(raw);
226
+ if (value.length === 0) {
227
+ continue;
228
+ }
229
+ const map: Map<string, number> = counts[key]!;
230
+ map.set(value, (map.get(value) || 0) + 1);
231
+ }
232
+
233
+ if (attributeKeys.length > 0) {
234
+ const attrs: unknown = row["attributes"];
235
+ let parsed: Record<string, unknown> | null = null;
236
+ if (attrs && typeof attrs === "object") {
237
+ parsed = attrs as Record<string, unknown>;
238
+ } else if (typeof attrs === "string" && attrs.length > 0) {
239
+ try {
240
+ parsed = JSON.parse(attrs) as Record<string, unknown>;
241
+ } catch {
242
+ parsed = null;
243
+ }
244
+ }
245
+ if (parsed) {
246
+ for (const key of attributeKeys) {
247
+ const raw: unknown = parsed[key];
248
+ if (raw === undefined || raw === null) {
249
+ continue;
250
+ }
251
+ const value: string =
252
+ typeof raw === "object" ? JSON.stringify(raw) : String(raw);
253
+ if (value.length === 0) {
254
+ continue;
255
+ }
256
+ const map: Map<string, number> = counts[key]!;
257
+ map.set(value, (map.get(value) || 0) + 1);
258
+ }
259
+ }
260
+ }
261
+ }
262
+
263
+ const result: Record<string, Array<FacetValue>> = {};
264
+ for (const key of request.facetKeys) {
265
+ const entries: Array<FacetValue> = Array.from(counts[key]!.entries()).map(
266
+ ([value, count]: [string, number]): FacetValue => {
267
+ return { value, count };
268
+ },
269
+ );
270
+ entries.sort((a: FacetValue, b: FacetValue): number => {
271
+ return b.count - a.count;
272
+ });
273
+ result[key] = entries.slice(0, limit);
274
+ }
275
+
276
+ return result;
277
+ }
278
+
279
+ private static mapStatusCodeToSeries(code: number): string {
280
+ if (code === 1) {
281
+ return "ok";
282
+ }
283
+ if (code === 2) {
284
+ return "error";
285
+ }
286
+ return "unset";
287
+ }
288
+
289
+ private static buildHistogramStatement(request: HistogramRequest): Statement {
290
+ const intervalSeconds: number = request.bucketSizeInMinutes * 60;
291
+
292
+ const statement: Statement = SQL`
293
+ SELECT
294
+ toStartOfInterval(startTime, INTERVAL ${{
295
+ type: TableColumnType.Number,
296
+ value: intervalSeconds,
297
+ }} SECOND) AS bucket,
298
+ statusCode,
299
+ count() AS cnt
300
+ FROM ${TraceAggregationService.TABLE_NAME}
301
+ WHERE projectId = ${{
302
+ type: TableColumnType.ObjectID,
303
+ value: request.projectId,
304
+ }}
305
+ AND startTime >= ${{
306
+ type: TableColumnType.Date,
307
+ value: request.startTime,
308
+ }}
309
+ AND startTime <= ${{
310
+ type: TableColumnType.Date,
311
+ value: request.endTime,
312
+ }}
313
+ `;
314
+
315
+ TraceAggregationService.appendCommonFilters(statement, request);
316
+
317
+ statement.append(" GROUP BY bucket, statusCode ORDER BY bucket ASC");
318
+
319
+ /*
320
+ * Defense in depth: cap histogram runtime below nginx's 60s
321
+ * proxy_read_timeout. ClickHouse returns partial aggregated results
322
+ * with 'break' mode rather than throwing, which is acceptable for
323
+ * a density visualization.
324
+ */
325
+ statement.append(
326
+ " SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break'",
327
+ );
328
+
329
+ return statement;
330
+ }
331
+
332
+ private static buildFacetStatement(request: FacetRequest): Statement {
333
+ const limit: number =
334
+ request.limit ?? TraceAggregationService.DEFAULT_FACET_LIMIT;
335
+
336
+ TraceAggregationService.validateFacetKey(request.facetKey);
337
+
338
+ const isTopLevelColumn: boolean = TraceAggregationService.isTopLevelColumn(
339
+ request.facetKey,
340
+ );
341
+
342
+ const statement: Statement = new Statement();
343
+
344
+ if (isTopLevelColumn) {
345
+ statement.append(
346
+ SQL`SELECT toString(${request.facetKey}) AS val, count() AS cnt FROM ${TraceAggregationService.TABLE_NAME}`,
347
+ );
348
+ } else {
349
+ statement.append(
350
+ SQL`SELECT JSONExtractRaw(attributes, ${{
351
+ type: TableColumnType.Text,
352
+ value: request.facetKey,
353
+ }}) AS val, count() AS cnt FROM ${TraceAggregationService.TABLE_NAME}`,
354
+ );
355
+ }
356
+
357
+ statement.append(
358
+ SQL` WHERE projectId = ${{
359
+ type: TableColumnType.ObjectID,
360
+ value: request.projectId,
361
+ }} AND startTime >= ${{
362
+ type: TableColumnType.Date,
363
+ value: request.startTime,
364
+ }} AND startTime <= ${{
365
+ type: TableColumnType.Date,
366
+ value: request.endTime,
367
+ }}`,
368
+ );
369
+
370
+ if (!isTopLevelColumn) {
371
+ statement.append(
372
+ SQL` AND JSONHas(attributes, ${{
373
+ type: TableColumnType.Text,
374
+ value: request.facetKey,
375
+ }}) = 1`,
376
+ );
377
+ }
378
+
379
+ TraceAggregationService.appendCommonFilters(statement, request);
380
+
381
+ statement.append(
382
+ SQL` GROUP BY val ORDER BY cnt DESC LIMIT ${{
383
+ type: TableColumnType.Number,
384
+ value: limit,
385
+ }}`,
386
+ );
387
+
388
+ /*
389
+ * Defense in depth: cap individual facet query runtime below nginx's
390
+ * 60s proxy_read_timeout so a slow facet never starves the endpoint.
391
+ */
392
+ statement.append(
393
+ " SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break'",
394
+ );
395
+
396
+ return statement;
397
+ }
398
+
399
+ private static appendCommonFilters(
400
+ statement: Statement,
401
+ request: TraceFilters,
402
+ ): void {
403
+ if (request.rootOnly) {
404
+ statement.append(" AND (parentSpanId = '' OR parentSpanId IS NULL)");
405
+ }
406
+
407
+ if (request.serviceIds && request.serviceIds.length > 0) {
408
+ statement.append(
409
+ SQL` AND serviceId IN (${{
410
+ type: TableColumnType.ObjectID,
411
+ value: new Includes(
412
+ request.serviceIds.map((id: ObjectID) => {
413
+ return id.toString();
414
+ }),
415
+ ),
416
+ }})`,
417
+ );
418
+ }
419
+
420
+ if (request.statusCodes && request.statusCodes.length > 0) {
421
+ statement.append(
422
+ SQL` AND statusCode IN (${{
423
+ type: TableColumnType.Number,
424
+ value: new Includes(
425
+ request.statusCodes.map((code: number) => {
426
+ return String(code);
427
+ }),
428
+ ),
429
+ }})`,
430
+ );
431
+ }
432
+
433
+ if (request.spanKinds && request.spanKinds.length > 0) {
434
+ statement.append(
435
+ SQL` AND toString(kind) IN (${{
436
+ type: TableColumnType.Text,
437
+ value: new Includes(request.spanKinds),
438
+ }})`,
439
+ );
440
+ }
441
+
442
+ if (request.spanNames && request.spanNames.length > 0) {
443
+ statement.append(
444
+ SQL` AND name IN (${{
445
+ type: TableColumnType.Text,
446
+ value: new Includes(request.spanNames),
447
+ }})`,
448
+ );
449
+ }
450
+
451
+ if (request.traceIds && request.traceIds.length > 0) {
452
+ statement.append(
453
+ SQL` AND traceId IN (${{
454
+ type: TableColumnType.Text,
455
+ value: new Includes(request.traceIds),
456
+ }})`,
457
+ );
458
+ }
459
+
460
+ if (request.nameSearchText && request.nameSearchText.trim().length > 0) {
461
+ statement.append(
462
+ SQL` AND name ILIKE ${{
463
+ type: TableColumnType.Text,
464
+ value: `%${request.nameSearchText.trim()}%`,
465
+ }}`,
466
+ );
467
+ }
468
+
469
+ if (request.attributes && Object.keys(request.attributes).length > 0) {
470
+ for (const [attrKey, attrValue] of Object.entries(request.attributes)) {
471
+ TraceAggregationService.validateFacetKey(attrKey);
472
+
473
+ statement.append(
474
+ SQL` AND attributes[${{
475
+ type: TableColumnType.Text,
476
+ value: attrKey,
477
+ }}] = ${{
478
+ type: TableColumnType.Text,
479
+ value: attrValue,
480
+ }}`,
481
+ );
482
+ }
483
+ }
484
+ }
485
+
486
+ private static isTopLevelColumn(key: string): boolean {
487
+ return TraceAggregationService.TOP_LEVEL_COLUMNS.has(key);
488
+ }
489
+
490
+ private static validateFacetKey(
491
+ facetKey: unknown,
492
+ ): asserts facetKey is string {
493
+ if (typeof facetKey !== "string") {
494
+ throw new BadDataException("Invalid facetKey");
495
+ }
496
+
497
+ if (
498
+ facetKey.length === 0 ||
499
+ facetKey.length > TraceAggregationService.MAX_FACET_KEY_LENGTH
500
+ ) {
501
+ throw new BadDataException("Invalid facetKey");
502
+ }
503
+
504
+ if (TraceAggregationService.isTopLevelColumn(facetKey)) {
505
+ return;
506
+ }
507
+
508
+ if (!TraceAggregationService.ATTRIBUTE_KEY_PATTERN.test(facetKey)) {
509
+ throw new BadDataException("Invalid facetKey");
510
+ }
511
+ }
512
+ }
513
+
514
+ export default TraceAggregationService;
@@ -49,6 +49,7 @@ import MetricMonitorResponse, {
49
49
  KubernetesAffectedResource,
50
50
  KubernetesResourceBreakdown,
51
51
  } from "../../../Types/Monitor/MetricMonitor/MetricMonitorResponse";
52
+ import MonitorStepDockerMonitor from "../../../Types/Monitor/MonitorStepDockerMonitor";
52
53
 
53
54
  export default class MonitorCriteriaEvaluator {
54
55
  public static async processMonitorStep(input: {
@@ -459,7 +460,8 @@ ${contextBlock}
459
460
 
460
461
  if (
461
462
  input.monitor.monitorType === MonitorType.Metrics ||
462
- input.monitor.monitorType === MonitorType.Kubernetes
463
+ input.monitor.monitorType === MonitorType.Kubernetes ||
464
+ input.monitor.monitorType === MonitorType.Docker
463
465
  ) {
464
466
  const metricMonitorResult: string | null =
465
467
  await MetricMonitorCriteria.isMonitorInstanceCriteriaFilterMet({
@@ -574,6 +576,11 @@ ${contextBlock}
574
576
  );
575
577
  }
576
578
 
579
+ // Handle Docker monitors with resource context
580
+ if (input.monitor.monitorType === MonitorType.Docker) {
581
+ return MonitorCriteriaEvaluator.buildDockerRootCauseContext(input);
582
+ }
583
+
577
584
  const requestDetails: Array<string> = [];
578
585
  const responseDetails: Array<string> = [];
579
586
  const failureDetails: Array<string> = [];
@@ -922,6 +929,71 @@ ${contextBlock}
922
929
  return sections.join("\n");
923
930
  }
924
931
 
932
+ private static buildDockerRootCauseContext(input: {
933
+ dataToProcess: DataToProcess;
934
+ monitorStep: MonitorStep;
935
+ monitor: Monitor;
936
+ }): string | null {
937
+ const metricResponse: MetricMonitorResponse =
938
+ input.dataToProcess as MetricMonitorResponse;
939
+
940
+ const sections: Array<string> = [];
941
+
942
+ // Docker host context
943
+ const dockerMonitor: MonitorStepDockerMonitor | undefined =
944
+ input.monitorStep.data?.dockerMonitor;
945
+
946
+ if (dockerMonitor) {
947
+ const hostDetails: Array<string> = [];
948
+ hostDetails.push(`- Host: ${dockerMonitor.hostIdentifier || "Unknown"}`);
949
+
950
+ if (dockerMonitor.containerFilters?.containerName) {
951
+ hostDetails.push(
952
+ `- Container Name Filter: ${dockerMonitor.containerFilters.containerName}`,
953
+ );
954
+ }
955
+
956
+ if (dockerMonitor.containerFilters?.containerImage) {
957
+ hostDetails.push(
958
+ `- Container Image Filter: ${dockerMonitor.containerFilters.containerImage}`,
959
+ );
960
+ }
961
+
962
+ // Add metric name from the query config
963
+ if (
964
+ dockerMonitor.metricViewConfig?.queryConfigs?.length > 0 &&
965
+ dockerMonitor.metricViewConfig.queryConfigs[0]
966
+ ) {
967
+ const metricName: string = dockerMonitor.metricViewConfig
968
+ .queryConfigs[0].metricQueryData?.filterData?.metricName as string;
969
+ if (metricName) {
970
+ hostDetails.push(`- Metric: \`${metricName}\``);
971
+ }
972
+ }
973
+
974
+ sections.push(`**Docker Host Details**\n${hostDetails.join("\n")}`);
975
+ }
976
+
977
+ // Metric results summary
978
+ if (metricResponse.metricResult && metricResponse.metricResult.length > 0) {
979
+ const resultDetails: Array<string> = [];
980
+
981
+ for (const result of metricResponse.metricResult) {
982
+ if (result.data && result.data.length > 0) {
983
+ resultDetails.push(
984
+ `- ${result.data.length} metric data point(s) returned`,
985
+ );
986
+ }
987
+ }
988
+
989
+ if (resultDetails.length > 0) {
990
+ sections.push(`\n\n**Metric Summary**\n${resultDetails.join("\n")}`);
991
+ }
992
+ }
993
+
994
+ return sections.length > 0 ? sections.join("\n") : null;
995
+ }
996
+
925
997
  private static buildKubernetesRootCauseAnalysis(input: {
926
998
  breakdown: KubernetesResourceBreakdown;
927
999
  topResource: KubernetesAffectedResource;
@@ -31,7 +31,7 @@ describe("LogAggregationService", () => {
31
31
  });
32
32
 
33
33
  expect(statement.query).toBe(
34
- "SELECT toString({p0:Identifier}) AS val, count() AS cnt FROM {p1:Identifier} WHERE projectId = {p2:String} AND time >= {p3:DateTime} AND time <= {p4:DateTime} GROUP BY val ORDER BY cnt DESC LIMIT {p5:Int32}",
34
+ "SELECT toString({p0:Identifier}) AS val, count() AS cnt FROM {p1:Identifier} WHERE projectId = {p2:String} AND time >= {p3:DateTime} AND time <= {p4:DateTime} GROUP BY val ORDER BY cnt DESC LIMIT {p5:Int32} SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break'",
35
35
  );
36
36
 
37
37
  expect(statement.query_params).toStrictEqual({
@@ -51,7 +51,7 @@ describe("LogAggregationService", () => {
51
51
  });
52
52
 
53
53
  expect(statement.query).toBe(
54
- "SELECT JSONExtractRaw(attributes, {p0:String}) AS val, count() AS cnt FROM {p1:Identifier} WHERE projectId = {p2:String} AND time >= {p3:DateTime} AND time <= {p4:DateTime} AND JSONHas(attributes, {p5:String}) = 1 GROUP BY val ORDER BY cnt DESC LIMIT {p6:Int32}",
54
+ "SELECT JSONExtractRaw(attributes, {p0:String}) AS val, count() AS cnt FROM {p1:Identifier} WHERE projectId = {p2:String} AND time >= {p3:DateTime} AND time <= {p4:DateTime} AND JSONHas(attributes, {p5:String}) = 1 GROUP BY val ORDER BY cnt DESC LIMIT {p6:Int32} SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break'",
55
55
  );
56
56
 
57
57
  expect(statement.query_params).toStrictEqual({
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+
3
+ const mermaid = {
4
+ initialize: function () {},
5
+ render: function () {
6
+ return Promise.resolve({ svg: "" });
7
+ },
8
+ run: function () {
9
+ return Promise.resolve();
10
+ },
11
+ parse: function () {
12
+ return Promise.resolve(true);
13
+ },
14
+ contentLoaded: function () {},
15
+ };
16
+
17
+ module.exports = mermaid;
18
+ module.exports.default = mermaid;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+
3
+ const React = require("react");
4
+
5
+ function ReactMarkdown(props) {
6
+ return React.createElement(
7
+ "div",
8
+ { "data-testid": "react-markdown" },
9
+ props && props.children,
10
+ );
11
+ }
12
+
13
+ module.exports = ReactMarkdown;
14
+ module.exports.default = ReactMarkdown;
15
+ module.exports.defaultUrlTransform = function (url) {
16
+ return url;
17
+ };
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+
3
+ const React = require("react");
4
+
5
+ function SyntaxHighlighter(props) {
6
+ return React.createElement(
7
+ "pre",
8
+ { "data-testid": "syntax-highlighter" },
9
+ props && props.children,
10
+ );
11
+ }
12
+
13
+ SyntaxHighlighter.registerLanguage = function () {};
14
+
15
+ module.exports = SyntaxHighlighter;
16
+ module.exports.default = SyntaxHighlighter;
17
+ module.exports.Prism = SyntaxHighlighter;
18
+ module.exports.Light = SyntaxHighlighter;
19
+ module.exports.PrismLight = SyntaxHighlighter;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+
3
+ function remarkGfm() {
4
+ return function transformer() {};
5
+ }
6
+
7
+ module.exports = remarkGfm;
8
+ module.exports.default = remarkGfm;
@@ -321,6 +321,7 @@ enum IconProp {
321
321
  UserIcon = "UserIcon",
322
322
  XCircle = "XCircle",
323
323
  Kubernetes = "Kubernetes",
324
+ Docker = "Docker",
324
325
  Gauge = "Gauge",
325
326
  }
326
327