@oneuptime/common 11.3.6 → 11.3.7

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.
@@ -33,6 +33,7 @@ import AlertStateTimeline from "../../Models/DatabaseModels/AlertStateTimeline";
33
33
  import User from "../../Models/DatabaseModels/User";
34
34
  import { IsBillingEnabled } from "../EnvironmentConfig";
35
35
  import logger, { LogAttributes } from "../Utils/Logger";
36
+ import Semaphore, { SemaphoreMutex } from "../Infrastructure/Semaphore";
36
37
  import TelemetryUtil from "../Utils/Telemetry/Telemetry";
37
38
  import MetricService from "./MetricService";
38
39
  import GlobalConfigService from "./GlobalConfigService";
@@ -1335,91 +1336,111 @@ ${alertSeverity.name}
1335
1336
  const firstAlertStateTimeline: AlertStateTimeline | undefined =
1336
1337
  alertStateTimelines[0];
1337
1338
 
1338
- // delete all the alert metrics with this alert id because it's a refresh
1339
- await MetricService.deleteBy({
1340
- query: {
1341
- primaryEntityId: data.alertId,
1342
- },
1343
- props: {
1344
- isRoot: true,
1345
- },
1346
- });
1347
-
1348
- const itemsToSave: Array<Metric> = [];
1349
- const metricTypesMap: Dictionary<MetricType> = {};
1350
-
1351
- const metricRetentionDays: number = await this.getMetricRetentionDays();
1352
- const alertMetricRetentionDate: Date = OneUptimeDate.addRemoveDays(
1353
- OneUptimeDate.getCurrentDate(),
1354
- metricRetentionDays,
1355
- );
1339
+ /*
1340
+ * Serialize concurrent refreshes for this alert across pods. The
1341
+ * write-once guard below is read-then-insert (findBy existing, then a
1342
+ * conditional createMany). Both call sites fire refresh fire-and-forget
1343
+ * after releasing the per-alert state-timeline mutex, so two
1344
+ * close-together transitions (e.g. auto-acknowledge then auto-resolve)
1345
+ * could interleave: both read a snapshot missing a metric and both
1346
+ * insert it. The Metric table is a plain MergeTree with no row collapse,
1347
+ * so those duplicates would be permanent and re-inflate the very Sum/Avg
1348
+ * widgets this change fixes. A best-effort distributed lock keyed on the
1349
+ * alert makes the read+insert atomic; if the lock can't be taken we still
1350
+ * proceed (the existence check alone covers the common sequential case).
1351
+ */
1352
+ let metricRefreshMutex: SemaphoreMutex | null = null;
1353
+ try {
1354
+ metricRefreshMutex = await Semaphore.lock({
1355
+ key: data.alertId.toString(),
1356
+ namespace: "AlertService.refreshAlertMetrics",
1357
+ lockTimeout: 30000,
1358
+ });
1359
+ } catch (err) {
1360
+ logger.error(
1361
+ err as Error,
1362
+ {
1363
+ projectId: alert.projectId?.toString(),
1364
+ alertId: alert.id?.toString(),
1365
+ } as LogAttributes,
1366
+ );
1367
+ }
1356
1368
 
1357
- // now we need to create new metrics for this alert - TimeToAcknowledge, TimeToResolve, AlertCount, AlertDuration
1358
- const alertStartsAt: Date =
1359
- firstAlertStateTimeline?.startsAt ||
1360
- alert.createdAt ||
1361
- OneUptimeDate.getCurrentDate();
1362
-
1363
- const alertCountMetric: Metric = new Metric();
1364
-
1365
- alertCountMetric.projectId = alert.projectId;
1366
- alertCountMetric.primaryEntityId = alert.id!;
1367
- alertCountMetric.primaryEntityType = ServiceType.Alert;
1368
- alertCountMetric.name = AlertMetricType.AlertCount;
1369
- alertCountMetric.value = 1;
1370
- alertCountMetric.attributes = {
1371
- alertId: data.alertId.toString(),
1372
- projectId: alert.projectId.toString(),
1373
- monitorId: alert.monitor?._id?.toString(),
1374
- monitorName: alert.monitor?.name?.toString(),
1375
- alertSeverityId: alert.alertSeverity?._id?.toString(),
1376
- alertSeverityName: alert.alertSeverity?.name?.toString(),
1377
- };
1378
- alertCountMetric.attributeKeys = TelemetryUtil.getAttributeKeys(
1379
- alertCountMetric.attributes,
1380
- );
1369
+ try {
1370
+ /*
1371
+ * Write-once metrics — no deletes.
1372
+ *
1373
+ * The raw Metric table (`MetricItemV3`) feeds an AggregatingMergeTree
1374
+ * materialized view (`MetricItemAggMV1m_mv`) that accumulates
1375
+ * `sumState`/`countState` per (projectId, name, primaryEntityId,
1376
+ * minute). The MV trigger fires only on INSERT — an `ALTER ... DELETE`
1377
+ * mutation on the source rolls back nothing in the MV. So the old
1378
+ * "delete every metric for this alert, then re-insert" refresh both
1379
+ * (a) inflated those rollups and (b) issued one heavy `ALTER ... DELETE`
1380
+ * mutation per state transition per alert — a mutation storm.
1381
+ *
1382
+ * Every alert metric has a single eventually-final value: AlertCount is
1383
+ * constant `value = 1`; MTTA/MTTR/duration are each fixed once the
1384
+ * relevant state transition has happened. So we load what we've already
1385
+ * emitted for this alert and insert each metric exactly once, when it
1386
+ * becomes final. Repeat refreshes that find nothing new are no-ops.
1387
+ */
1388
+ const existingMetricNames: Set<string> = new Set<string>(
1389
+ (
1390
+ await MetricService.findBy({
1391
+ query: {
1392
+ projectId: alert.projectId,
1393
+ primaryEntityId: data.alertId,
1394
+ },
1395
+ select: {
1396
+ name: true,
1397
+ },
1398
+ skip: 0,
1399
+ limit: LIMIT_PER_PROJECT,
1400
+ props: {
1401
+ isRoot: true,
1402
+ },
1403
+ })
1404
+ )
1405
+ .map((metric: Metric) => {
1406
+ return metric.name;
1407
+ })
1408
+ .filter(Boolean) as Array<string>,
1409
+ );
1381
1410
 
1382
- alertCountMetric.time = alertStartsAt;
1383
- alertCountMetric.timeUnixNano = OneUptimeDate.toUnixNano(
1384
- alertCountMetric.time,
1385
- );
1386
- alertCountMetric.metricPointType = MetricPointType.Sum;
1387
- alertCountMetric.retentionDate = alertMetricRetentionDate;
1388
-
1389
- itemsToSave.push(alertCountMetric);
1390
-
1391
- const metricType: MetricType = new MetricType();
1392
- metricType.name = alertCountMetric.name;
1393
- metricType.description = "Number of alerts created";
1394
- metricType.unit = "";
1395
- metricType.services = [];
1396
- metricTypesMap[alertCountMetric.name] = metricType;
1397
-
1398
- // is the alert acknowledged?
1399
- const isAlertAcknowledged: boolean = alertStateTimelines.some(
1400
- (timeline: AlertStateTimeline) => {
1401
- return timeline.alertState?.isAcknowledgedState;
1402
- },
1403
- );
1411
+ const itemsToSave: Array<Metric> = [];
1412
+ const metricTypesMap: Dictionary<MetricType> = {};
1404
1413
 
1405
- if (isAlertAcknowledged) {
1406
- const ackAlertStateTimeline: AlertStateTimeline | undefined =
1407
- alertStateTimelines.find((timeline: AlertStateTimeline) => {
1408
- return timeline.alertState?.isAcknowledgedState;
1409
- });
1414
+ const metricRetentionDays: number = await this.getMetricRetentionDays();
1415
+ const alertMetricRetentionDate: Date = OneUptimeDate.addRemoveDays(
1416
+ OneUptimeDate.getCurrentDate(),
1417
+ metricRetentionDays,
1418
+ );
1410
1419
 
1411
- if (ackAlertStateTimeline) {
1412
- const timeToAcknowledgeMetric: Metric = new Metric();
1420
+ // now we need to create new metrics for this alert - TimeToAcknowledge, TimeToResolve, AlertCount, AlertDuration
1421
+ const alertStartsAt: Date =
1422
+ firstAlertStateTimeline?.startsAt ||
1423
+ alert.createdAt ||
1424
+ OneUptimeDate.getCurrentDate();
1413
1425
 
1414
- timeToAcknowledgeMetric.projectId = alert.projectId;
1415
- timeToAcknowledgeMetric.primaryEntityId = alert.id!;
1416
- timeToAcknowledgeMetric.primaryEntityType = ServiceType.Alert;
1417
- timeToAcknowledgeMetric.name = AlertMetricType.TimeToAcknowledge;
1418
- timeToAcknowledgeMetric.value = OneUptimeDate.getDifferenceInSeconds(
1419
- ackAlertStateTimeline?.startsAt || OneUptimeDate.getCurrentDate(),
1420
- alertStartsAt,
1421
- );
1422
- timeToAcknowledgeMetric.attributes = {
1426
+ // register the metric type so the catalog stays complete across refreshes.
1427
+ const alertCountMetricType: MetricType = new MetricType();
1428
+ alertCountMetricType.name = AlertMetricType.AlertCount;
1429
+ alertCountMetricType.description = "Number of alerts created";
1430
+ alertCountMetricType.unit = "";
1431
+ alertCountMetricType.services = [];
1432
+ metricTypesMap[AlertMetricType.AlertCount] = alertCountMetricType;
1433
+
1434
+ // write-once: AlertCount is a constant value=1 keyed by the alert.
1435
+ if (!existingMetricNames.has(AlertMetricType.AlertCount)) {
1436
+ const alertCountMetric: Metric = new Metric();
1437
+
1438
+ alertCountMetric.projectId = alert.projectId;
1439
+ alertCountMetric.primaryEntityId = alert.id!;
1440
+ alertCountMetric.primaryEntityType = ServiceType.Alert;
1441
+ alertCountMetric.name = AlertMetricType.AlertCount;
1442
+ alertCountMetric.value = 1;
1443
+ alertCountMetric.attributes = {
1423
1444
  alertId: data.alertId.toString(),
1424
1445
  projectId: alert.projectId.toString(),
1425
1446
  monitorId: alert.monitor?._id?.toString(),
@@ -1427,152 +1448,231 @@ ${alertSeverity.name}
1427
1448
  alertSeverityId: alert.alertSeverity?._id?.toString(),
1428
1449
  alertSeverityName: alert.alertSeverity?.name?.toString(),
1429
1450
  };
1430
- timeToAcknowledgeMetric.attributeKeys = TelemetryUtil.getAttributeKeys(
1431
- timeToAcknowledgeMetric.attributes,
1451
+ alertCountMetric.attributeKeys = TelemetryUtil.getAttributeKeys(
1452
+ alertCountMetric.attributes,
1432
1453
  );
1433
1454
 
1434
- timeToAcknowledgeMetric.time =
1435
- ackAlertStateTimeline?.startsAt ||
1436
- alert.createdAt ||
1437
- OneUptimeDate.getCurrentDate();
1438
- timeToAcknowledgeMetric.timeUnixNano = OneUptimeDate.toUnixNano(
1439
- timeToAcknowledgeMetric.time,
1455
+ alertCountMetric.time = alertStartsAt;
1456
+ alertCountMetric.timeUnixNano = OneUptimeDate.toUnixNano(
1457
+ alertCountMetric.time,
1440
1458
  );
1441
- timeToAcknowledgeMetric.metricPointType = MetricPointType.Sum;
1442
- timeToAcknowledgeMetric.retentionDate = alertMetricRetentionDate;
1459
+ alertCountMetric.metricPointType = MetricPointType.Sum;
1460
+ alertCountMetric.retentionDate = alertMetricRetentionDate;
1443
1461
 
1444
- itemsToSave.push(timeToAcknowledgeMetric);
1462
+ itemsToSave.push(alertCountMetric);
1463
+ }
1445
1464
 
1446
- const metricType: MetricType = new MetricType();
1447
- metricType.name = timeToAcknowledgeMetric.name;
1448
- metricType.description = "Time taken to acknowledge the alert";
1449
- metricType.unit = "seconds";
1450
- metricTypesMap[timeToAcknowledgeMetric.name] = metricType;
1465
+ // is the alert acknowledged?
1466
+ const isAlertAcknowledged: boolean = alertStateTimelines.some(
1467
+ (timeline: AlertStateTimeline) => {
1468
+ return timeline.alertState?.isAcknowledgedState;
1469
+ },
1470
+ );
1471
+
1472
+ if (isAlertAcknowledged) {
1473
+ const ackAlertStateTimeline: AlertStateTimeline | undefined =
1474
+ alertStateTimelines.find((timeline: AlertStateTimeline) => {
1475
+ return timeline.alertState?.isAcknowledgedState;
1476
+ });
1477
+
1478
+ if (ackAlertStateTimeline) {
1479
+ // register the metric type so the catalog stays complete across refreshes.
1480
+ const metricType: MetricType = new MetricType();
1481
+ metricType.name = AlertMetricType.TimeToAcknowledge;
1482
+ metricType.description = "Time taken to acknowledge the alert";
1483
+ metricType.unit = "seconds";
1484
+ metricTypesMap[AlertMetricType.TimeToAcknowledge] = metricType;
1485
+
1486
+ // write-once: MTTA is fixed once the alert is first acknowledged.
1487
+ if (!existingMetricNames.has(AlertMetricType.TimeToAcknowledge)) {
1488
+ const timeToAcknowledgeMetric: Metric = new Metric();
1489
+
1490
+ timeToAcknowledgeMetric.projectId = alert.projectId;
1491
+ timeToAcknowledgeMetric.primaryEntityId = alert.id!;
1492
+ timeToAcknowledgeMetric.primaryEntityType = ServiceType.Alert;
1493
+ timeToAcknowledgeMetric.name = AlertMetricType.TimeToAcknowledge;
1494
+ timeToAcknowledgeMetric.value =
1495
+ OneUptimeDate.getDifferenceInSeconds(
1496
+ ackAlertStateTimeline?.startsAt ||
1497
+ OneUptimeDate.getCurrentDate(),
1498
+ alertStartsAt,
1499
+ );
1500
+ timeToAcknowledgeMetric.attributes = {
1501
+ alertId: data.alertId.toString(),
1502
+ projectId: alert.projectId.toString(),
1503
+ monitorId: alert.monitor?._id?.toString(),
1504
+ monitorName: alert.monitor?.name?.toString(),
1505
+ alertSeverityId: alert.alertSeverity?._id?.toString(),
1506
+ alertSeverityName: alert.alertSeverity?.name?.toString(),
1507
+ };
1508
+ timeToAcknowledgeMetric.attributeKeys =
1509
+ TelemetryUtil.getAttributeKeys(
1510
+ timeToAcknowledgeMetric.attributes,
1511
+ );
1512
+
1513
+ timeToAcknowledgeMetric.time =
1514
+ ackAlertStateTimeline?.startsAt ||
1515
+ alert.createdAt ||
1516
+ OneUptimeDate.getCurrentDate();
1517
+ timeToAcknowledgeMetric.timeUnixNano = OneUptimeDate.toUnixNano(
1518
+ timeToAcknowledgeMetric.time,
1519
+ );
1520
+ timeToAcknowledgeMetric.metricPointType = MetricPointType.Sum;
1521
+ timeToAcknowledgeMetric.retentionDate = alertMetricRetentionDate;
1522
+
1523
+ itemsToSave.push(timeToAcknowledgeMetric);
1524
+ }
1525
+ }
1451
1526
  }
1452
- }
1453
1527
 
1454
- // time to resolve
1455
- const isAlertResolved: boolean = alertStateTimelines.some(
1456
- (timeline: AlertStateTimeline) => {
1457
- return timeline.alertState?.isResolvedState;
1458
- },
1459
- );
1528
+ // time to resolve
1529
+ const isAlertResolved: boolean = alertStateTimelines.some(
1530
+ (timeline: AlertStateTimeline) => {
1531
+ return timeline.alertState?.isResolvedState;
1532
+ },
1533
+ );
1460
1534
 
1461
- if (isAlertResolved) {
1462
1535
  const resolvedAlertStateTimeline: AlertStateTimeline | undefined =
1463
1536
  alertStateTimelines.find((timeline: AlertStateTimeline) => {
1464
1537
  return timeline.alertState?.isResolvedState;
1465
1538
  });
1466
1539
 
1467
- if (resolvedAlertStateTimeline) {
1468
- const timeToResolveMetric: Metric = new Metric();
1469
-
1470
- timeToResolveMetric.projectId = alert.projectId;
1471
- timeToResolveMetric.primaryEntityId = alert.id!;
1472
- timeToResolveMetric.primaryEntityType = ServiceType.Alert;
1473
- timeToResolveMetric.name = AlertMetricType.TimeToResolve;
1474
- timeToResolveMetric.value = OneUptimeDate.getDifferenceInSeconds(
1475
- resolvedAlertStateTimeline?.startsAt ||
1476
- OneUptimeDate.getCurrentDate(),
1477
- alertStartsAt,
1478
- );
1479
- timeToResolveMetric.attributes = {
1480
- alertId: data.alertId.toString(),
1481
- projectId: alert.projectId.toString(),
1482
- monitorId: alert.monitor?._id?.toString(),
1483
- monitorName: alert.monitor?.name?.toString(),
1484
- alertSeverityId: alert.alertSeverity?._id?.toString(),
1485
- alertSeverityName: alert.alertSeverity?.name?.toString(),
1486
- };
1487
- timeToResolveMetric.attributeKeys = TelemetryUtil.getAttributeKeys(
1488
- timeToResolveMetric.attributes,
1489
- );
1490
-
1491
- timeToResolveMetric.time =
1492
- resolvedAlertStateTimeline?.startsAt ||
1493
- alert.createdAt ||
1494
- OneUptimeDate.getCurrentDate();
1495
- timeToResolveMetric.timeUnixNano = OneUptimeDate.toUnixNano(
1496
- timeToResolveMetric.time,
1497
- );
1498
- timeToResolveMetric.metricPointType = MetricPointType.Sum;
1499
- timeToResolveMetric.retentionDate = alertMetricRetentionDate;
1500
-
1501
- itemsToSave.push(timeToResolveMetric);
1502
-
1540
+ if (isAlertResolved && resolvedAlertStateTimeline) {
1541
+ // register the metric type so the catalog stays complete across refreshes.
1503
1542
  const metricType: MetricType = new MetricType();
1504
- metricType.name = timeToResolveMetric.name;
1543
+ metricType.name = AlertMetricType.TimeToResolve;
1505
1544
  metricType.description = "Time taken to resolve the alert";
1506
1545
  metricType.unit = "seconds";
1507
- metricTypesMap[timeToResolveMetric.name] = metricType;
1508
- }
1509
- }
1546
+ metricTypesMap[AlertMetricType.TimeToResolve] = metricType;
1547
+
1548
+ // write-once: MTTR is fixed once the alert is first resolved.
1549
+ if (!existingMetricNames.has(AlertMetricType.TimeToResolve)) {
1550
+ const timeToResolveMetric: Metric = new Metric();
1551
+
1552
+ timeToResolveMetric.projectId = alert.projectId;
1553
+ timeToResolveMetric.primaryEntityId = alert.id!;
1554
+ timeToResolveMetric.primaryEntityType = ServiceType.Alert;
1555
+ timeToResolveMetric.name = AlertMetricType.TimeToResolve;
1556
+ timeToResolveMetric.value = OneUptimeDate.getDifferenceInSeconds(
1557
+ resolvedAlertStateTimeline?.startsAt ||
1558
+ OneUptimeDate.getCurrentDate(),
1559
+ alertStartsAt,
1560
+ );
1561
+ timeToResolveMetric.attributes = {
1562
+ alertId: data.alertId.toString(),
1563
+ projectId: alert.projectId.toString(),
1564
+ monitorId: alert.monitor?._id?.toString(),
1565
+ monitorName: alert.monitor?.name?.toString(),
1566
+ alertSeverityId: alert.alertSeverity?._id?.toString(),
1567
+ alertSeverityName: alert.alertSeverity?.name?.toString(),
1568
+ };
1569
+ timeToResolveMetric.attributeKeys = TelemetryUtil.getAttributeKeys(
1570
+ timeToResolveMetric.attributes,
1571
+ );
1510
1572
 
1511
- // alert duration
1512
- const alertDurationMetric: Metric = new Metric();
1573
+ timeToResolveMetric.time =
1574
+ resolvedAlertStateTimeline?.startsAt ||
1575
+ alert.createdAt ||
1576
+ OneUptimeDate.getCurrentDate();
1577
+ timeToResolveMetric.timeUnixNano = OneUptimeDate.toUnixNano(
1578
+ timeToResolveMetric.time,
1579
+ );
1580
+ timeToResolveMetric.metricPointType = MetricPointType.Sum;
1581
+ timeToResolveMetric.retentionDate = alertMetricRetentionDate;
1513
1582
 
1514
- const lastAlertStateTimeline: AlertStateTimeline | undefined =
1515
- alertStateTimelines[alertStateTimelines.length - 1];
1583
+ itemsToSave.push(timeToResolveMetric);
1584
+ }
1585
+ }
1516
1586
 
1517
- if (lastAlertStateTimeline) {
1518
- const alertEndsAt: Date =
1519
- lastAlertStateTimeline.startsAt || OneUptimeDate.getCurrentDate();
1587
+ /*
1588
+ * Alert duration — write-once, finalized at resolution.
1589
+ *
1590
+ * Previously recomputed as (latest state transition − start) on every
1591
+ * refresh, which grew for open alerts and required delete + re-insert.
1592
+ * With no deletes we emit a single final duration once the alert reaches
1593
+ * a resolved state: (first resolution − start). An alert that is never
1594
+ * resolved has no duration row (its lifetime is not yet final).
1595
+ */
1596
+ if (isAlertResolved && resolvedAlertStateTimeline) {
1597
+ // register the metric type so the catalog stays complete across refreshes.
1598
+ const metricType: MetricType = new MetricType();
1599
+ metricType.name = AlertMetricType.AlertDuration;
1600
+ metricType.description = "Duration of the alert";
1601
+ metricType.unit = "seconds";
1602
+ metricTypesMap[AlertMetricType.AlertDuration] = metricType;
1603
+
1604
+ if (!existingMetricNames.has(AlertMetricType.AlertDuration)) {
1605
+ const alertEndsAt: Date =
1606
+ resolvedAlertStateTimeline.startsAt ||
1607
+ OneUptimeDate.getCurrentDate();
1608
+
1609
+ const alertDurationMetric: Metric = new Metric();
1610
+
1611
+ alertDurationMetric.projectId = alert.projectId;
1612
+ alertDurationMetric.primaryEntityId = alert.id!;
1613
+ alertDurationMetric.primaryEntityType = ServiceType.Alert;
1614
+ alertDurationMetric.name = AlertMetricType.AlertDuration;
1615
+ alertDurationMetric.value = OneUptimeDate.getDifferenceInSeconds(
1616
+ alertEndsAt,
1617
+ alertStartsAt,
1618
+ );
1619
+ alertDurationMetric.attributes = {
1620
+ alertId: data.alertId.toString(),
1621
+ projectId: alert.projectId.toString(),
1622
+ monitorId: alert.monitor?._id?.toString(),
1623
+ monitorName: alert.monitor?.name?.toString(),
1624
+ alertSeverityId: alert.alertSeverity?._id?.toString(),
1625
+ alertSeverityName: alert.alertSeverity?.name?.toString(),
1626
+ };
1627
+ alertDurationMetric.attributeKeys = TelemetryUtil.getAttributeKeys(
1628
+ alertDurationMetric.attributes,
1629
+ );
1520
1630
 
1521
- alertDurationMetric.projectId = alert.projectId;
1522
- alertDurationMetric.primaryEntityId = alert.id!;
1523
- alertDurationMetric.primaryEntityType = ServiceType.Alert;
1524
- alertDurationMetric.name = AlertMetricType.AlertDuration;
1525
- alertDurationMetric.value = OneUptimeDate.getDifferenceInSeconds(
1526
- alertEndsAt,
1527
- alertStartsAt,
1528
- );
1529
- alertDurationMetric.attributes = {
1530
- alertId: data.alertId.toString(),
1531
- projectId: alert.projectId.toString(),
1532
- monitorId: alert.monitor?._id?.toString(),
1533
- monitorName: alert.monitor?.name?.toString(),
1534
- alertSeverityId: alert.alertSeverity?._id?.toString(),
1535
- alertSeverityName: alert.alertSeverity?.name?.toString(),
1536
- };
1537
- alertDurationMetric.attributeKeys = TelemetryUtil.getAttributeKeys(
1538
- alertDurationMetric.attributes,
1539
- );
1631
+ alertDurationMetric.time = alertEndsAt;
1632
+ alertDurationMetric.timeUnixNano = OneUptimeDate.toUnixNano(
1633
+ alertDurationMetric.time,
1634
+ );
1635
+ alertDurationMetric.metricPointType = MetricPointType.Sum;
1636
+ alertDurationMetric.retentionDate = alertMetricRetentionDate;
1540
1637
 
1541
- alertDurationMetric.time =
1542
- lastAlertStateTimeline?.startsAt ||
1543
- alert.createdAt ||
1544
- OneUptimeDate.getCurrentDate();
1545
- alertDurationMetric.timeUnixNano = OneUptimeDate.toUnixNano(
1546
- alertDurationMetric.time,
1547
- );
1548
- alertDurationMetric.metricPointType = MetricPointType.Sum;
1549
- alertDurationMetric.retentionDate = alertMetricRetentionDate;
1638
+ itemsToSave.push(alertDurationMetric);
1639
+ }
1640
+ }
1550
1641
 
1551
- itemsToSave.push(alertDurationMetric);
1642
+ // write-once: a refresh that finds nothing new inserts nothing.
1643
+ if (itemsToSave.length > 0) {
1644
+ await MetricService.createMany({
1645
+ items: itemsToSave,
1646
+ props: {
1647
+ isRoot: true,
1648
+ },
1649
+ });
1650
+ }
1552
1651
 
1553
- const metricType: MetricType = new MetricType();
1554
- metricType.name = alertDurationMetric.name;
1555
- metricType.description = "Duration of the alert";
1556
- metricType.unit = "seconds";
1557
- metricTypesMap[alertDurationMetric.name] = metricType;
1652
+ TelemetryUtil.indexMetricNameServiceNameMap({
1653
+ metricNameServiceNameMap: metricTypesMap,
1654
+ projectId: alert.projectId,
1655
+ }).catch((err: Error) => {
1656
+ logger.error(err, {
1657
+ projectId: alert.projectId?.toString(),
1658
+ alertId: alert.id?.toString(),
1659
+ } as LogAttributes);
1660
+ });
1661
+ } finally {
1662
+ if (metricRefreshMutex) {
1663
+ try {
1664
+ await Semaphore.release(metricRefreshMutex);
1665
+ } catch (err) {
1666
+ logger.error(
1667
+ err as Error,
1668
+ {
1669
+ projectId: alert.projectId?.toString(),
1670
+ alertId: alert.id?.toString(),
1671
+ } as LogAttributes,
1672
+ );
1673
+ }
1674
+ }
1558
1675
  }
1559
-
1560
- await MetricService.createMany({
1561
- items: itemsToSave,
1562
- props: {
1563
- isRoot: true,
1564
- },
1565
- });
1566
-
1567
- TelemetryUtil.indexMetricNameServiceNameMap({
1568
- metricNameServiceNameMap: metricTypesMap,
1569
- projectId: alert.projectId,
1570
- }).catch((err: Error) => {
1571
- logger.error(err, {
1572
- projectId: alert.projectId?.toString(),
1573
- alertId: alert.id?.toString(),
1574
- } as LogAttributes);
1575
- });
1576
1676
  }
1577
1677
 
1578
1678
  @CaptureSpan()