@oneuptime/common 10.0.20 → 10.0.22

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 (95) hide show
  1. package/Server/API/TelemetryAPI.ts +208 -0
  2. package/Server/API/UserCallAPI.ts +29 -0
  3. package/Server/API/UserEmailAPI.ts +29 -0
  4. package/Server/API/UserSmsAPI.ts +29 -0
  5. package/Server/API/UserWhatsAppAPI.ts +29 -0
  6. package/Server/Services/LogAggregationService.ts +251 -0
  7. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +2 -2
  8. package/Server/Types/Database/Permissions/TenantPermission.ts +2 -2
  9. package/Server/Utils/VM/VMRunner.ts +10 -0
  10. package/Types/Log/LogQueryParser.ts +252 -0
  11. package/Types/Log/LogQueryToFilter.ts +131 -0
  12. package/UI/Components/CopyTextButton/CopyTextButton.tsx +3 -3
  13. package/UI/Components/LogsViewer/LogsViewer.tsx +166 -93
  14. package/UI/Components/LogsViewer/components/ActiveFilterChips.tsx +58 -0
  15. package/UI/Components/LogsViewer/components/FacetSection.tsx +119 -0
  16. package/UI/Components/LogsViewer/components/FacetValueRow.tsx +102 -0
  17. package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +122 -0
  18. package/UI/Components/LogsViewer/components/LiveLogsToggle.tsx +4 -4
  19. package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +22 -26
  20. package/UI/Components/LogsViewer/components/LogSearchBar.tsx +360 -0
  21. package/UI/Components/LogsViewer/components/LogSearchHelp.tsx +128 -0
  22. package/UI/Components/LogsViewer/components/LogSearchSuggestions.tsx +64 -0
  23. package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +199 -0
  24. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +172 -0
  25. package/UI/Components/LogsViewer/components/LogsFilterCard.tsx +27 -57
  26. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +268 -0
  27. package/UI/Components/LogsViewer/components/LogsPagination.tsx +12 -10
  28. package/UI/Components/LogsViewer/components/LogsTable.tsx +33 -32
  29. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +16 -18
  30. package/UI/Components/LogsViewer/components/severityColors.ts +31 -0
  31. package/UI/Components/LogsViewer/components/severityTheme.ts +25 -25
  32. package/UI/Components/LogsViewer/types.ts +20 -0
  33. package/build/dist/Server/API/TelemetryAPI.js +136 -0
  34. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  35. package/build/dist/Server/API/UserCallAPI.js +17 -0
  36. package/build/dist/Server/API/UserCallAPI.js.map +1 -1
  37. package/build/dist/Server/API/UserEmailAPI.js +17 -0
  38. package/build/dist/Server/API/UserEmailAPI.js.map +1 -1
  39. package/build/dist/Server/API/UserSmsAPI.js +17 -0
  40. package/build/dist/Server/API/UserSmsAPI.js.map +1 -1
  41. package/build/dist/Server/API/UserWhatsAppAPI.js +17 -0
  42. package/build/dist/Server/API/UserWhatsAppAPI.js.map +1 -1
  43. package/build/dist/Server/Services/LogAggregationService.js +163 -0
  44. package/build/dist/Server/Services/LogAggregationService.js.map +1 -0
  45. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +2 -2
  46. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  47. package/build/dist/Server/Types/Database/Permissions/TenantPermission.js +2 -2
  48. package/build/dist/Server/Types/Database/Permissions/TenantPermission.js.map +1 -1
  49. package/build/dist/Server/Utils/VM/VMRunner.js +10 -0
  50. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  51. package/build/dist/Types/Log/LogQueryParser.js +200 -0
  52. package/build/dist/Types/Log/LogQueryParser.js.map +1 -0
  53. package/build/dist/Types/Log/LogQueryToFilter.js +96 -0
  54. package/build/dist/Types/Log/LogQueryToFilter.js.map +1 -0
  55. package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js +3 -3
  56. package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js.map +1 -1
  57. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +64 -42
  58. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  59. package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js +24 -0
  60. package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js.map +1 -0
  61. package/build/dist/UI/Components/LogsViewer/components/FacetSection.js +46 -0
  62. package/build/dist/UI/Components/LogsViewer/components/FacetSection.js.map +1 -0
  63. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js +35 -0
  64. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js.map +1 -0
  65. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +64 -0
  66. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -0
  67. package/build/dist/UI/Components/LogsViewer/components/LiveLogsToggle.js +4 -4
  68. package/build/dist/UI/Components/LogsViewer/components/LiveLogsToggle.js.map +1 -1
  69. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +19 -21
  70. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
  71. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js +230 -0
  72. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js.map +1 -0
  73. package/build/dist/UI/Components/LogsViewer/components/LogSearchHelp.js +84 -0
  74. package/build/dist/UI/Components/LogsViewer/components/LogSearchHelp.js.map +1 -0
  75. package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js +27 -0
  76. package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js.map +1 -0
  77. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +100 -0
  78. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -0
  79. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +104 -0
  80. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -0
  81. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js +14 -35
  82. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js.map +1 -1
  83. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +127 -0
  84. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -0
  85. package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js +9 -9
  86. package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js.map +1 -1
  87. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +31 -30
  88. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
  89. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +7 -8
  90. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  91. package/build/dist/UI/Components/LogsViewer/components/severityColors.js +22 -0
  92. package/build/dist/UI/Components/LogsViewer/components/severityColors.js.map +1 -0
  93. package/build/dist/UI/Components/LogsViewer/components/severityTheme.js +25 -25
  94. package/build/dist/UI/Components/LogsViewer/components/severityTheme.js.map +1 -1
  95. package/package.json +1 -1
@@ -11,6 +11,15 @@ import CommonAPI from "./CommonAPI";
11
11
  import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
12
12
  import TelemetryType from "../../Types/Telemetry/TelemetryType";
13
13
  import TelemetryAttributeService from "../Services/TelemetryAttributeService";
14
+ import LogAggregationService, {
15
+ HistogramBucket,
16
+ HistogramRequest,
17
+ FacetValue,
18
+ FacetRequest,
19
+ } from "../Services/LogAggregationService";
20
+ import ObjectID from "../../Types/ObjectID";
21
+ import OneUptimeDate from "../../Types/Date";
22
+ import { JSONObject } from "../../Types/JSON";
14
23
 
15
24
  const router: ExpressRouter = Express.getRouter();
16
25
 
@@ -85,4 +94,203 @@ const getAttributes: GetAttributesFunction = async (
85
94
  }
86
95
  };
87
96
 
97
+ // --- Log Histogram Endpoint ---
98
+
99
+ router.post(
100
+ "/telemetry/logs/histogram",
101
+ UserMiddleware.getUserMiddleware,
102
+ async (
103
+ req: ExpressRequest,
104
+ res: ExpressResponse,
105
+ next: NextFunction,
106
+ ): Promise<void> => {
107
+ try {
108
+ const databaseProps: DatabaseCommonInteractionProps =
109
+ await CommonAPI.getDatabaseCommonInteractionProps(req);
110
+
111
+ if (!databaseProps?.tenantId) {
112
+ return Response.sendErrorResponse(
113
+ req,
114
+ res,
115
+ new BadDataException("Invalid Project ID"),
116
+ );
117
+ }
118
+
119
+ const body: JSONObject = req.body as JSONObject;
120
+
121
+ const startTime: Date = body["startTime"]
122
+ ? OneUptimeDate.fromString(body["startTime"] as string)
123
+ : OneUptimeDate.addRemoveHours(OneUptimeDate.getCurrentDate(), -1);
124
+
125
+ const endTime: Date = body["endTime"]
126
+ ? OneUptimeDate.fromString(body["endTime"] as string)
127
+ : OneUptimeDate.getCurrentDate();
128
+
129
+ const bucketSizeInMinutes: number =
130
+ (body["bucketSizeInMinutes"] as number) ||
131
+ computeDefaultBucketSize(startTime, endTime);
132
+
133
+ const serviceIds: Array<ObjectID> | undefined = body["serviceIds"]
134
+ ? (body["serviceIds"] as Array<string>).map((id: string) => {
135
+ return new ObjectID(id);
136
+ })
137
+ : undefined;
138
+
139
+ const severityTexts: Array<string> | undefined = body["severityTexts"]
140
+ ? (body["severityTexts"] as Array<string>)
141
+ : undefined;
142
+
143
+ const bodySearchText: string | undefined = body["bodySearchText"]
144
+ ? (body["bodySearchText"] as string)
145
+ : undefined;
146
+
147
+ const traceIds: Array<string> | undefined = body["traceIds"]
148
+ ? (body["traceIds"] as Array<string>)
149
+ : undefined;
150
+
151
+ const spanIds: Array<string> | undefined = body["spanIds"]
152
+ ? (body["spanIds"] as Array<string>)
153
+ : undefined;
154
+
155
+ const request: HistogramRequest = {
156
+ projectId: databaseProps.tenantId,
157
+ startTime,
158
+ endTime,
159
+ bucketSizeInMinutes,
160
+ serviceIds,
161
+ severityTexts,
162
+ bodySearchText,
163
+ traceIds,
164
+ spanIds,
165
+ };
166
+
167
+ const buckets: Array<HistogramBucket> =
168
+ await LogAggregationService.getHistogram(request);
169
+
170
+ return Response.sendJsonObjectResponse(req, res, {
171
+ buckets: buckets as unknown as JSONObject,
172
+ });
173
+ } catch (err: unknown) {
174
+ next(err);
175
+ }
176
+ },
177
+ );
178
+
179
+ // --- Log Facets Endpoint ---
180
+
181
+ router.post(
182
+ "/telemetry/logs/facets",
183
+ UserMiddleware.getUserMiddleware,
184
+ async (
185
+ req: ExpressRequest,
186
+ res: ExpressResponse,
187
+ next: NextFunction,
188
+ ): Promise<void> => {
189
+ try {
190
+ const databaseProps: DatabaseCommonInteractionProps =
191
+ await CommonAPI.getDatabaseCommonInteractionProps(req);
192
+
193
+ if (!databaseProps?.tenantId) {
194
+ return Response.sendErrorResponse(
195
+ req,
196
+ res,
197
+ new BadDataException("Invalid Project ID"),
198
+ );
199
+ }
200
+
201
+ const body: JSONObject = req.body as JSONObject;
202
+
203
+ const facetKeys: Array<string> = body["facetKeys"]
204
+ ? (body["facetKeys"] as Array<string>)
205
+ : ["severityText", "serviceId"];
206
+
207
+ const startTime: Date = body["startTime"]
208
+ ? OneUptimeDate.fromString(body["startTime"] as string)
209
+ : OneUptimeDate.addRemoveHours(OneUptimeDate.getCurrentDate(), -1);
210
+
211
+ const endTime: Date = body["endTime"]
212
+ ? OneUptimeDate.fromString(body["endTime"] as string)
213
+ : OneUptimeDate.getCurrentDate();
214
+
215
+ const limit: number = (body["limit"] as number) || 10;
216
+
217
+ const serviceIds: Array<ObjectID> | undefined = body["serviceIds"]
218
+ ? (body["serviceIds"] as Array<string>).map((id: string) => {
219
+ return new ObjectID(id);
220
+ })
221
+ : undefined;
222
+
223
+ const severityTexts: Array<string> | undefined = body["severityTexts"]
224
+ ? (body["severityTexts"] as Array<string>)
225
+ : undefined;
226
+
227
+ const bodySearchText: string | undefined = body["bodySearchText"]
228
+ ? (body["bodySearchText"] as string)
229
+ : undefined;
230
+
231
+ const traceIds: Array<string> | undefined = body["traceIds"]
232
+ ? (body["traceIds"] as Array<string>)
233
+ : undefined;
234
+
235
+ const spanIds: Array<string> | undefined = body["spanIds"]
236
+ ? (body["spanIds"] as Array<string>)
237
+ : undefined;
238
+
239
+ const facets: Record<string, Array<FacetValue>> = {};
240
+
241
+ for (const facetKey of facetKeys) {
242
+ const request: FacetRequest = {
243
+ projectId: databaseProps.tenantId,
244
+ startTime,
245
+ endTime,
246
+ facetKey,
247
+ limit,
248
+ serviceIds,
249
+ severityTexts,
250
+ bodySearchText,
251
+ traceIds,
252
+ spanIds,
253
+ };
254
+
255
+ facets[facetKey] = await LogAggregationService.getFacetValues(request);
256
+ }
257
+
258
+ return Response.sendJsonObjectResponse(req, res, {
259
+ facets: facets as unknown as JSONObject,
260
+ });
261
+ } catch (err: unknown) {
262
+ next(err);
263
+ }
264
+ },
265
+ );
266
+
267
+ // --- Helpers ---
268
+
269
+ function computeDefaultBucketSize(startTime: Date, endTime: Date): number {
270
+ const diffMs: number = endTime.getTime() - startTime.getTime();
271
+ const diffMinutes: number = diffMs / (1000 * 60);
272
+
273
+ if (diffMinutes <= 60) {
274
+ return 1;
275
+ }
276
+
277
+ if (diffMinutes <= 360) {
278
+ return 5;
279
+ }
280
+
281
+ if (diffMinutes <= 1440) {
282
+ return 15;
283
+ }
284
+
285
+ if (diffMinutes <= 10080) {
286
+ return 60;
287
+ }
288
+
289
+ if (diffMinutes <= 43200) {
290
+ return 360;
291
+ }
292
+
293
+ return 1440;
294
+ }
295
+
88
296
  export default router;
@@ -136,6 +136,35 @@ export default class UserCallAPI extends BaseAPI<
136
136
  );
137
137
  }
138
138
 
139
+ const item: UserCall | null = await this.service.findOneById({
140
+ id: req.body["itemId"],
141
+ props: {
142
+ isRoot: true,
143
+ },
144
+ select: {
145
+ userId: true,
146
+ },
147
+ });
148
+
149
+ if (!item) {
150
+ return Response.sendErrorResponse(
151
+ req,
152
+ res,
153
+ new BadDataException("Item not found"),
154
+ );
155
+ }
156
+
157
+ if (
158
+ item.userId?.toString() !==
159
+ (req as OneUptimeRequest)?.userAuthorization?.userId?.toString()
160
+ ) {
161
+ return Response.sendErrorResponse(
162
+ req,
163
+ res,
164
+ new BadDataException("Invalid user ID"),
165
+ );
166
+ }
167
+
139
168
  await this.service.resendVerificationCode(req.body.itemId);
140
169
 
141
170
  return Response.sendEmptySuccessResponse(req, res);
@@ -137,6 +137,35 @@ export default class UserEmailAPI extends BaseAPI<
137
137
  );
138
138
  }
139
139
 
140
+ const item: UserEmail | null = await this.service.findOneById({
141
+ id: req.body["itemId"],
142
+ props: {
143
+ isRoot: true,
144
+ },
145
+ select: {
146
+ userId: true,
147
+ },
148
+ });
149
+
150
+ if (!item) {
151
+ return Response.sendErrorResponse(
152
+ req,
153
+ res,
154
+ new BadDataException("Item not found"),
155
+ );
156
+ }
157
+
158
+ if (
159
+ item.userId?.toString() !==
160
+ (req as OneUptimeRequest)?.userAuthorization?.userId?.toString()
161
+ ) {
162
+ return Response.sendErrorResponse(
163
+ req,
164
+ res,
165
+ new BadDataException("Invalid user ID"),
166
+ );
167
+ }
168
+
140
169
  await this.service.resendVerificationCode(req.body.itemId);
141
170
 
142
171
  return Response.sendEmptySuccessResponse(req, res);
@@ -132,6 +132,35 @@ export default class UserSMSAPI extends BaseAPI<UserSMS, UserSMSServiceType> {
132
132
  );
133
133
  }
134
134
 
135
+ const item: UserSMS | null = await this.service.findOneById({
136
+ id: req.body["itemId"],
137
+ props: {
138
+ isRoot: true,
139
+ },
140
+ select: {
141
+ userId: true,
142
+ },
143
+ });
144
+
145
+ if (!item) {
146
+ return Response.sendErrorResponse(
147
+ req,
148
+ res,
149
+ new BadDataException("Item not found"),
150
+ );
151
+ }
152
+
153
+ if (
154
+ item.userId?.toString() !==
155
+ (req as OneUptimeRequest)?.userAuthorization?.userId?.toString()
156
+ ) {
157
+ return Response.sendErrorResponse(
158
+ req,
159
+ res,
160
+ new BadDataException("Invalid user ID"),
161
+ );
162
+ }
163
+
135
164
  await this.service.resendVerificationCode(req.body.itemId);
136
165
 
137
166
  return Response.sendEmptySuccessResponse(req, res);
@@ -143,6 +143,35 @@ export default class UserWhatsAppAPI extends BaseAPI<
143
143
  );
144
144
  }
145
145
 
146
+ const item: UserWhatsApp | null = await this.service.findOneById({
147
+ id: req.body["itemId"],
148
+ props: {
149
+ isRoot: true,
150
+ },
151
+ select: {
152
+ userId: true,
153
+ },
154
+ });
155
+
156
+ if (!item) {
157
+ return Response.sendErrorResponse(
158
+ req,
159
+ res,
160
+ new BadDataException("Item not found"),
161
+ );
162
+ }
163
+
164
+ if (
165
+ item.userId?.toString() !==
166
+ (req as OneUptimeRequest)?.userAuthorization?.userId?.toString()
167
+ ) {
168
+ return Response.sendErrorResponse(
169
+ req,
170
+ res,
171
+ new BadDataException("Invalid user ID"),
172
+ );
173
+ }
174
+
146
175
  await this.service.resendVerificationCode(req.body.itemId);
147
176
 
148
177
  return Response.sendEmptySuccessResponse(req, res);
@@ -0,0 +1,251 @@
1
+ import { SQL, Statement } from "../Utils/AnalyticsDatabase/Statement";
2
+ import LogDatabaseService from "./LogService";
3
+ import TableColumnType from "../../Types/AnalyticsDatabase/TableColumnType";
4
+ import { JSONObject } from "../../Types/JSON";
5
+ import ObjectID from "../../Types/ObjectID";
6
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
7
+ import { DbJSONResponse, Results } from "./AnalyticsDatabaseService";
8
+
9
+ export interface HistogramBucket {
10
+ time: string;
11
+ severity: string;
12
+ count: number;
13
+ }
14
+
15
+ export interface HistogramRequest {
16
+ projectId: ObjectID;
17
+ startTime: Date;
18
+ endTime: Date;
19
+ bucketSizeInMinutes: number;
20
+ serviceIds?: Array<ObjectID> | undefined;
21
+ severityTexts?: Array<string> | undefined;
22
+ bodySearchText?: string | undefined;
23
+ traceIds?: Array<string> | undefined;
24
+ spanIds?: Array<string> | undefined;
25
+ }
26
+
27
+ export interface FacetValue {
28
+ value: string;
29
+ count: number;
30
+ }
31
+
32
+ export interface FacetRequest {
33
+ projectId: ObjectID;
34
+ startTime: Date;
35
+ endTime: Date;
36
+ facetKey: string;
37
+ limit?: number | undefined;
38
+ serviceIds?: Array<ObjectID> | undefined;
39
+ severityTexts?: Array<string> | undefined;
40
+ bodySearchText?: string | undefined;
41
+ traceIds?: Array<string> | undefined;
42
+ spanIds?: Array<string> | undefined;
43
+ }
44
+
45
+ export class LogAggregationService {
46
+ private static readonly DEFAULT_FACET_LIMIT: number = 10;
47
+ private static readonly TABLE_NAME: string = "LogItem";
48
+
49
+ @CaptureSpan()
50
+ public static async getHistogram(
51
+ request: HistogramRequest,
52
+ ): Promise<Array<HistogramBucket>> {
53
+ const statement: Statement =
54
+ LogAggregationService.buildHistogramStatement(request);
55
+
56
+ const dbResult: Results = await LogDatabaseService.executeQuery(statement);
57
+ const response: DbJSONResponse = await dbResult.json<{
58
+ data?: Array<JSONObject>;
59
+ }>();
60
+
61
+ const rows: Array<JSONObject> = response.data || [];
62
+
63
+ return rows.map((row: JSONObject): HistogramBucket => {
64
+ return {
65
+ time: String(row["bucket"] || ""),
66
+ severity: String(row["severityText"] || "Unspecified"),
67
+ count: Number(row["cnt"] || 0),
68
+ };
69
+ });
70
+ }
71
+
72
+ @CaptureSpan()
73
+ public static async getFacetValues(
74
+ request: FacetRequest,
75
+ ): Promise<Array<FacetValue>> {
76
+ const statement: Statement =
77
+ LogAggregationService.buildFacetStatement(request);
78
+
79
+ const dbResult: Results = await LogDatabaseService.executeQuery(statement);
80
+ const response: DbJSONResponse = await dbResult.json<{
81
+ data?: Array<JSONObject>;
82
+ }>();
83
+
84
+ const rows: Array<JSONObject> = response.data || [];
85
+
86
+ return rows
87
+ .map((row: JSONObject): FacetValue => {
88
+ return {
89
+ value: String(row["val"] || ""),
90
+ count: Number(row["cnt"] || 0),
91
+ };
92
+ })
93
+ .filter((facet: FacetValue): boolean => {
94
+ return facet.value.length > 0;
95
+ });
96
+ }
97
+
98
+ private static buildHistogramStatement(request: HistogramRequest): Statement {
99
+ const intervalSeconds: number = request.bucketSizeInMinutes * 60;
100
+
101
+ const statement: Statement = SQL`
102
+ SELECT
103
+ toStartOfInterval(time, INTERVAL ${{
104
+ type: TableColumnType.Number,
105
+ value: intervalSeconds,
106
+ }} SECOND) AS bucket,
107
+ severityText,
108
+ count() AS cnt
109
+ FROM ${LogAggregationService.TABLE_NAME}
110
+ WHERE projectId = ${{
111
+ type: TableColumnType.ObjectID,
112
+ value: request.projectId,
113
+ }}
114
+ AND time >= ${{
115
+ type: TableColumnType.Date,
116
+ value: request.startTime,
117
+ }}
118
+ AND time <= ${{
119
+ type: TableColumnType.Date,
120
+ value: request.endTime,
121
+ }}
122
+ `;
123
+
124
+ LogAggregationService.appendCommonFilters(statement, request);
125
+
126
+ statement.append(" GROUP BY bucket, severityText ORDER BY bucket ASC");
127
+
128
+ return statement;
129
+ }
130
+
131
+ private static buildFacetStatement(request: FacetRequest): Statement {
132
+ const limit: number =
133
+ request.limit ?? LogAggregationService.DEFAULT_FACET_LIMIT;
134
+
135
+ const isTopLevelColumn: boolean = LogAggregationService.isTopLevelColumn(
136
+ request.facetKey,
137
+ );
138
+
139
+ const escapedKey: string = LogAggregationService.escapeSingleQuotes(
140
+ request.facetKey,
141
+ );
142
+
143
+ const valueExpression: string = isTopLevelColumn
144
+ ? `toString(${escapedKey})`
145
+ : `JSONExtractRaw(attributes, '${escapedKey}')`;
146
+
147
+ // Build with raw SQL for the expression part, then parameterize WHERE values
148
+ const statement: Statement = new Statement();
149
+
150
+ statement.append(
151
+ `SELECT ${valueExpression} AS val, count() AS cnt FROM ${LogAggregationService.TABLE_NAME}`,
152
+ );
153
+
154
+ statement.append(
155
+ SQL` WHERE projectId = ${{
156
+ type: TableColumnType.ObjectID,
157
+ value: request.projectId,
158
+ }} AND time >= ${{
159
+ type: TableColumnType.Date,
160
+ value: request.startTime,
161
+ }} AND time <= ${{
162
+ type: TableColumnType.Date,
163
+ value: request.endTime,
164
+ }}`,
165
+ );
166
+
167
+ if (!isTopLevelColumn) {
168
+ statement.append(` AND JSONHas(attributes, '${escapedKey}') = 1`);
169
+ }
170
+
171
+ LogAggregationService.appendCommonFilters(statement, request);
172
+
173
+ statement.append(
174
+ SQL` GROUP BY val ORDER BY cnt DESC LIMIT ${{
175
+ type: TableColumnType.Number,
176
+ value: limit,
177
+ }}`,
178
+ );
179
+
180
+ return statement;
181
+ }
182
+
183
+ private static appendCommonFilters(
184
+ statement: Statement,
185
+ request: Pick<
186
+ HistogramRequest,
187
+ "serviceIds" | "severityTexts" | "bodySearchText" | "traceIds" | "spanIds"
188
+ >,
189
+ ): void {
190
+ if (request.serviceIds && request.serviceIds.length > 0) {
191
+ const idStrings: Array<string> = request.serviceIds.map(
192
+ (id: ObjectID): string => {
193
+ return `'${id.toString()}'`;
194
+ },
195
+ );
196
+ statement.append(` AND serviceId IN (${idStrings.join(",")})`);
197
+ }
198
+
199
+ if (request.severityTexts && request.severityTexts.length > 0) {
200
+ const sevStrings: Array<string> = request.severityTexts.map(
201
+ (s: string): string => {
202
+ return `'${LogAggregationService.escapeSingleQuotes(s)}'`;
203
+ },
204
+ );
205
+ statement.append(` AND severityText IN (${sevStrings.join(",")})`);
206
+ }
207
+
208
+ if (request.traceIds && request.traceIds.length > 0) {
209
+ const traceStrings: Array<string> = request.traceIds.map(
210
+ (s: string): string => {
211
+ return `'${LogAggregationService.escapeSingleQuotes(s)}'`;
212
+ },
213
+ );
214
+ statement.append(` AND traceId IN (${traceStrings.join(",")})`);
215
+ }
216
+
217
+ if (request.spanIds && request.spanIds.length > 0) {
218
+ const spanStrings: Array<string> = request.spanIds.map(
219
+ (s: string): string => {
220
+ return `'${LogAggregationService.escapeSingleQuotes(s)}'`;
221
+ },
222
+ );
223
+ statement.append(` AND spanId IN (${spanStrings.join(",")})`);
224
+ }
225
+
226
+ if (request.bodySearchText && request.bodySearchText.trim().length > 0) {
227
+ statement.append(
228
+ ` AND body ILIKE ${{
229
+ type: TableColumnType.Text,
230
+ value: `%${request.bodySearchText.trim()}%`,
231
+ }}`,
232
+ );
233
+ }
234
+ }
235
+
236
+ private static isTopLevelColumn(key: string): boolean {
237
+ const topLevelColumns: Set<string> = new Set([
238
+ "severityText",
239
+ "serviceId",
240
+ "traceId",
241
+ "spanId",
242
+ ]);
243
+ return topLevelColumns.has(key);
244
+ }
245
+
246
+ private static escapeSingleQuotes(value: string): string {
247
+ return value.replace(/'/g, "\\'");
248
+ }
249
+ }
250
+
251
+ export default LogAggregationService;
@@ -392,8 +392,8 @@ export default class ModelPermission {
392
392
  (query as any)[tenantColumn] = props.tenantId;
393
393
  } else if (
394
394
  tenantColumn &&
395
- !props.tenantId &&
396
- props.userGlobalAccessPermission
395
+ props.userGlobalAccessPermission &&
396
+ (!props.tenantId || props.isMultiTenantRequest)
397
397
  ) {
398
398
  /*
399
399
  * for each of these projectIds,
@@ -42,8 +42,8 @@ export default class TenantPermission {
42
42
  (query as any)[model.getUserColumn() as string] = props.userId;
43
43
  } else if (
44
44
  tenantColumn &&
45
- !props.tenantId &&
46
- props.userGlobalAccessPermission
45
+ props.userGlobalAccessPermission &&
46
+ (!props.tenantId || props.isMultiTenantRequest)
47
47
  ) {
48
48
  /*
49
49
  * for each of these projectIds,
@@ -22,6 +22,16 @@ const BLOCKED_SANDBOX_PROPERTIES: ReadonlySet<string> = new Set([
22
22
  "__proto__",
23
23
  "prototype",
24
24
  "mainModule",
25
+ /*
26
+ * Block Playwright methods that can spawn processes or access internals.
27
+ * Prevents RCE via browser.browserType().launch({executablePath:"/bin/sh"})
28
+ * and traversal via page.context().browser().browserType().launch(...)
29
+ */
30
+ "browserType", // Browser → BrowserType (which has launch/connect)
31
+ "launch", // BrowserType.launch() spawns a child process
32
+ "launchPersistentContext", // BrowserType.launchPersistentContext() spawns a child process
33
+ "connectOverCDP", // BrowserType.connectOverCDP() connects via Chrome DevTools Protocol
34
+ "newCDPSession", // BrowserContext/Page.newCDPSession() opens raw CDP sessions
25
35
  ]);
26
36
 
27
37
  /**