@oneuptime/common 10.0.86 → 10.0.89
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.
- package/Models/DatabaseModels/EnterpriseLicense.ts +54 -0
- package/Models/DatabaseModels/GlobalConfig.ts +51 -0
- package/Server/API/EnterpriseLicenseAPI.ts +83 -0
- package/Server/API/GlobalConfigAPI.ts +59 -0
- package/Server/API/MetricAPI.ts +149 -0
- package/Server/API/TelemetryAPI.ts +24 -0
- package/Server/EnvironmentConfig.ts +10 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.ts +59 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Infrastructure/Queue.ts +4 -4
- package/Server/Services/AnalyticsDatabaseService.ts +21 -0
- package/Server/Services/MetricService.ts +193 -1
- package/Server/Services/TelemetryAttributeService.ts +37 -3
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +174 -7
- package/Tests/Types/Date.test.ts +46 -0
- package/Types/Dashboard/DashboardComponentType.ts +3 -0
- package/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.ts +13 -0
- package/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.ts +13 -0
- package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +13 -0
- package/Types/Date.ts +9 -4
- package/Types/JSONFunctions.ts +61 -1
- package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +60 -21
- package/UI/Components/Dictionary/Dictionary.tsx +188 -26
- package/UI/Components/Dictionary/DictionaryFilterOperator.ts +357 -0
- package/UI/Components/Dictionary/DictionaryOfStrings.tsx +12 -7
- package/UI/Components/EditionLabel/EditionLabel.tsx +224 -10
- package/UI/Components/Filters/FilterViewer.tsx +81 -16
- package/UI/Components/Filters/FiltersForm.tsx +18 -3
- package/UI/Components/Filters/JSONFilter.tsx +11 -2
- package/UI/Components/Filters/Types/Filter.ts +3 -0
- package/UI/Components/Forms/Fields/FormField.tsx +6 -1
- package/UI/Components/Forms/Types/Field.ts +5 -0
- package/UI/Components/LogsViewer/LogsViewer.tsx +73 -4
- package/UI/Components/LogsViewer/components/LogSearchBar.tsx +77 -31
- package/UI/Components/LogsViewer/components/LogSearchSuggestions.tsx +44 -1
- package/UI/Components/LogsViewer/components/LogsFilterCard.tsx +7 -5
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +6 -0
- package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +84 -25
- package/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.tsx +44 -1
- package/Utils/Dashboard/Components/DashboardAlertListComponent.ts +86 -0
- package/Utils/Dashboard/Components/DashboardIncidentListComponent.ts +86 -0
- package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +85 -0
- package/Utils/Dashboard/Components/Index.ts +21 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +57 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +54 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Server/API/EnterpriseLicenseAPI.js +64 -1
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
- package/build/dist/Server/API/GlobalConfigAPI.js +47 -0
- package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
- package/build/dist/Server/API/MetricAPI.js +123 -0
- package/build/dist/Server/API/MetricAPI.js.map +1 -0
- package/build/dist/Server/API/TelemetryAPI.js +9 -0
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +3 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Queue.js +3 -3
- package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +18 -0
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/MetricService.js +151 -1
- package/build/dist/Server/Services/MetricService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryAttributeService.js +36 -7
- package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +135 -5
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Tests/Types/Date.test.js +40 -0
- package/build/dist/Tests/Types/Date.test.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponentType.js +3 -0
- package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.js.map +1 -0
- package/build/dist/Types/Date.js +7 -2
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/JSONFunctions.js +47 -1
- package/build/dist/Types/JSONFunctions.js.map +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +21 -10
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/Dictionary.js +109 -16
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js +263 -0
- package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js.map +1 -0
- package/build/dist/UI/Components/Dictionary/DictionaryOfStrings.js +10 -6
- package/build/dist/UI/Components/Dictionary/DictionaryOfStrings.js.map +1 -1
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js +124 -6
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
- package/build/dist/UI/Components/Filters/FilterViewer.js +50 -12
- package/build/dist/UI/Components/Filters/FilterViewer.js.map +1 -1
- package/build/dist/UI/Components/Filters/FiltersForm.js +5 -4
- package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
- package/build/dist/UI/Components/Filters/JSONFilter.js +1 -1
- package/build/dist/UI/Components/Filters/JSONFilter.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +54 -5
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js +59 -29
- package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js +10 -2
- package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js +2 -5
- package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +59 -22
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js +10 -2
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js.map +1 -1
- package/build/dist/Utils/Dashboard/Components/DashboardAlertListComponent.js +70 -0
- package/build/dist/Utils/Dashboard/Components/DashboardAlertListComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardIncidentListComponent.js +70 -0
- package/build/dist/Utils/Dashboard/Components/DashboardIncidentListComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +69 -0
- package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/Index.js +12 -0
- package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,10 +5,11 @@ import AggregateBy, {
|
|
|
5
5
|
AggregateUtil,
|
|
6
6
|
} from "../Types/AnalyticsDatabase/AggregateBy";
|
|
7
7
|
import { SQL, Statement } from "../Utils/AnalyticsDatabase/Statement";
|
|
8
|
-
import {
|
|
8
|
+
import AggregationType, {
|
|
9
9
|
getPercentileLevel,
|
|
10
10
|
isPercentileAggregation,
|
|
11
11
|
} from "../../Types/BaseDatabase/AggregationType";
|
|
12
|
+
import AggregationInterval from "../../Types/BaseDatabase/AggregationInterval";
|
|
12
13
|
import TableColumnType from "../../Types/AnalyticsDatabase/TableColumnType";
|
|
13
14
|
import logger, { LogAttributes } from "../Utils/Logger";
|
|
14
15
|
|
|
@@ -56,6 +57,13 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
56
57
|
columns: Array<string>;
|
|
57
58
|
} {
|
|
58
59
|
if (!isPercentileAggregation(aggregateBy.aggregationType)) {
|
|
60
|
+
const mvStatement: {
|
|
61
|
+
statement: Statement;
|
|
62
|
+
columns: Array<string>;
|
|
63
|
+
} | null = this.tryBuildMinuteAggregateMVStatement(aggregateBy);
|
|
64
|
+
if (mvStatement) {
|
|
65
|
+
return mvStatement;
|
|
66
|
+
}
|
|
59
67
|
return super.toAggregateStatement(aggregateBy);
|
|
60
68
|
}
|
|
61
69
|
|
|
@@ -216,6 +224,16 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
216
224
|
}} `,
|
|
217
225
|
);
|
|
218
226
|
|
|
227
|
+
/*
|
|
228
|
+
* Match the read-path settings the base aggregator now appends (see
|
|
229
|
+
* AnalyticsDatabaseService.toAggregateStatement). The percentile
|
|
230
|
+
* path bypasses the base method, so we mirror them here to keep
|
|
231
|
+
* cluster behavior consistent across aggregation kinds.
|
|
232
|
+
*/
|
|
233
|
+
statement.append(
|
|
234
|
+
` SETTINGS optimize_aggregation_in_order=1, optimize_move_to_prewhere=1, max_threads=4`,
|
|
235
|
+
);
|
|
236
|
+
|
|
219
237
|
const columns: Array<string> = [
|
|
220
238
|
aggregationColumn,
|
|
221
239
|
aggregationTimestampColumn,
|
|
@@ -231,6 +249,180 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
231
249
|
|
|
232
250
|
return { statement, columns };
|
|
233
251
|
}
|
|
252
|
+
|
|
253
|
+
/*
|
|
254
|
+
* Materialized-view fast path for scalar aggregations.
|
|
255
|
+
*
|
|
256
|
+
* Returns a statement that reads from MetricItemAggMV1m (the
|
|
257
|
+
* 1-minute pre-aggregate created by
|
|
258
|
+
* AddMetricMinuteAggregateMaterializedView) when:
|
|
259
|
+
*
|
|
260
|
+
* - The aggregation is Sum/Avg/Min/Max/Count over `value`.
|
|
261
|
+
* - The dashboard's effective bucket interval is >= 1 minute (the
|
|
262
|
+
* MV stores 1-minute states; sub-minute requests need raw rows).
|
|
263
|
+
* - The query carries no per-attribute filter or group-by, since
|
|
264
|
+
* the MV is keyed by (projectId, name, serviceId, bucketTime)
|
|
265
|
+
* only — it does not preserve attribute breakdowns.
|
|
266
|
+
* - The query carries no group-by other than the time bucket.
|
|
267
|
+
*
|
|
268
|
+
* Returns `null` if any condition fails so the caller falls back to
|
|
269
|
+
* the base table. The result row shape (columns: aggregateColumn,
|
|
270
|
+
* timestampColumn) matches the base statement so downstream code
|
|
271
|
+
* needs no changes.
|
|
272
|
+
*/
|
|
273
|
+
private tryBuildMinuteAggregateMVStatement(
|
|
274
|
+
aggregateBy: AggregateBy<Metric>,
|
|
275
|
+
): { statement: Statement; columns: Array<string> } | null {
|
|
276
|
+
const aggType: AggregationType = aggregateBy.aggregationType;
|
|
277
|
+
const supported: ReadonlyArray<AggregationType> = [
|
|
278
|
+
AggregationType.Sum,
|
|
279
|
+
AggregationType.Avg,
|
|
280
|
+
AggregationType.Min,
|
|
281
|
+
AggregationType.Max,
|
|
282
|
+
AggregationType.Count,
|
|
283
|
+
];
|
|
284
|
+
if (!supported.includes(aggType)) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (
|
|
289
|
+
aggregateBy.aggregateColumnName.toString() !== "value" ||
|
|
290
|
+
aggregateBy.aggregationTimestampColumnName.toString() !== "time"
|
|
291
|
+
) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const interval: AggregationInterval = AggregateUtil.getAggregationInterval({
|
|
296
|
+
startDate: aggregateBy.startTimestamp!,
|
|
297
|
+
endDate: aggregateBy.endTimestamp!,
|
|
298
|
+
});
|
|
299
|
+
/*
|
|
300
|
+
* The MV is bucketed at 1 minute, so all values of AggregationInterval
|
|
301
|
+
* (Minute / Hour / Day / Week / Month / Year) are >= MV resolution
|
|
302
|
+
* and acceptable. Kept as a no-op read so the dependency on
|
|
303
|
+
* AggregateUtil makes the intent obvious.
|
|
304
|
+
*/
|
|
305
|
+
void interval;
|
|
306
|
+
|
|
307
|
+
const queryRecord: Record<string, unknown> =
|
|
308
|
+
(aggregateBy.query as unknown as Record<string, unknown>) || {};
|
|
309
|
+
const attrs: unknown = queryRecord["attributes"];
|
|
310
|
+
if (
|
|
311
|
+
attrs !== undefined &&
|
|
312
|
+
attrs !== null &&
|
|
313
|
+
!(
|
|
314
|
+
typeof attrs === "object" &&
|
|
315
|
+
Object.keys(attrs as Record<string, unknown>).length === 0
|
|
316
|
+
)
|
|
317
|
+
) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (aggregateBy.groupBy && Object.keys(aggregateBy.groupBy).length > 0) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!this.database) {
|
|
326
|
+
this.useDefaultDatabase();
|
|
327
|
+
}
|
|
328
|
+
const databaseName: string = this.database.getDatasourceOptions().database!;
|
|
329
|
+
|
|
330
|
+
const intervalLower: string = interval.toLowerCase();
|
|
331
|
+
|
|
332
|
+
let mergedExpr: string;
|
|
333
|
+
if (aggType === AggregationType.Sum) {
|
|
334
|
+
mergedExpr = `sumMerge(valueSumState)`;
|
|
335
|
+
} else if (aggType === AggregationType.Count) {
|
|
336
|
+
mergedExpr = `countMerge(valueCountState)`;
|
|
337
|
+
} else if (aggType === AggregationType.Min) {
|
|
338
|
+
mergedExpr = `minMerge(valueMinState)`;
|
|
339
|
+
} else if (aggType === AggregationType.Max) {
|
|
340
|
+
mergedExpr = `maxMerge(valueMaxState)`;
|
|
341
|
+
} else {
|
|
342
|
+
// Avg = sum / count, derived from the two stored states.
|
|
343
|
+
mergedExpr = `if(countMerge(valueCountState) = 0, 0, sumMerge(valueSumState) / countMerge(valueCountState))`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/*
|
|
347
|
+
* Build the WHERE on a copy of the query with `time` removed so
|
|
348
|
+
* the generator never references a column that doesn't exist on
|
|
349
|
+
* the MV. We then add an explicit `bucketTime` range from
|
|
350
|
+
* startTimestamp/endTimestamp.
|
|
351
|
+
*/
|
|
352
|
+
const nonTimeWhere: Statement = this.statementGenerator.toWhereStatement(
|
|
353
|
+
this.stripTimeFromQuery(aggregateBy.query) as typeof aggregateBy.query,
|
|
354
|
+
);
|
|
355
|
+
const sortStatement: Statement = this.statementGenerator.toSortStatement(
|
|
356
|
+
aggregateBy.sort!,
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
const statement: Statement = SQL``;
|
|
360
|
+
|
|
361
|
+
statement.append(
|
|
362
|
+
`SELECT ${mergedExpr} as value, date_trunc('${intervalLower}', toStartOfInterval(bucketTime, INTERVAL 1 ${intervalLower})) as time`,
|
|
363
|
+
);
|
|
364
|
+
statement.append(SQL` FROM ${databaseName}.MetricItemAggMV1m`);
|
|
365
|
+
statement.append(
|
|
366
|
+
` WHERE bucketTime >= toDateTime('${this.formatDateTime(aggregateBy.startTimestamp!)}') AND bucketTime <= toDateTime('${this.formatDateTime(aggregateBy.endTimestamp!)}')`,
|
|
367
|
+
);
|
|
368
|
+
statement.append(SQL` `).append(nonTimeWhere);
|
|
369
|
+
|
|
370
|
+
statement.append(SQL` GROUP BY `).append(`time`);
|
|
371
|
+
statement.append(SQL` ORDER BY `).append(sortStatement);
|
|
372
|
+
statement.append(
|
|
373
|
+
SQL` LIMIT ${{
|
|
374
|
+
value: Number(aggregateBy.limit),
|
|
375
|
+
type: TableColumnType.Number,
|
|
376
|
+
}}`,
|
|
377
|
+
);
|
|
378
|
+
statement.append(
|
|
379
|
+
SQL` OFFSET ${{
|
|
380
|
+
value: Number(aggregateBy.skip),
|
|
381
|
+
type: TableColumnType.Number,
|
|
382
|
+
}} `,
|
|
383
|
+
);
|
|
384
|
+
statement.append(
|
|
385
|
+
` SETTINGS optimize_aggregation_in_order=1, optimize_move_to_prewhere=1, max_threads=4`,
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
logger.debug(`${this.model.tableName} MV Aggregate Statement`, {
|
|
389
|
+
tableName: this.model.tableName,
|
|
390
|
+
} as LogAttributes);
|
|
391
|
+
logger.debug(statement, {
|
|
392
|
+
tableName: this.model.tableName,
|
|
393
|
+
} as LogAttributes);
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
statement,
|
|
397
|
+
columns: [
|
|
398
|
+
aggregateBy.aggregateColumnName.toString(),
|
|
399
|
+
aggregateBy.aggregationTimestampColumnName.toString(),
|
|
400
|
+
],
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private stripTimeFromQuery(query: unknown): typeof query {
|
|
405
|
+
if (!query || typeof query !== "object") {
|
|
406
|
+
return query;
|
|
407
|
+
}
|
|
408
|
+
const out: Record<string, unknown> = {};
|
|
409
|
+
for (const [k, v] of Object.entries(query as Record<string, unknown>)) {
|
|
410
|
+
if (k === "time") {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
out[k] = v;
|
|
414
|
+
}
|
|
415
|
+
return out as typeof query;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private formatDateTime(d: Date): string {
|
|
419
|
+
/*
|
|
420
|
+
* ClickHouse's DateTime parser accepts 'YYYY-MM-DD HH:MM:SS'.
|
|
421
|
+
* toISOString gives 'YYYY-MM-DDTHH:MM:SS.sssZ'; trim the milliseconds
|
|
422
|
+
* and the trailing 'Z' and replace 'T' with a space.
|
|
423
|
+
*/
|
|
424
|
+
return new Date(d).toISOString().replace("T", " ").substring(0, 19);
|
|
425
|
+
}
|
|
234
426
|
}
|
|
235
427
|
|
|
236
428
|
export default new MetricService();
|
|
@@ -3,6 +3,7 @@ import TelemetryType from "../../Types/Telemetry/TelemetryType";
|
|
|
3
3
|
import LogDatabaseService from "./LogService";
|
|
4
4
|
import MetricDatabaseService from "./MetricService";
|
|
5
5
|
import SpanDatabaseService from "./SpanService";
|
|
6
|
+
import ExceptionInstanceService from "./ExceptionInstanceService";
|
|
6
7
|
import TableColumnType from "../../Types/AnalyticsDatabase/TableColumnType";
|
|
7
8
|
import { JSONObject } from "../../Types/JSON";
|
|
8
9
|
import ObjectID from "../../Types/ObjectID";
|
|
@@ -18,7 +19,12 @@ type TelemetrySource = {
|
|
|
18
19
|
service: AnalyticsDatabaseService<any>;
|
|
19
20
|
tableName: string;
|
|
20
21
|
attributesColumn: string;
|
|
21
|
-
|
|
22
|
+
/*
|
|
23
|
+
* Some tables (e.g. ExceptionInstance) don't have a separate
|
|
24
|
+
* attributeKeys array column — only the attributes map. Leave this
|
|
25
|
+
* undefined for those; the SQL falls back to mapKeys(attributes).
|
|
26
|
+
*/
|
|
27
|
+
attributeKeysColumn?: string | undefined;
|
|
22
28
|
timeColumn: string;
|
|
23
29
|
};
|
|
24
30
|
|
|
@@ -62,6 +68,14 @@ export class TelemetryAttributeService {
|
|
|
62
68
|
attributeKeysColumn: "attributeKeys",
|
|
63
69
|
timeColumn: "startTime",
|
|
64
70
|
};
|
|
71
|
+
case TelemetryType.Exception:
|
|
72
|
+
return {
|
|
73
|
+
service: ExceptionInstanceService,
|
|
74
|
+
tableName: ExceptionInstanceService.model.tableName,
|
|
75
|
+
attributesColumn: "attributes",
|
|
76
|
+
// ExceptionInstance has no attributeKeys column.
|
|
77
|
+
timeColumn: "time",
|
|
78
|
+
};
|
|
65
79
|
default:
|
|
66
80
|
return null;
|
|
67
81
|
}
|
|
@@ -225,14 +239,21 @@ export class TelemetryAttributeService {
|
|
|
225
239
|
projectId: ObjectID;
|
|
226
240
|
tableName: string;
|
|
227
241
|
attributesColumn: string;
|
|
228
|
-
attributeKeysColumn
|
|
242
|
+
attributeKeysColumn?: string | undefined;
|
|
229
243
|
timeColumn: string;
|
|
230
244
|
metricName?: string | undefined;
|
|
231
245
|
}): Statement {
|
|
232
246
|
const lookbackStartDate: Date =
|
|
233
247
|
TelemetryAttributeService.getLookbackStartDate();
|
|
234
248
|
|
|
235
|
-
|
|
249
|
+
/*
|
|
250
|
+
* If the source has a denormalized attributeKeys array column, prefer it
|
|
251
|
+
* (avoids materializing every row's map). Otherwise fall back to
|
|
252
|
+
* mapKeys(attributes) — slower but works for tables that don't carry
|
|
253
|
+
* the precomputed array (e.g. ExceptionInstance).
|
|
254
|
+
*/
|
|
255
|
+
const statement: Statement = data.attributeKeysColumn
|
|
256
|
+
? SQL`
|
|
236
257
|
WITH filtered AS (
|
|
237
258
|
SELECT arrayJoin(
|
|
238
259
|
if(
|
|
@@ -250,6 +271,19 @@ export class TelemetryAttributeService {
|
|
|
250
271
|
NOT empty(${data.attributeKeysColumn}) OR
|
|
251
272
|
NOT empty(${data.attributesColumn})
|
|
252
273
|
)
|
|
274
|
+
AND ${data.timeColumn} >= ${{
|
|
275
|
+
type: TableColumnType.Date,
|
|
276
|
+
value: lookbackStartDate,
|
|
277
|
+
}}`
|
|
278
|
+
: SQL`
|
|
279
|
+
WITH filtered AS (
|
|
280
|
+
SELECT arrayJoin(mapKeys(${data.attributesColumn})) AS attribute
|
|
281
|
+
FROM ${data.tableName}
|
|
282
|
+
WHERE projectId = ${{
|
|
283
|
+
type: TableColumnType.ObjectID,
|
|
284
|
+
value: data.projectId,
|
|
285
|
+
}}
|
|
286
|
+
AND NOT empty(${data.attributesColumn})
|
|
253
287
|
AND ${data.timeColumn} >= ${{
|
|
254
288
|
type: TableColumnType.Date,
|
|
255
289
|
value: lookbackStartDate,
|
|
@@ -15,6 +15,7 @@ import AnalyticsTableColumn, {
|
|
|
15
15
|
SkipIndexType,
|
|
16
16
|
} from "../../../Types/AnalyticsDatabase/TableColumn";
|
|
17
17
|
import TableColumnType from "../../../Types/AnalyticsDatabase/TableColumnType";
|
|
18
|
+
import EqualTo from "../../../Types/BaseDatabase/EqualTo";
|
|
18
19
|
import GreaterThan from "../../../Types/BaseDatabase/GreaterThan";
|
|
19
20
|
import GreaterThanOrEqual from "../../../Types/BaseDatabase/GreaterThanOrEqual";
|
|
20
21
|
import InBetween from "../../../Types/BaseDatabase/InBetween";
|
|
@@ -25,7 +26,11 @@ import LessThanOrEqual from "../../../Types/BaseDatabase/LessThanOrEqual";
|
|
|
25
26
|
import GreaterThanOrNull from "../../../Types/BaseDatabase/GreaterThanOrNull";
|
|
26
27
|
import LessThanOrNull from "../../../Types/BaseDatabase/LessThanOrNull";
|
|
27
28
|
import NotEqual from "../../../Types/BaseDatabase/NotEqual";
|
|
29
|
+
import NotContains from "../../../Types/BaseDatabase/NotContains";
|
|
30
|
+
import NotNull from "../../../Types/BaseDatabase/NotNull";
|
|
28
31
|
import Search from "../../../Types/BaseDatabase/Search";
|
|
32
|
+
import StartsWith from "../../../Types/BaseDatabase/StartsWith";
|
|
33
|
+
import EndsWith from "../../../Types/BaseDatabase/EndsWith";
|
|
29
34
|
import SortOrder from "../../../Types/BaseDatabase/SortOrder";
|
|
30
35
|
import OneUptimeDate from "../../../Types/Date";
|
|
31
36
|
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
@@ -447,33 +452,195 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
447
452
|
tableColumn.type === TableColumnType.MapStringString &&
|
|
448
453
|
typeof value === "object"
|
|
449
454
|
) {
|
|
450
|
-
const mapValue: Record<string,
|
|
451
|
-
|
|
455
|
+
const mapValue: Record<string, unknown> = value as Record<
|
|
456
|
+
string,
|
|
457
|
+
unknown
|
|
458
|
+
>;
|
|
452
459
|
for (const mapKey in mapValue) {
|
|
453
|
-
|
|
460
|
+
const mapEntry: unknown = mapValue[mapKey];
|
|
461
|
+
if (mapEntry === undefined || mapEntry === null) {
|
|
454
462
|
continue;
|
|
455
463
|
}
|
|
456
|
-
|
|
464
|
+
|
|
465
|
+
/*
|
|
466
|
+
* ClickHouse Map columns return the value type's default for
|
|
467
|
+
* missing keys (empty string for String values), so to express
|
|
468
|
+
* "is empty" we have to cover both the missing-key and the
|
|
469
|
+
* empty-string case explicitly.
|
|
470
|
+
*/
|
|
471
|
+
if (mapEntry instanceof IsNull) {
|
|
472
|
+
whereStatement.append(
|
|
473
|
+
SQL`AND ((NOT mapContains(${key}, ${{
|
|
474
|
+
value: mapKey,
|
|
475
|
+
type: TableColumnType.Text,
|
|
476
|
+
}})) OR ${key}[${{
|
|
477
|
+
value: mapKey,
|
|
478
|
+
type: TableColumnType.Text,
|
|
479
|
+
}}] = '')`,
|
|
480
|
+
);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (mapEntry instanceof NotNull) {
|
|
485
|
+
whereStatement.append(
|
|
486
|
+
SQL`AND mapContains(${key}, ${{
|
|
487
|
+
value: mapKey,
|
|
488
|
+
type: TableColumnType.Text,
|
|
489
|
+
}}) AND ${key}[${{
|
|
490
|
+
value: mapKey,
|
|
491
|
+
type: TableColumnType.Text,
|
|
492
|
+
}}] != ''`,
|
|
493
|
+
);
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (mapEntry instanceof Search) {
|
|
498
|
+
whereStatement.append(
|
|
499
|
+
SQL`AND ${key}[${{
|
|
500
|
+
value: mapKey,
|
|
501
|
+
type: TableColumnType.Text,
|
|
502
|
+
}}] ILIKE ${{
|
|
503
|
+
value: mapEntry as Search<string>,
|
|
504
|
+
type: TableColumnType.Text,
|
|
505
|
+
}}`,
|
|
506
|
+
);
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (mapEntry instanceof NotContains) {
|
|
511
|
+
const literalValue: string = `%${(mapEntry.value as string) || ""}%`;
|
|
512
|
+
whereStatement.append(
|
|
513
|
+
SQL`AND ${key}[${{
|
|
514
|
+
value: mapKey,
|
|
515
|
+
type: TableColumnType.Text,
|
|
516
|
+
}}] NOT ILIKE ${{
|
|
517
|
+
value: literalValue,
|
|
518
|
+
type: TableColumnType.Text,
|
|
519
|
+
}}`,
|
|
520
|
+
);
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (mapEntry instanceof StartsWith) {
|
|
525
|
+
const literalValue: string = `${(mapEntry.value as string) || ""}%`;
|
|
526
|
+
whereStatement.append(
|
|
527
|
+
SQL`AND ${key}[${{
|
|
528
|
+
value: mapKey,
|
|
529
|
+
type: TableColumnType.Text,
|
|
530
|
+
}}] ILIKE ${{
|
|
531
|
+
value: literalValue,
|
|
532
|
+
type: TableColumnType.Text,
|
|
533
|
+
}}`,
|
|
534
|
+
);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (mapEntry instanceof EndsWith) {
|
|
539
|
+
const literalValue: string = `%${(mapEntry.value as string) || ""}`;
|
|
457
540
|
whereStatement.append(
|
|
458
541
|
SQL`AND ${key}[${{
|
|
459
542
|
value: mapKey,
|
|
460
543
|
type: TableColumnType.Text,
|
|
461
544
|
}}] ILIKE ${{
|
|
462
|
-
value:
|
|
545
|
+
value: literalValue,
|
|
463
546
|
type: TableColumnType.Text,
|
|
464
547
|
}}`,
|
|
465
548
|
);
|
|
466
|
-
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (mapEntry instanceof NotEqual) {
|
|
553
|
+
whereStatement.append(
|
|
554
|
+
SQL`AND ${key}[${{
|
|
555
|
+
value: mapKey,
|
|
556
|
+
type: TableColumnType.Text,
|
|
557
|
+
}}] != ${{
|
|
558
|
+
value: String((mapEntry as NotEqual<string>).value ?? ""),
|
|
559
|
+
type: TableColumnType.Text,
|
|
560
|
+
}}`,
|
|
561
|
+
);
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (mapEntry instanceof EqualTo) {
|
|
467
566
|
whereStatement.append(
|
|
468
567
|
SQL`AND ${key}[${{
|
|
469
568
|
value: mapKey,
|
|
470
569
|
type: TableColumnType.Text,
|
|
471
570
|
}}] = ${{
|
|
472
|
-
value:
|
|
571
|
+
value: String((mapEntry as EqualTo<any>).value ?? ""),
|
|
473
572
|
type: TableColumnType.Text,
|
|
474
573
|
}}`,
|
|
475
574
|
);
|
|
575
|
+
continue;
|
|
476
576
|
}
|
|
577
|
+
|
|
578
|
+
/*
|
|
579
|
+
* Map values are stored as text; cast to Float64 for numeric
|
|
580
|
+
* comparisons and skip rows where the cast fails (non-numeric).
|
|
581
|
+
*/
|
|
582
|
+
if (mapEntry instanceof GreaterThan) {
|
|
583
|
+
whereStatement.append(
|
|
584
|
+
SQL`AND toFloat64OrNull(${key}[${{
|
|
585
|
+
value: mapKey,
|
|
586
|
+
type: TableColumnType.Text,
|
|
587
|
+
}}]) > ${{
|
|
588
|
+
value: Number((mapEntry as GreaterThan<any>).value),
|
|
589
|
+
type: TableColumnType.Number,
|
|
590
|
+
}}`,
|
|
591
|
+
);
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (mapEntry instanceof GreaterThanOrEqual) {
|
|
596
|
+
whereStatement.append(
|
|
597
|
+
SQL`AND toFloat64OrNull(${key}[${{
|
|
598
|
+
value: mapKey,
|
|
599
|
+
type: TableColumnType.Text,
|
|
600
|
+
}}]) >= ${{
|
|
601
|
+
value: Number((mapEntry as GreaterThanOrEqual<any>).value),
|
|
602
|
+
type: TableColumnType.Number,
|
|
603
|
+
}}`,
|
|
604
|
+
);
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (mapEntry instanceof LessThan) {
|
|
609
|
+
whereStatement.append(
|
|
610
|
+
SQL`AND toFloat64OrNull(${key}[${{
|
|
611
|
+
value: mapKey,
|
|
612
|
+
type: TableColumnType.Text,
|
|
613
|
+
}}]) < ${{
|
|
614
|
+
value: Number((mapEntry as LessThan<any>).value),
|
|
615
|
+
type: TableColumnType.Number,
|
|
616
|
+
}}`,
|
|
617
|
+
);
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (mapEntry instanceof LessThanOrEqual) {
|
|
622
|
+
whereStatement.append(
|
|
623
|
+
SQL`AND toFloat64OrNull(${key}[${{
|
|
624
|
+
value: mapKey,
|
|
625
|
+
type: TableColumnType.Text,
|
|
626
|
+
}}]) <= ${{
|
|
627
|
+
value: Number((mapEntry as LessThanOrEqual<any>).value),
|
|
628
|
+
type: TableColumnType.Number,
|
|
629
|
+
}}`,
|
|
630
|
+
);
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Bare string/number/boolean — back-compat with existing data.
|
|
635
|
+
whereStatement.append(
|
|
636
|
+
SQL`AND ${key}[${{
|
|
637
|
+
value: mapKey,
|
|
638
|
+
type: TableColumnType.Text,
|
|
639
|
+
}}] = ${{
|
|
640
|
+
value: String(mapEntry),
|
|
641
|
+
type: TableColumnType.Text,
|
|
642
|
+
}}`,
|
|
643
|
+
);
|
|
477
644
|
}
|
|
478
645
|
} else if (
|
|
479
646
|
(tableColumn.type === TableColumnType.JSON ||
|
package/Tests/Types/Date.test.ts
CHANGED
|
@@ -250,4 +250,50 @@ describe("class OneUptimeDate", () => {
|
|
|
250
250
|
expect(result[0]).not.toContain("EST");
|
|
251
251
|
});
|
|
252
252
|
});
|
|
253
|
+
|
|
254
|
+
describe("getDateAsFormattedArrayInMultipleTimezones default timezones", () => {
|
|
255
|
+
test("defaults include UTC, London, New York, LA, Kolkata, Sydney", () => {
|
|
256
|
+
const result: Array<string> =
|
|
257
|
+
OneUptimeDate.getDateAsFormattedArrayInMultipleTimezones({
|
|
258
|
+
date: new Date("2026-07-15T17:00:00Z"),
|
|
259
|
+
use12HourFormat: true,
|
|
260
|
+
});
|
|
261
|
+
expect(result).toHaveLength(6);
|
|
262
|
+
expect(result[0]).toContain("UTC");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("summer (DST) event shows EDT and BST in defaults, not EST or GMT", () => {
|
|
266
|
+
/*
|
|
267
|
+
* Apr 27 2026 21:30 UTC — both US and UK are in DST. Use word
|
|
268
|
+
* boundaries so AEST/AEDT (Sydney) doesn't match EST/EDT.
|
|
269
|
+
*/
|
|
270
|
+
const result: Array<string> =
|
|
271
|
+
OneUptimeDate.getDateAsFormattedArrayInMultipleTimezones({
|
|
272
|
+
date: new Date("2026-04-27T21:30:00Z"),
|
|
273
|
+
use12HourFormat: true,
|
|
274
|
+
});
|
|
275
|
+
const joined: string = result.join("\n");
|
|
276
|
+
expect(joined).toMatch(/\bEDT\b/);
|
|
277
|
+
expect(joined).toMatch(/\bBST\b/);
|
|
278
|
+
expect(joined).not.toMatch(/\bEST\b/);
|
|
279
|
+
expect(joined).not.toMatch(/\bGMT\b/);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("winter (no DST) event shows EST and GMT in defaults, not EDT or BST", () => {
|
|
283
|
+
/*
|
|
284
|
+
* Jan 15 2026 17:00 UTC — both US and UK are on standard time. Use
|
|
285
|
+
* word boundaries so AEDT (Sydney) doesn't match EDT.
|
|
286
|
+
*/
|
|
287
|
+
const result: Array<string> =
|
|
288
|
+
OneUptimeDate.getDateAsFormattedArrayInMultipleTimezones({
|
|
289
|
+
date: new Date("2026-01-15T17:00:00Z"),
|
|
290
|
+
use12HourFormat: true,
|
|
291
|
+
});
|
|
292
|
+
const joined: string = result.join("\n");
|
|
293
|
+
expect(joined).toMatch(/\bEST\b/);
|
|
294
|
+
expect(joined).toMatch(/\bGMT\b/);
|
|
295
|
+
expect(joined).not.toMatch(/\bEDT\b/);
|
|
296
|
+
expect(joined).not.toMatch(/\bBST\b/);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
253
299
|
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import ObjectID from "../../ObjectID";
|
|
2
|
+
import DashboardComponentType from "../DashboardComponentType";
|
|
3
|
+
import BaseComponent from "./DashboardBaseComponent";
|
|
4
|
+
|
|
5
|
+
export default interface DashboardAlertListComponent extends BaseComponent {
|
|
6
|
+
componentType: DashboardComponentType.AlertList;
|
|
7
|
+
componentId: ObjectID;
|
|
8
|
+
arguments: {
|
|
9
|
+
title?: string | undefined;
|
|
10
|
+
maxRows?: number | undefined;
|
|
11
|
+
stateFilter?: string | undefined;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import ObjectID from "../../ObjectID";
|
|
2
|
+
import DashboardComponentType from "../DashboardComponentType";
|
|
3
|
+
import BaseComponent from "./DashboardBaseComponent";
|
|
4
|
+
|
|
5
|
+
export default interface DashboardIncidentListComponent extends BaseComponent {
|
|
6
|
+
componentType: DashboardComponentType.IncidentList;
|
|
7
|
+
componentId: ObjectID;
|
|
8
|
+
arguments: {
|
|
9
|
+
title?: string | undefined;
|
|
10
|
+
maxRows?: number | undefined;
|
|
11
|
+
stateFilter?: string | undefined;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import ObjectID from "../../ObjectID";
|
|
2
|
+
import DashboardComponentType from "../DashboardComponentType";
|
|
3
|
+
import BaseComponent from "./DashboardBaseComponent";
|
|
4
|
+
|
|
5
|
+
export default interface DashboardMonitorListComponent extends BaseComponent {
|
|
6
|
+
componentType: DashboardComponentType.MonitorList;
|
|
7
|
+
componentId: ObjectID;
|
|
8
|
+
arguments: {
|
|
9
|
+
title?: string | undefined;
|
|
10
|
+
maxRows?: number | undefined;
|
|
11
|
+
statusFilter?: string | undefined;
|
|
12
|
+
};
|
|
13
|
+
}
|
package/Types/Date.ts
CHANGED
|
@@ -1385,13 +1385,18 @@ export default class OneUptimeDate {
|
|
|
1385
1385
|
formatstring = "MMM DD, YYYY";
|
|
1386
1386
|
}
|
|
1387
1387
|
|
|
1388
|
-
// convert this date into GMT, EST, PST, IST, ACT with moment
|
|
1389
1388
|
const timezoneDates: Array<string> = [];
|
|
1390
1389
|
|
|
1391
1390
|
if (!timezones || timezones.length === 0) {
|
|
1391
|
+
/*
|
|
1392
|
+
* Use IANA region zones (not fixed-offset zones like "EST") so the
|
|
1393
|
+
* abbreviation reflects DST at the event's date — e.g. EST/EDT for
|
|
1394
|
+
* America/New_York and GMT/BST for Europe/London.
|
|
1395
|
+
*/
|
|
1392
1396
|
timezones = [
|
|
1393
1397
|
Timezone.UTC,
|
|
1394
|
-
Timezone.
|
|
1398
|
+
Timezone.EuropeLondon,
|
|
1399
|
+
Timezone.AmericaNew_York,
|
|
1395
1400
|
Timezone.AmericaLos_Angeles,
|
|
1396
1401
|
Timezone.AsiaKolkata,
|
|
1397
1402
|
Timezone.AustraliaSydney,
|
|
@@ -1416,7 +1421,7 @@ export default class OneUptimeDate {
|
|
|
1416
1421
|
public static getDateAsFormattedHTMLInMultipleTimezones(data: {
|
|
1417
1422
|
date: string | Date;
|
|
1418
1423
|
onlyShowDate?: boolean;
|
|
1419
|
-
timezones?: Array<Timezone> | undefined; // if
|
|
1424
|
+
timezones?: Array<Timezone> | undefined; // if skipped, defaults to UTC, Europe/London, America/New_York, America/Los_Angeles, Asia/Kolkata, Australia/Sydney (DST-aware)
|
|
1420
1425
|
use12HourFormat?: boolean | undefined;
|
|
1421
1426
|
}): string {
|
|
1422
1427
|
const date: string | Date = data.date;
|
|
@@ -1435,7 +1440,7 @@ export default class OneUptimeDate {
|
|
|
1435
1440
|
public static getDateAsFormattedStringInMultipleTimezones(data: {
|
|
1436
1441
|
date: string | Date;
|
|
1437
1442
|
onlyShowDate?: boolean | undefined;
|
|
1438
|
-
timezones?: Array<Timezone> | undefined; // if
|
|
1443
|
+
timezones?: Array<Timezone> | undefined; // if skipped, defaults to UTC, Europe/London, America/New_York, America/Los_Angeles, Asia/Kolkata, Australia/Sydney (DST-aware)
|
|
1439
1444
|
use12HourFormat?: boolean | undefined;
|
|
1440
1445
|
}): string {
|
|
1441
1446
|
const date: string | Date = data.date;
|