@oneuptime/common 11.0.13 → 11.1.0
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/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.ts +23 -0
- package/Models/AnalyticsModels/AuditLog.ts +2 -0
- package/Models/AnalyticsModels/ExceptionInstance.ts +5 -0
- package/Models/AnalyticsModels/Log.ts +10 -0
- package/Models/AnalyticsModels/Metric.ts +7 -0
- package/Models/AnalyticsModels/MetricBaselineHourly.ts +5 -0
- package/Models/AnalyticsModels/MetricItemAggMV1m.ts +5 -0
- package/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.ts +2 -0
- package/Models/AnalyticsModels/MonitorLog.ts +2 -0
- package/Models/AnalyticsModels/Profile.ts +9 -0
- package/Models/AnalyticsModels/ProfileSample.ts +2 -0
- package/Models/AnalyticsModels/Span.ts +5 -0
- package/Models/DatabaseModels/DockerResource.ts +6 -6
- package/Server/EnvironmentConfig.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1782800000000-WidenDockerResourceImageColumns.ts +45 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/AnalyticsDatabaseService.ts +27 -14
- package/Server/Services/IncidentService.ts +19 -1
- package/Server/Utils/AnalyticsDatabase/ClusterConfig.ts +177 -0
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +101 -21
- package/Server/Utils/CodeRepository/CodeRepository.ts +8 -0
- package/Server/Utils/Execute.ts +9 -0
- package/Server/Utils/Monitor/MonitorIncident.ts +64 -14
- package/Server/Utils/Monitor/MonitorResource.ts +40 -10
- package/Server/Utils/Monitor/MonitorStatusTimeline.ts +32 -6
- package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +8 -3
- package/Tests/Server/Utils/AnalyticsDatabase/ClusterAwareSchema.test.ts +296 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +5 -3
- package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js +8 -0
- package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/AuditLog.js +2 -0
- package/build/dist/Models/AnalyticsModels/AuditLog.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +5 -0
- package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Log.js +10 -0
- package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Metric.js +7 -0
- package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js +5 -0
- package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js +5 -0
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js +2 -0
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MonitorLog.js +2 -0
- package/build/dist/Models/AnalyticsModels/MonitorLog.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Profile.js +9 -0
- package/build/dist/Models/AnalyticsModels/Profile.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/ProfileSample.js +2 -0
- package/build/dist/Models/AnalyticsModels/ProfileSample.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Span.js +5 -0
- package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DockerResource.js +6 -6
- package/build/dist/Models/DatabaseModels/DockerResource.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +21 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782800000000-WidenDockerResourceImageColumns.js +30 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782800000000-WidenDockerResourceImageColumns.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/Services/AnalyticsDatabaseService.js +22 -14
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +17 -1
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/ClusterConfig.js +144 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/ClusterConfig.js.map +1 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +68 -15
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/CodeRepository/CodeRepository.js +2 -6
- package/build/dist/Server/Utils/CodeRepository/CodeRepository.js.map +1 -1
- package/build/dist/Server/Utils/Execute.js +3 -1
- package/build/dist/Server/Utils/Execute.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +70 -27
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +36 -8
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +29 -7
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
- package/package.json +1 -1
|
@@ -6,6 +6,13 @@ import Sort from "../../Types/AnalyticsDatabase/Sort";
|
|
|
6
6
|
import UpdateBy from "../../Types/AnalyticsDatabase/UpdateBy";
|
|
7
7
|
import logger from "../Logger";
|
|
8
8
|
import { SQL, Statement } from "./Statement";
|
|
9
|
+
import {
|
|
10
|
+
adaptTableSettingsForStorage,
|
|
11
|
+
getDistributedEngine,
|
|
12
|
+
getStorageEngine,
|
|
13
|
+
getStorageTableName,
|
|
14
|
+
onClusterClause,
|
|
15
|
+
} from "./ClusterConfig";
|
|
9
16
|
import AnalyticsBaseModel from "../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
|
|
10
17
|
import CommonModel, {
|
|
11
18
|
Record as AnalyticsRecord,
|
|
@@ -78,12 +85,24 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
78
85
|
const setStatement: Statement = this.toSetStatement(updateBy.data);
|
|
79
86
|
const whereStatement: Statement = this.toWhereStatement(updateBy.query);
|
|
80
87
|
|
|
88
|
+
/*
|
|
89
|
+
* `ALTER TABLE … UPDATE` is a mutation and cannot target a Distributed
|
|
90
|
+
* table, so in cluster mode it runs against the local storage table and is
|
|
91
|
+
* dispatched to every shard via ON CLUSTER (replicated within each shard by
|
|
92
|
+
* Keeper). onClusterClause() is appended as RAW SQL — it is not an
|
|
93
|
+
* identifier and must not become a {pN:Identifier} parameter — and is empty
|
|
94
|
+
* in single-node mode, leaving the original statement unchanged.
|
|
95
|
+
*/
|
|
81
96
|
/* eslint-disable prettier/prettier */
|
|
82
97
|
const statement: Statement = SQL`
|
|
83
|
-
ALTER TABLE ${this.database.getDatasourceOptions().database!}.${
|
|
84
|
-
this.model.tableName
|
|
85
|
-
}
|
|
86
|
-
|
|
98
|
+
ALTER TABLE ${this.database.getDatasourceOptions().database!}.${getStorageTableName(
|
|
99
|
+
this.model.tableName,
|
|
100
|
+
)}`
|
|
101
|
+
.append(onClusterClause())
|
|
102
|
+
.append(
|
|
103
|
+
SQL`
|
|
104
|
+
UPDATE `,
|
|
105
|
+
)
|
|
87
106
|
.append(setStatement)
|
|
88
107
|
.append(
|
|
89
108
|
SQL`
|
|
@@ -1089,9 +1108,9 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
1089
1108
|
): Promise<Statement> {
|
|
1090
1109
|
const statement: string = `ALTER TABLE ${
|
|
1091
1110
|
this.database.getDatasourceOptions().database
|
|
1092
|
-
}.${
|
|
1093
|
-
this.model.tableName
|
|
1094
|
-
} RENAME COLUMN IF EXISTS ${oldColumnName} TO ${newColumnName}`;
|
|
1111
|
+
}.${getStorageTableName(
|
|
1112
|
+
this.model.tableName,
|
|
1113
|
+
)} RENAME COLUMN IF EXISTS ${oldColumnName} TO ${newColumnName}`;
|
|
1095
1114
|
|
|
1096
1115
|
return SQL`${statement}`;
|
|
1097
1116
|
}
|
|
@@ -1309,9 +1328,9 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
1309
1328
|
}
|
|
1310
1329
|
|
|
1311
1330
|
const statement: Statement = SQL`
|
|
1312
|
-
ALTER TABLE ${this.database.getDatasourceOptions().database!}.${
|
|
1313
|
-
this.model.tableName
|
|
1314
|
-
} ADD COLUMN IF NOT EXISTS `.append(columnDef);
|
|
1331
|
+
ALTER TABLE ${this.database.getDatasourceOptions().database!}.${getStorageTableName(
|
|
1332
|
+
this.model.tableName,
|
|
1333
|
+
)} ADD COLUMN IF NOT EXISTS `.append(columnDef);
|
|
1315
1334
|
|
|
1316
1335
|
logger.debug(`${this.model.tableName} Add Column Statement`);
|
|
1317
1336
|
logger.debug(statement);
|
|
@@ -1341,7 +1360,7 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
1341
1360
|
const databaseName: string = this.database.getDatasourceOptions().database!;
|
|
1342
1361
|
const statement: Statement = new Statement();
|
|
1343
1362
|
statement.append(
|
|
1344
|
-
`ALTER TABLE ${databaseName}.${this.model.tableName} ADD INDEX IF NOT EXISTS ${idx.name} ${columnExpr} TYPE ${idx.type}${paramsStr} GRANULARITY ${idx.granularity}`,
|
|
1363
|
+
`ALTER TABLE ${databaseName}.${getStorageTableName(this.model.tableName)} ADD INDEX IF NOT EXISTS ${idx.name} ${columnExpr} TYPE ${idx.type}${paramsStr} GRANULARITY ${idx.granularity}`,
|
|
1345
1364
|
);
|
|
1346
1365
|
|
|
1347
1366
|
logger.debug(`${this.model.tableName} Add Skip Index Statement`);
|
|
@@ -1352,7 +1371,7 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
1352
1371
|
|
|
1353
1372
|
public toDropSkipIndexStatement(indexName: string): string {
|
|
1354
1373
|
const databaseName: string = this.database.getDatasourceOptions().database!;
|
|
1355
|
-
const statement: string = `ALTER TABLE ${databaseName}.${this.model.tableName} DROP INDEX IF EXISTS ${indexName}`;
|
|
1374
|
+
const statement: string = `ALTER TABLE ${databaseName}.${getStorageTableName(this.model.tableName)} DROP INDEX IF EXISTS ${indexName}`;
|
|
1356
1375
|
|
|
1357
1376
|
logger.debug(`${this.model.tableName} Drop Skip Index Statement`);
|
|
1358
1377
|
logger.debug(statement);
|
|
@@ -1362,7 +1381,9 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
1362
1381
|
|
|
1363
1382
|
public toDropColumnStatement(columnName: string): string {
|
|
1364
1383
|
const statement: string = `ALTER TABLE ${this.database.getDatasourceOptions()
|
|
1365
|
-
.database!}.${
|
|
1384
|
+
.database!}.${getStorageTableName(
|
|
1385
|
+
this.model.tableName,
|
|
1386
|
+
)} DROP COLUMN IF EXISTS ${columnName}`;
|
|
1366
1387
|
|
|
1367
1388
|
logger.debug(`${this.model.tableName} Drop Column Statement`);
|
|
1368
1389
|
logger.debug(statement);
|
|
@@ -1377,16 +1398,40 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
1377
1398
|
);
|
|
1378
1399
|
|
|
1379
1400
|
/*
|
|
1380
|
-
* special case - ClickHouse does not support using a query parameter
|
|
1381
|
-
*
|
|
1401
|
+
* special case - ClickHouse does not support using a query parameter to
|
|
1402
|
+
* specify the table name, engine, or ON CLUSTER clause, so these are
|
|
1403
|
+
* interpolated as raw SQL (the SQL tag only parameterizes ${{value,type}}
|
|
1404
|
+
* objects, not plain string interpolations).
|
|
1405
|
+
*
|
|
1406
|
+
* In cluster mode this builds the LOCAL storage table
|
|
1407
|
+
* (`<tableName>Local` with a Replicated* engine) `ON CLUSTER '<name>'`; the
|
|
1408
|
+
* app-facing Distributed table is created separately via
|
|
1409
|
+
* toDistributedTableCreateStatement(). In single-node mode it builds the
|
|
1410
|
+
* model's own table with its plain engine, exactly as before.
|
|
1382
1411
|
*/
|
|
1383
|
-
const tableEngineStatement: string =
|
|
1412
|
+
const tableEngineStatement: string = getStorageEngine(
|
|
1413
|
+
this.model.tableEngine,
|
|
1414
|
+
);
|
|
1415
|
+
|
|
1416
|
+
const storageTableName: string = getStorageTableName(this.model.tableName);
|
|
1417
|
+
|
|
1418
|
+
const onCluster: string = onClusterClause();
|
|
1384
1419
|
|
|
1385
1420
|
const partitionKey: string = this.model.partitionKey;
|
|
1386
1421
|
|
|
1387
1422
|
const statement: Statement = SQL`
|
|
1388
|
-
CREATE TABLE IF NOT EXISTS ${databaseName}.${
|
|
1389
|
-
|
|
1423
|
+
CREATE TABLE IF NOT EXISTS ${databaseName}.${storageTableName}`
|
|
1424
|
+
/*
|
|
1425
|
+
* ON CLUSTER is appended as RAW SQL — the SQL tag turns every ${..}
|
|
1426
|
+
* interpolation into a {pN:Identifier} parameter, which would wrongly
|
|
1427
|
+
* quote the whole " ON CLUSTER '<name>'" clause as a single identifier.
|
|
1428
|
+
* onCluster is "" in single-node mode, so this is a no-op there.
|
|
1429
|
+
*/
|
|
1430
|
+
.append(onCluster)
|
|
1431
|
+
.append(
|
|
1432
|
+
SQL`
|
|
1433
|
+
(\n`,
|
|
1434
|
+
)
|
|
1390
1435
|
.append(columnsStatement)
|
|
1391
1436
|
.append(
|
|
1392
1437
|
SQL`
|
|
@@ -1426,10 +1471,15 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
1426
1471
|
|
|
1427
1472
|
/*
|
|
1428
1473
|
* Append table-level SETTINGS if specified (e.g. ttl_only_drop_parts = 1
|
|
1429
|
-
* so TTL drops whole time-partitions instead of rewriting parts).
|
|
1474
|
+
* so TTL drops whole time-partitions instead of rewriting parts). In
|
|
1475
|
+
* cluster mode the non-replicated dedup window is rewritten to its
|
|
1476
|
+
* replicated equivalent so insert idempotency survives.
|
|
1430
1477
|
*/
|
|
1431
|
-
|
|
1432
|
-
|
|
1478
|
+
const tableSettings: string | undefined = adaptTableSettingsForStorage(
|
|
1479
|
+
this.model.tableSettings,
|
|
1480
|
+
);
|
|
1481
|
+
if (tableSettings) {
|
|
1482
|
+
statement.append(`\nSETTINGS ${tableSettings}`);
|
|
1433
1483
|
}
|
|
1434
1484
|
|
|
1435
1485
|
/* eslint-enable prettier/prettier */
|
|
@@ -1439,4 +1489,34 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
1439
1489
|
|
|
1440
1490
|
return statement;
|
|
1441
1491
|
}
|
|
1492
|
+
|
|
1493
|
+
/*
|
|
1494
|
+
* The app-facing Distributed table that wraps the model's local storage table.
|
|
1495
|
+
* Built with `AS <db>.<local>` so its column layout is copied from — and stays
|
|
1496
|
+
* identical to — the local table, and with `CREATE OR REPLACE` so re-running it
|
|
1497
|
+
* every boot atomically re-syncs the wrapper after a column is reconciled onto
|
|
1498
|
+
* the local table (the Distributed table holds no data, so the replace is cheap
|
|
1499
|
+
* and lossless). The Distributed engine routes writes by the model's sharding
|
|
1500
|
+
* key and scatter-gathers reads across all shards.
|
|
1501
|
+
*/
|
|
1502
|
+
public toDistributedTableCreateStatement(): Statement {
|
|
1503
|
+
const databaseName: string = this.database.getDatasourceOptions().database!;
|
|
1504
|
+
const distributedTableName: string = this.model.tableName;
|
|
1505
|
+
const localTableName: string = getStorageTableName(this.model.tableName);
|
|
1506
|
+
const onCluster: string = onClusterClause();
|
|
1507
|
+
const distributedEngine: string = getDistributedEngine(
|
|
1508
|
+
localTableName,
|
|
1509
|
+
this.model.shardingKey,
|
|
1510
|
+
);
|
|
1511
|
+
|
|
1512
|
+
const statement: Statement = new Statement();
|
|
1513
|
+
statement.append(
|
|
1514
|
+
`CREATE OR REPLACE TABLE ${databaseName}.${distributedTableName}${onCluster} AS ${databaseName}.${localTableName} ENGINE = ${distributedEngine}`,
|
|
1515
|
+
);
|
|
1516
|
+
|
|
1517
|
+
logger.debug(`${this.model.tableName} Distributed Table Create Statement`);
|
|
1518
|
+
logger.debug(statement);
|
|
1519
|
+
|
|
1520
|
+
return statement;
|
|
1521
|
+
}
|
|
1442
1522
|
}
|
|
@@ -397,6 +397,13 @@ export default class CodeRepositoryUtil {
|
|
|
397
397
|
public static async getCommitAuthorsWithFiles(data: {
|
|
398
398
|
repoPath: string;
|
|
399
399
|
path?: string | undefined;
|
|
400
|
+
/*
|
|
401
|
+
* Hard upper bound (ms) on the `git log` invocation. Over a full-history repo
|
|
402
|
+
* this walk can take a very long time, so callers on (or near) a request path
|
|
403
|
+
* should bound it; on timeout the child is killed and this rejects, letting
|
|
404
|
+
* the caller fall back gracefully.
|
|
405
|
+
*/
|
|
406
|
+
timeoutInMS?: number | undefined;
|
|
400
407
|
}): Promise<
|
|
401
408
|
Array<{
|
|
402
409
|
authorName: string;
|
|
@@ -428,6 +435,7 @@ export default class CodeRepositoryUtil {
|
|
|
428
435
|
cwd: path.resolve(data.repoPath),
|
|
429
436
|
// git log over a full repo can easily exceed the 1 MB default.
|
|
430
437
|
maxBuffer: 256 * 1024 * 1024,
|
|
438
|
+
...(data.timeoutInMS ? { timeoutInMS: data.timeoutInMS } : {}),
|
|
431
439
|
});
|
|
432
440
|
|
|
433
441
|
const commits: Array<{
|
package/Server/Utils/Execute.ts
CHANGED
|
@@ -59,6 +59,12 @@ export default class Execute {
|
|
|
59
59
|
* can emit large output (e.g. `git log` over a whole repo) should raise this.
|
|
60
60
|
*/
|
|
61
61
|
maxBuffer?: number | undefined;
|
|
62
|
+
/*
|
|
63
|
+
* Hard upper bound (ms) on how long the child may run. When exceeded the
|
|
64
|
+
* child is force-killed (SIGKILL) and the promise rejects, so a slow command
|
|
65
|
+
* (e.g. `git log` over a huge full-history repo) can never hang the caller.
|
|
66
|
+
*/
|
|
67
|
+
timeoutInMS?: number | undefined;
|
|
62
68
|
}): Promise<string> {
|
|
63
69
|
return new Promise(
|
|
64
70
|
(
|
|
@@ -71,6 +77,9 @@ export default class Execute {
|
|
|
71
77
|
{
|
|
72
78
|
cwd: data.cwd,
|
|
73
79
|
...(data.maxBuffer ? { maxBuffer: data.maxBuffer } : {}),
|
|
80
|
+
...(data.timeoutInMS
|
|
81
|
+
? { timeout: data.timeoutInMS, killSignal: "SIGKILL" }
|
|
82
|
+
: {}),
|
|
74
83
|
},
|
|
75
84
|
(err: ExecException | null, stdout: string, stderr: string) => {
|
|
76
85
|
if (err) {
|
|
@@ -341,8 +341,18 @@ export default class MonitorIncident {
|
|
|
341
341
|
storageMap,
|
|
342
342
|
});
|
|
343
343
|
|
|
344
|
-
|
|
345
|
-
|
|
344
|
+
/*
|
|
345
|
+
* Resolve the incident severity. `criteriaIncident.incidentSeverityId`
|
|
346
|
+
* can be a truthy-but-EMPTY ObjectID (id === "") — a stored
|
|
347
|
+
* `{"_type":"ObjectID","value":""}` deserializes to `new ObjectID("")`,
|
|
348
|
+
* which is an object so `!incidentSeverityId` is false. That empty id
|
|
349
|
+
* serializes to "" for the `uuid` (not-null) column and lands as NULL,
|
|
350
|
+
* throwing 23502 inside the probe-ingest worker and retrying forever.
|
|
351
|
+
* Use `?.toString()` truthiness so an empty/blank ObjectID is treated
|
|
352
|
+
* the same as "missing" and falls through to the project-default lookup.
|
|
353
|
+
*/
|
|
354
|
+
if (!criteriaIncident.incidentSeverityId?.toString()) {
|
|
355
|
+
// pick the critical (first/lowest-order root) severity.
|
|
346
356
|
|
|
347
357
|
const severity: IncidentSeverity | null =
|
|
348
358
|
await IncidentSeverityService.findOneBy({
|
|
@@ -360,15 +370,33 @@ export default class MonitorIncident {
|
|
|
360
370
|
},
|
|
361
371
|
});
|
|
362
372
|
|
|
363
|
-
if (!severity) {
|
|
364
|
-
|
|
365
|
-
|
|
373
|
+
if (!severity?.id?.toString()) {
|
|
374
|
+
/*
|
|
375
|
+
* The project has no incident severity configured. Throwing here
|
|
376
|
+
* would fail the entire probe/telemetry ingest job, which then
|
|
377
|
+
* retries forever for a misconfiguration the worker cannot fix.
|
|
378
|
+
* Skip incident creation gracefully and log instead.
|
|
379
|
+
*/
|
|
380
|
+
logger.error(
|
|
381
|
+
`${input.monitor.id?.toString()} - Cannot create incident: project ${input.monitor.projectId?.toString()} has no incident severity configured. Skipping incident creation for criteria "${
|
|
382
|
+
input.criteriaInstance.data?.name
|
|
383
|
+
}".`,
|
|
366
384
|
);
|
|
367
|
-
|
|
368
|
-
|
|
385
|
+
|
|
386
|
+
input.evaluationSummary?.events.push({
|
|
387
|
+
type: "incident-skipped",
|
|
388
|
+
title: "Incident creation skipped",
|
|
389
|
+
message:
|
|
390
|
+
"Skipped creating an incident because the project has no incident severity configured.",
|
|
391
|
+
relatedCriteriaId: input.criteriaInstance.data?.id,
|
|
392
|
+
at: OneUptimeDate.getCurrentDate(),
|
|
393
|
+
});
|
|
394
|
+
continue;
|
|
369
395
|
}
|
|
396
|
+
|
|
397
|
+
incident.incidentSeverityId = severity.id!;
|
|
370
398
|
} else {
|
|
371
|
-
incident.incidentSeverityId = criteriaIncident.incidentSeverityId
|
|
399
|
+
incident.incidentSeverityId = criteriaIncident.incidentSeverityId;
|
|
372
400
|
}
|
|
373
401
|
|
|
374
402
|
incident.monitors = [input.monitor];
|
|
@@ -882,12 +910,34 @@ export default class MonitorIncident {
|
|
|
882
910
|
);
|
|
883
911
|
}
|
|
884
912
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
913
|
+
try {
|
|
914
|
+
await IncidentStateTimelineService.create({
|
|
915
|
+
data: incidentStateTimeline,
|
|
916
|
+
props: {
|
|
917
|
+
isRoot: true,
|
|
918
|
+
},
|
|
919
|
+
});
|
|
920
|
+
} catch (err) {
|
|
921
|
+
/*
|
|
922
|
+
* Idempotent concurrency race: two probe/ingest results for the same monitor
|
|
923
|
+
* can both decide to auto-resolve the same open incident near-simultaneously.
|
|
924
|
+
* The loser's IncidentStateTimelineService.onBeforeCreate dedupe check throws
|
|
925
|
+
* this exact BadDataException (incident is already in the resolved state).
|
|
926
|
+
* Treat as a no-op at debug level instead of failing the job and logging a
|
|
927
|
+
* full ERROR stack. Match the exact message so unrelated BadDataExceptions
|
|
928
|
+
* (e.g. state-order validation) still propagate.
|
|
929
|
+
*/
|
|
930
|
+
if (
|
|
931
|
+
err instanceof BadDataException &&
|
|
932
|
+
err.message === "Incident state cannot be same as previous state."
|
|
933
|
+
) {
|
|
934
|
+
logger.debug(
|
|
935
|
+
`${input.openIncident.id?.toString()} - Incident already in resolved state; skipping duplicate state timeline (concurrent race).`,
|
|
936
|
+
);
|
|
937
|
+
} else {
|
|
938
|
+
throw err;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
891
941
|
}
|
|
892
942
|
|
|
893
943
|
private static shouldCloseIncident(input: {
|
|
@@ -402,9 +402,18 @@ export default class MonitorResourceUtil {
|
|
|
402
402
|
`${dataToProcess.monitorId.toString()} - Monitor metrics saved`,
|
|
403
403
|
);
|
|
404
404
|
|
|
405
|
-
|
|
405
|
+
/*
|
|
406
|
+
* `monitorSteps` is optional on the model. When a monitor has none
|
|
407
|
+
* configured, `monitor.monitorSteps` is undefined and the previous
|
|
408
|
+
* non-null assertion (`!`) was a lie — `monitorSteps.data` then threw
|
|
409
|
+
* "Cannot read properties of null (reading 'data')" inside the probe
|
|
410
|
+
* ingest worker. Guard the value itself and take the existing
|
|
411
|
+
* "no monitoring steps" early return.
|
|
412
|
+
*/
|
|
413
|
+
const monitorSteps: MonitorSteps | undefined = monitor.monitorSteps;
|
|
406
414
|
|
|
407
415
|
if (
|
|
416
|
+
!monitorSteps ||
|
|
408
417
|
!monitorSteps.data?.monitorStepsInstanceArray ||
|
|
409
418
|
monitorSteps.data?.monitorStepsInstanceArray.length === 0
|
|
410
419
|
) {
|
|
@@ -856,15 +865,36 @@ export default class MonitorResourceUtil {
|
|
|
856
865
|
monitorStatusTimeline.rootCause =
|
|
857
866
|
"No monitoring criteria met. Change to default status. ";
|
|
858
867
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
+
try {
|
|
869
|
+
await MonitorStatusTimelineService.create({
|
|
870
|
+
data: monitorStatusTimeline,
|
|
871
|
+
props: {
|
|
872
|
+
isRoot: true,
|
|
873
|
+
},
|
|
874
|
+
});
|
|
875
|
+
logger.debug(
|
|
876
|
+
`${dataToProcess.monitorId.toString()} - Monitor status updated to default.`,
|
|
877
|
+
);
|
|
878
|
+
} catch (err) {
|
|
879
|
+
/*
|
|
880
|
+
* Idempotent concurrency race (see MonitorStatusTimeline.ts): a
|
|
881
|
+
* concurrent result already moved the monitor to this default status,
|
|
882
|
+
* so onBeforeCreate's dedupe check throws this exact BadDataException.
|
|
883
|
+
* Treat as a no-op at debug level rather than failing the job. Match the
|
|
884
|
+
* exact message so unrelated BadDataExceptions still propagate.
|
|
885
|
+
*/
|
|
886
|
+
if (
|
|
887
|
+
err instanceof BadDataException &&
|
|
888
|
+
err.message ===
|
|
889
|
+
"Monitor Status cannot be same as previous status."
|
|
890
|
+
) {
|
|
891
|
+
logger.debug(
|
|
892
|
+
`${dataToProcess.monitorId.toString()} - Monitor status already at default; skipping duplicate status timeline (concurrent race).`,
|
|
893
|
+
);
|
|
894
|
+
} else {
|
|
895
|
+
throw err;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
868
898
|
|
|
869
899
|
const defaultStatusName: string | null = await getMonitorStatusName(
|
|
870
900
|
monitorSteps.data.defaultMonitorStatusId,
|
|
@@ -114,12 +114,38 @@ export default class MonitorStatusTimelineUtil {
|
|
|
114
114
|
);
|
|
115
115
|
monitorStatusTimeline.rootCause = input.rootCause;
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
117
|
+
try {
|
|
118
|
+
return await MonitorStatusTimelineService.create({
|
|
119
|
+
data: monitorStatusTimeline,
|
|
120
|
+
props: {
|
|
121
|
+
isRoot: true,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
} catch (err) {
|
|
125
|
+
/*
|
|
126
|
+
* Concurrency race: two probe/ingest results for the same monitor can be
|
|
127
|
+
* processed near-simultaneously (the per-monitor mutexes in
|
|
128
|
+
* MonitorResourceUtil.monitorResource and MonitorStatusTimeline.create can
|
|
129
|
+
* time out under load and fall through unlocked). Both see the same prior
|
|
130
|
+
* status and both try to write the same new status row. The
|
|
131
|
+
* MonitorStatusTimelineService.onBeforeCreate dedupe check then throws this
|
|
132
|
+
* exact BadDataException for the loser of the race. This is an idempotent
|
|
133
|
+
* no-op (the desired status is already the current status), so swallow it at
|
|
134
|
+
* debug level instead of failing the job and logging a full ERROR stack.
|
|
135
|
+
* Match the exact message so unrelated BadDataExceptions still propagate.
|
|
136
|
+
*/
|
|
137
|
+
if (
|
|
138
|
+
err instanceof BadDataException &&
|
|
139
|
+
err.message === "Monitor Status cannot be same as previous status."
|
|
140
|
+
) {
|
|
141
|
+
logger.debug(
|
|
142
|
+
`${input.monitor.id?.toString()} - Monitor status already equals desired status; skipping duplicate status timeline (concurrent race).`,
|
|
143
|
+
);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
123
149
|
}
|
|
124
150
|
|
|
125
151
|
return null;
|
|
@@ -356,13 +356,18 @@ describe("AnalyticsDatabaseService", () => {
|
|
|
356
356
|
tableName: "<table-name>",
|
|
357
357
|
});
|
|
358
358
|
|
|
359
|
+
/*
|
|
360
|
+
* Cluster mode: deletes are an ALTER ... DELETE mutation on the local
|
|
361
|
+
* table, dispatched ON CLUSTER (lightweight DELETE can't hit a Distributed
|
|
362
|
+
* table).
|
|
363
|
+
*/
|
|
359
364
|
expect(statement.query).toBe(
|
|
360
|
-
"
|
|
361
|
-
"WHERE TRUE <where-statement>",
|
|
365
|
+
"ALTER TABLE {p0:Identifier}.{p1:Identifier} ON CLUSTER 'oneuptime'\n" +
|
|
366
|
+
"DELETE WHERE TRUE <where-statement>",
|
|
362
367
|
);
|
|
363
368
|
expect(statement.query_params).toStrictEqual({
|
|
364
369
|
p0: "oneuptime",
|
|
365
|
-
p1: "<table-name>",
|
|
370
|
+
p1: "<table-name>Local",
|
|
366
371
|
});
|
|
367
372
|
});
|
|
368
373
|
});
|