@oneuptime/common 11.3.6 → 11.3.8
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/Server/Services/AlertService.ts +302 -202
- package/Server/Services/IncidentService.ts +410 -306
- package/build/dist/Server/Services/AlertService.js +255 -161
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +322 -236
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/package.json +1 -1
|
@@ -52,7 +52,7 @@ import ServiceType from "../../Types/Telemetry/ServiceType";
|
|
|
52
52
|
import OneUptimeDate from "../../Types/Date";
|
|
53
53
|
import TelemetryUtil from "../Utils/Telemetry/Telemetry";
|
|
54
54
|
import logger, { LogAttributes } from "../Utils/Logger";
|
|
55
|
-
import
|
|
55
|
+
import Semaphore, { SemaphoreMutex } from "../Infrastructure/Semaphore";
|
|
56
56
|
import IncidentFeedService from "./IncidentFeedService";
|
|
57
57
|
import IncidentSlaService from "./IncidentSlaService";
|
|
58
58
|
import { setIsPublicForMarkdownImages } from "../Utils/InlineImageAccessTokenSync";
|
|
@@ -2765,359 +2765,463 @@ ${incidentSeverity.name}
|
|
|
2765
2765
|
incidentStateTimelines[0];
|
|
2766
2766
|
|
|
2767
2767
|
/*
|
|
2768
|
-
*
|
|
2769
|
-
*
|
|
2770
|
-
*
|
|
2771
|
-
*
|
|
2772
|
-
*
|
|
2773
|
-
*
|
|
2774
|
-
*
|
|
2775
|
-
*
|
|
2776
|
-
*
|
|
2777
|
-
*
|
|
2778
|
-
*
|
|
2779
|
-
*
|
|
2768
|
+
* Serialize concurrent refreshes for this incident across pods. The
|
|
2769
|
+
* write-once guard below is read-then-insert (findBy existing, then a
|
|
2770
|
+
* conditional createMany). Both call sites fire refresh fire-and-forget
|
|
2771
|
+
* after releasing the per-incident state-timeline mutex, so two
|
|
2772
|
+
* close-together transitions (e.g. auto-acknowledge then auto-resolve)
|
|
2773
|
+
* could interleave: both read a snapshot missing a metric and both
|
|
2774
|
+
* insert it. The Metric table is a plain MergeTree with no row collapse,
|
|
2775
|
+
* so those duplicates would be permanent and re-inflate the very Sum/Avg
|
|
2776
|
+
* widgets this change fixes. A best-effort distributed lock keyed on the
|
|
2777
|
+
* incident makes the read+insert atomic; if the lock can't be taken we
|
|
2778
|
+
* still proceed (the existence check alone covers the common sequential
|
|
2779
|
+
* case).
|
|
2780
2780
|
*/
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
metricRetentionDays,
|
|
2798
|
-
);
|
|
2781
|
+
let metricRefreshMutex: SemaphoreMutex | null = null;
|
|
2782
|
+
try {
|
|
2783
|
+
metricRefreshMutex = await Semaphore.lock({
|
|
2784
|
+
key: data.incidentId.toString(),
|
|
2785
|
+
namespace: "IncidentService.refreshIncidentMetrics",
|
|
2786
|
+
lockTimeout: 30000,
|
|
2787
|
+
});
|
|
2788
|
+
} catch (err) {
|
|
2789
|
+
logger.error(
|
|
2790
|
+
err as Error,
|
|
2791
|
+
{
|
|
2792
|
+
projectId: incident.projectId?.toString(),
|
|
2793
|
+
incidentId: incident.id?.toString(),
|
|
2794
|
+
} as LogAttributes,
|
|
2795
|
+
);
|
|
2796
|
+
}
|
|
2799
2797
|
|
|
2800
|
-
|
|
2798
|
+
try {
|
|
2799
|
+
/*
|
|
2800
|
+
* Write-once metrics — no deletes.
|
|
2801
|
+
*
|
|
2802
|
+
* The raw Metric table (`MetricItemV3`) feeds an AggregatingMergeTree
|
|
2803
|
+
* materialized view (`MetricItemAggMV1m_mv`) that accumulates
|
|
2804
|
+
* `sumState`/`countState` per (projectId, name, primaryEntityId,
|
|
2805
|
+
* minute). The MV trigger fires only on INSERT — an `ALTER ... DELETE`
|
|
2806
|
+
* mutation on the source rolls back nothing in the MV. So the old
|
|
2807
|
+
* "delete every metric for this incident, then re-insert" refresh both
|
|
2808
|
+
* (a) inflated those rollups (the Sum-of-IncidentCount widget read high)
|
|
2809
|
+
* and (b) issued one heavy `ALTER ... DELETE` mutation per state
|
|
2810
|
+
* transition per incident — a mutation storm.
|
|
2811
|
+
*
|
|
2812
|
+
* Every incident metric has a single eventually-final value:
|
|
2813
|
+
* IncidentCount is constant `value = 1`; MTTA/MTTR/duration/time-in-state
|
|
2814
|
+
* are each fixed once the relevant state transition has happened. So we
|
|
2815
|
+
* load what we've already emitted for this incident and insert each
|
|
2816
|
+
* metric exactly once, when it becomes final. Repeat refreshes that find
|
|
2817
|
+
* nothing new are pure no-ops (no insert, no mutation).
|
|
2818
|
+
*/
|
|
2819
|
+
const existingMetrics: Array<Metric> = await MetricService.findBy({
|
|
2820
|
+
query: {
|
|
2821
|
+
projectId: incident.projectId,
|
|
2822
|
+
primaryEntityId: data.incidentId,
|
|
2823
|
+
},
|
|
2824
|
+
select: {
|
|
2825
|
+
name: true,
|
|
2826
|
+
time: true,
|
|
2827
|
+
attributes: true,
|
|
2828
|
+
},
|
|
2829
|
+
skip: 0,
|
|
2830
|
+
limit: LIMIT_PER_PROJECT,
|
|
2831
|
+
props: {
|
|
2832
|
+
isRoot: true,
|
|
2833
|
+
},
|
|
2834
|
+
});
|
|
2801
2835
|
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2836
|
+
const existingMetricNames: Set<string> = new Set<string>(
|
|
2837
|
+
existingMetrics
|
|
2838
|
+
.map((metric: Metric) => {
|
|
2839
|
+
return metric.name;
|
|
2840
|
+
})
|
|
2841
|
+
.filter(Boolean) as Array<string>,
|
|
2842
|
+
);
|
|
2807
2843
|
|
|
2808
|
-
|
|
2844
|
+
/*
|
|
2845
|
+
* Identity of a TimeInState row: the state plus the instant that state
|
|
2846
|
+
* began (a state can be entered more than once over a re-opened
|
|
2847
|
+
* incident). Second granularity is robust against DateTime64 round-trip
|
|
2848
|
+
* jitter and two distinct entries into the same state within one second
|
|
2849
|
+
* never happen.
|
|
2850
|
+
*/
|
|
2851
|
+
const buildTimeInStateKey: (
|
|
2852
|
+
stateId: string | undefined,
|
|
2853
|
+
startsAt: Date | undefined,
|
|
2854
|
+
) => string = (
|
|
2855
|
+
stateId: string | undefined,
|
|
2856
|
+
startsAt: Date | undefined,
|
|
2857
|
+
): string => {
|
|
2858
|
+
return `${stateId || ""}|${
|
|
2859
|
+
startsAt ? OneUptimeDate.toUnixTimestamp(startsAt) : ""
|
|
2860
|
+
}`;
|
|
2861
|
+
};
|
|
2809
2862
|
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
*/
|
|
2815
|
-
const baseMetricAttributes: JSONObject = {
|
|
2816
|
-
incidentId: data.incidentId.toString(),
|
|
2817
|
-
projectId: incident.projectId.toString(),
|
|
2818
|
-
monitorIds: (
|
|
2819
|
-
incident.monitors
|
|
2820
|
-
?.map((monitor: Monitor) => {
|
|
2821
|
-
return monitor._id?.toString();
|
|
2822
|
-
})
|
|
2823
|
-
.filter(Boolean) || []
|
|
2824
|
-
).join(", "),
|
|
2825
|
-
monitorNames: (
|
|
2826
|
-
incident.monitors
|
|
2827
|
-
?.map((monitor: Monitor) => {
|
|
2828
|
-
return monitor.name?.toString();
|
|
2863
|
+
const existingTimeInStateKeys: Set<string> = new Set<string>(
|
|
2864
|
+
existingMetrics
|
|
2865
|
+
.filter((metric: Metric) => {
|
|
2866
|
+
return metric.name === IncidentMetricType.TimeInState;
|
|
2829
2867
|
})
|
|
2830
|
-
.
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
ownerTeamNames: ownerTeamNames.join(", "),
|
|
2838
|
-
};
|
|
2868
|
+
.map((metric: Metric) => {
|
|
2869
|
+
return buildTimeInStateKey(
|
|
2870
|
+
metric.attributes?.["incidentStateId"] as string | undefined,
|
|
2871
|
+
metric.time,
|
|
2872
|
+
);
|
|
2873
|
+
}),
|
|
2874
|
+
);
|
|
2839
2875
|
|
|
2840
|
-
|
|
2841
|
-
* Only emit IncidentCount on the very first refresh (i.e. when no
|
|
2842
|
-
* existing IncidentCount row is present for this primaryEntityId). See
|
|
2843
|
-
* the delete comment above — emitting it on every refresh would
|
|
2844
|
-
* accumulate phantom `sumState` entries in the MV that ALTER
|
|
2845
|
-
* DELETE can't undo. By keeping the original row alive and never
|
|
2846
|
-
* re-emitting, the dashboard Sum stays equal to the true count of
|
|
2847
|
-
* distinct incidents.
|
|
2848
|
-
*/
|
|
2849
|
-
const incidentCountMetricExists: boolean = await MetricService.existsBy({
|
|
2850
|
-
query: {
|
|
2851
|
-
projectId: incident.projectId,
|
|
2852
|
-
primaryEntityId: data.incidentId,
|
|
2853
|
-
name: IncidentMetricType.IncidentCount,
|
|
2854
|
-
},
|
|
2855
|
-
props: {
|
|
2856
|
-
isRoot: true,
|
|
2857
|
-
},
|
|
2858
|
-
});
|
|
2876
|
+
const itemsToSave: Array<Metric> = [];
|
|
2859
2877
|
|
|
2860
|
-
|
|
2861
|
-
const
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
incidentCountMetric.primaryEntityId = incident.id!;
|
|
2865
|
-
incidentCountMetric.primaryEntityType = ServiceType.Incident;
|
|
2866
|
-
incidentCountMetric.name = IncidentMetricType.IncidentCount;
|
|
2867
|
-
incidentCountMetric.value = 1;
|
|
2868
|
-
incidentCountMetric.attributes = { ...baseMetricAttributes };
|
|
2869
|
-
incidentCountMetric.attributeKeys = TelemetryUtil.getAttributeKeys(
|
|
2870
|
-
incidentCountMetric.attributes,
|
|
2878
|
+
const metricRetentionDays: number = await this.getMetricRetentionDays();
|
|
2879
|
+
const incidentMetricRetentionDate: Date = OneUptimeDate.addRemoveDays(
|
|
2880
|
+
OneUptimeDate.getCurrentDate(),
|
|
2881
|
+
metricRetentionDays,
|
|
2871
2882
|
);
|
|
2872
2883
|
|
|
2873
|
-
|
|
2874
|
-
incidentCountMetric.timeUnixNano = OneUptimeDate.toUnixNano(
|
|
2875
|
-
incidentCountMetric.time,
|
|
2876
|
-
);
|
|
2877
|
-
incidentCountMetric.metricPointType = MetricPointType.Sum;
|
|
2878
|
-
incidentCountMetric.retentionDate = incidentMetricRetentionDate;
|
|
2884
|
+
// now we need to create new metrics for this incident - TimeToAcknowledge, TimeToResolve, IncidentCount, IncidentDuration
|
|
2879
2885
|
|
|
2880
|
-
|
|
2881
|
-
|
|
2886
|
+
const incidentStartsAt: Date =
|
|
2887
|
+
firstIncidentStateTimeline?.startsAt ||
|
|
2888
|
+
incident.declaredAt ||
|
|
2889
|
+
incident.createdAt ||
|
|
2890
|
+
OneUptimeDate.getCurrentDate();
|
|
2882
2891
|
|
|
2883
|
-
|
|
2884
|
-
const metricType: MetricType = new MetricType();
|
|
2885
|
-
metricType.name = IncidentMetricType.IncidentCount;
|
|
2886
|
-
metricType.description = "Number of incidents created";
|
|
2887
|
-
metricType.unit = "";
|
|
2888
|
-
metricType.services = [];
|
|
2892
|
+
const metricTypesMap: Dictionary<MetricType> = {};
|
|
2889
2893
|
|
|
2890
|
-
|
|
2894
|
+
/*
|
|
2895
|
+
* common attributes shared by all incident metrics
|
|
2896
|
+
* All values must be strings for ClickHouse Map(String, String) storage.
|
|
2897
|
+
* Arrays are joined as comma-separated strings.
|
|
2898
|
+
*/
|
|
2899
|
+
const baseMetricAttributes: JSONObject = {
|
|
2900
|
+
incidentId: data.incidentId.toString(),
|
|
2901
|
+
projectId: incident.projectId.toString(),
|
|
2902
|
+
monitorIds: (
|
|
2903
|
+
incident.monitors
|
|
2904
|
+
?.map((monitor: Monitor) => {
|
|
2905
|
+
return monitor._id?.toString();
|
|
2906
|
+
})
|
|
2907
|
+
.filter(Boolean) || []
|
|
2908
|
+
).join(", "),
|
|
2909
|
+
monitorNames: (
|
|
2910
|
+
incident.monitors
|
|
2911
|
+
?.map((monitor: Monitor) => {
|
|
2912
|
+
return monitor.name?.toString();
|
|
2913
|
+
})
|
|
2914
|
+
.filter(Boolean) || []
|
|
2915
|
+
).join(", "),
|
|
2916
|
+
incidentSeverityId: incident.incidentSeverity?._id?.toString(),
|
|
2917
|
+
incidentSeverityName: incident.incidentSeverity?.name?.toString(),
|
|
2918
|
+
ownerUserIds: ownerUserIds.join(", "),
|
|
2919
|
+
ownerUserNames: ownerUserNames.join(", "),
|
|
2920
|
+
ownerTeamIds: ownerTeamIds.join(", "),
|
|
2921
|
+
ownerTeamNames: ownerTeamNames.join(", "),
|
|
2922
|
+
};
|
|
2891
2923
|
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2924
|
+
/*
|
|
2925
|
+
* Only emit IncidentCount on the very first refresh (i.e. when no
|
|
2926
|
+
* existing IncidentCount row is present for this primaryEntityId). See
|
|
2927
|
+
* the write-once note above — emitting it on every refresh would
|
|
2928
|
+
* accumulate phantom `sumState` entries in the MV. By keeping the
|
|
2929
|
+
* original row alive and never re-emitting, the dashboard Sum stays
|
|
2930
|
+
* equal to the true count of distinct incidents.
|
|
2931
|
+
*/
|
|
2932
|
+
if (!existingMetricNames.has(IncidentMetricType.IncidentCount)) {
|
|
2933
|
+
const incidentCountMetric: Metric = new Metric();
|
|
2934
|
+
|
|
2935
|
+
incidentCountMetric.projectId = incident.projectId;
|
|
2936
|
+
incidentCountMetric.primaryEntityId = incident.id!;
|
|
2937
|
+
incidentCountMetric.primaryEntityType = ServiceType.Incident;
|
|
2938
|
+
incidentCountMetric.name = IncidentMetricType.IncidentCount;
|
|
2939
|
+
incidentCountMetric.value = 1;
|
|
2940
|
+
incidentCountMetric.attributes = { ...baseMetricAttributes };
|
|
2941
|
+
incidentCountMetric.attributeKeys = TelemetryUtil.getAttributeKeys(
|
|
2942
|
+
incidentCountMetric.attributes,
|
|
2943
|
+
);
|
|
2898
2944
|
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2945
|
+
incidentCountMetric.time = incidentStartsAt;
|
|
2946
|
+
incidentCountMetric.timeUnixNano = OneUptimeDate.toUnixNano(
|
|
2947
|
+
incidentCountMetric.time,
|
|
2948
|
+
);
|
|
2949
|
+
incidentCountMetric.metricPointType = MetricPointType.Sum;
|
|
2950
|
+
incidentCountMetric.retentionDate = incidentMetricRetentionDate;
|
|
2904
2951
|
|
|
2905
|
-
|
|
2906
|
-
|
|
2952
|
+
itemsToSave.push(incidentCountMetric);
|
|
2953
|
+
}
|
|
2907
2954
|
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
incidentStartsAt,
|
|
2915
|
-
);
|
|
2916
|
-
timeToAcknowledgeMetric.attributes = { ...baseMetricAttributes };
|
|
2917
|
-
timeToAcknowledgeMetric.attributeKeys = TelemetryUtil.getAttributeKeys(
|
|
2918
|
-
timeToAcknowledgeMetric.attributes,
|
|
2919
|
-
);
|
|
2955
|
+
// Always register the metric type so it shows up in the type catalog.
|
|
2956
|
+
const metricType: MetricType = new MetricType();
|
|
2957
|
+
metricType.name = IncidentMetricType.IncidentCount;
|
|
2958
|
+
metricType.description = "Number of incidents created";
|
|
2959
|
+
metricType.unit = "";
|
|
2960
|
+
metricType.services = [];
|
|
2920
2961
|
|
|
2921
|
-
|
|
2922
|
-
ackIncidentStateTimeline?.startsAt ||
|
|
2923
|
-
incident.declaredAt ||
|
|
2924
|
-
incident.createdAt ||
|
|
2925
|
-
OneUptimeDate.getCurrentDate();
|
|
2926
|
-
timeToAcknowledgeMetric.timeUnixNano = OneUptimeDate.toUnixNano(
|
|
2927
|
-
timeToAcknowledgeMetric.time,
|
|
2928
|
-
);
|
|
2929
|
-
timeToAcknowledgeMetric.metricPointType = MetricPointType.Sum;
|
|
2930
|
-
timeToAcknowledgeMetric.retentionDate = incidentMetricRetentionDate;
|
|
2962
|
+
metricTypesMap[IncidentMetricType.IncidentCount] = metricType;
|
|
2931
2963
|
|
|
2932
|
-
|
|
2964
|
+
// is the incident acknowledged?
|
|
2965
|
+
const isIncidentAcknowledged: boolean = incidentStateTimelines.some(
|
|
2966
|
+
(timeline: IncidentStateTimeline) => {
|
|
2967
|
+
return timeline.incidentState?.isAcknowledgedState;
|
|
2968
|
+
},
|
|
2969
|
+
);
|
|
2933
2970
|
|
|
2934
|
-
|
|
2935
|
-
const
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2971
|
+
if (isIncidentAcknowledged) {
|
|
2972
|
+
const ackIncidentStateTimeline: IncidentStateTimeline | undefined =
|
|
2973
|
+
incidentStateTimelines.find((timeline: IncidentStateTimeline) => {
|
|
2974
|
+
return timeline.incidentState?.isAcknowledgedState;
|
|
2975
|
+
});
|
|
2939
2976
|
|
|
2940
|
-
|
|
2941
|
-
|
|
2977
|
+
if (ackIncidentStateTimeline) {
|
|
2978
|
+
// register the metric type so the catalog stays complete across refreshes.
|
|
2979
|
+
const metricType: MetricType = new MetricType();
|
|
2980
|
+
metricType.name = IncidentMetricType.TimeToAcknowledge;
|
|
2981
|
+
metricType.description = "Time taken to acknowledge the incident";
|
|
2982
|
+
metricType.unit = "seconds";
|
|
2983
|
+
metricTypesMap[IncidentMetricType.TimeToAcknowledge] = metricType;
|
|
2984
|
+
|
|
2985
|
+
// write-once: MTTA is fixed once the incident is first acknowledged.
|
|
2986
|
+
if (!existingMetricNames.has(IncidentMetricType.TimeToAcknowledge)) {
|
|
2987
|
+
const timeToAcknowledgeMetric: Metric = new Metric();
|
|
2988
|
+
|
|
2989
|
+
timeToAcknowledgeMetric.projectId = incident.projectId;
|
|
2990
|
+
timeToAcknowledgeMetric.primaryEntityId = incident.id!;
|
|
2991
|
+
timeToAcknowledgeMetric.primaryEntityType = ServiceType.Incident;
|
|
2992
|
+
timeToAcknowledgeMetric.name = IncidentMetricType.TimeToAcknowledge;
|
|
2993
|
+
timeToAcknowledgeMetric.value =
|
|
2994
|
+
OneUptimeDate.getDifferenceInSeconds(
|
|
2995
|
+
ackIncidentStateTimeline?.startsAt ||
|
|
2996
|
+
OneUptimeDate.getCurrentDate(),
|
|
2997
|
+
incidentStartsAt,
|
|
2998
|
+
);
|
|
2999
|
+
timeToAcknowledgeMetric.attributes = { ...baseMetricAttributes };
|
|
3000
|
+
timeToAcknowledgeMetric.attributeKeys =
|
|
3001
|
+
TelemetryUtil.getAttributeKeys(
|
|
3002
|
+
timeToAcknowledgeMetric.attributes,
|
|
3003
|
+
);
|
|
3004
|
+
|
|
3005
|
+
timeToAcknowledgeMetric.time =
|
|
3006
|
+
ackIncidentStateTimeline?.startsAt ||
|
|
3007
|
+
incident.declaredAt ||
|
|
3008
|
+
incident.createdAt ||
|
|
3009
|
+
OneUptimeDate.getCurrentDate();
|
|
3010
|
+
timeToAcknowledgeMetric.timeUnixNano = OneUptimeDate.toUnixNano(
|
|
3011
|
+
timeToAcknowledgeMetric.time,
|
|
3012
|
+
);
|
|
3013
|
+
timeToAcknowledgeMetric.metricPointType = MetricPointType.Sum;
|
|
3014
|
+
timeToAcknowledgeMetric.retentionDate = incidentMetricRetentionDate;
|
|
3015
|
+
|
|
3016
|
+
itemsToSave.push(timeToAcknowledgeMetric);
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
2942
3019
|
}
|
|
2943
|
-
}
|
|
2944
3020
|
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
3021
|
+
// time to resolve
|
|
3022
|
+
const isIncidentResolved: boolean = incidentStateTimelines.some(
|
|
3023
|
+
(timeline: IncidentStateTimeline) => {
|
|
3024
|
+
return timeline.incidentState?.isResolvedState;
|
|
3025
|
+
},
|
|
3026
|
+
);
|
|
2951
3027
|
|
|
2952
|
-
if (isIncidentResolved) {
|
|
2953
3028
|
const resolvedIncidentStateTimeline: IncidentStateTimeline | undefined =
|
|
2954
3029
|
incidentStateTimelines.find((timeline: IncidentStateTimeline) => {
|
|
2955
3030
|
return timeline.incidentState?.isResolvedState;
|
|
2956
3031
|
});
|
|
2957
3032
|
|
|
2958
|
-
if (resolvedIncidentStateTimeline) {
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
timeToResolveMetric.projectId = incident.projectId;
|
|
2962
|
-
timeToResolveMetric.primaryEntityId = incident.id!;
|
|
2963
|
-
timeToResolveMetric.primaryEntityType = ServiceType.Incident;
|
|
2964
|
-
timeToResolveMetric.name = IncidentMetricType.TimeToResolve;
|
|
2965
|
-
timeToResolveMetric.value = OneUptimeDate.getDifferenceInSeconds(
|
|
2966
|
-
resolvedIncidentStateTimeline?.startsAt ||
|
|
2967
|
-
OneUptimeDate.getCurrentDate(),
|
|
2968
|
-
incidentStartsAt,
|
|
2969
|
-
);
|
|
2970
|
-
timeToResolveMetric.attributes = { ...baseMetricAttributes };
|
|
2971
|
-
timeToResolveMetric.attributeKeys = TelemetryUtil.getAttributeKeys(
|
|
2972
|
-
timeToResolveMetric.attributes,
|
|
2973
|
-
);
|
|
2974
|
-
|
|
2975
|
-
timeToResolveMetric.time =
|
|
2976
|
-
resolvedIncidentStateTimeline?.startsAt ||
|
|
2977
|
-
incident.declaredAt ||
|
|
2978
|
-
incident.createdAt ||
|
|
2979
|
-
OneUptimeDate.getCurrentDate();
|
|
2980
|
-
timeToResolveMetric.timeUnixNano = OneUptimeDate.toUnixNano(
|
|
2981
|
-
timeToResolveMetric.time,
|
|
2982
|
-
);
|
|
2983
|
-
timeToResolveMetric.metricPointType = MetricPointType.Sum;
|
|
2984
|
-
timeToResolveMetric.retentionDate = incidentMetricRetentionDate;
|
|
2985
|
-
|
|
2986
|
-
itemsToSave.push(timeToResolveMetric);
|
|
2987
|
-
|
|
2988
|
-
// add metric type for this to map.
|
|
3033
|
+
if (isIncidentResolved && resolvedIncidentStateTimeline) {
|
|
3034
|
+
// register the metric type so the catalog stays complete across refreshes.
|
|
2989
3035
|
const metricType: MetricType = new MetricType();
|
|
2990
|
-
metricType.name =
|
|
3036
|
+
metricType.name = IncidentMetricType.TimeToResolve;
|
|
2991
3037
|
metricType.description = "Time taken to resolve the incident";
|
|
2992
3038
|
metricType.unit = "seconds";
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3039
|
+
metricTypesMap[IncidentMetricType.TimeToResolve] = metricType;
|
|
3040
|
+
|
|
3041
|
+
// write-once: MTTR is fixed once the incident is first resolved.
|
|
3042
|
+
if (!existingMetricNames.has(IncidentMetricType.TimeToResolve)) {
|
|
3043
|
+
const timeToResolveMetric: Metric = new Metric();
|
|
3044
|
+
|
|
3045
|
+
timeToResolveMetric.projectId = incident.projectId;
|
|
3046
|
+
timeToResolveMetric.primaryEntityId = incident.id!;
|
|
3047
|
+
timeToResolveMetric.primaryEntityType = ServiceType.Incident;
|
|
3048
|
+
timeToResolveMetric.name = IncidentMetricType.TimeToResolve;
|
|
3049
|
+
timeToResolveMetric.value = OneUptimeDate.getDifferenceInSeconds(
|
|
3050
|
+
resolvedIncidentStateTimeline?.startsAt ||
|
|
3051
|
+
OneUptimeDate.getCurrentDate(),
|
|
3052
|
+
incidentStartsAt,
|
|
3053
|
+
);
|
|
3054
|
+
timeToResolveMetric.attributes = { ...baseMetricAttributes };
|
|
3055
|
+
timeToResolveMetric.attributeKeys = TelemetryUtil.getAttributeKeys(
|
|
3056
|
+
timeToResolveMetric.attributes,
|
|
3057
|
+
);
|
|
3001
3058
|
|
|
3002
|
-
|
|
3003
|
-
|
|
3059
|
+
timeToResolveMetric.time =
|
|
3060
|
+
resolvedIncidentStateTimeline?.startsAt ||
|
|
3061
|
+
incident.declaredAt ||
|
|
3062
|
+
incident.createdAt ||
|
|
3063
|
+
OneUptimeDate.getCurrentDate();
|
|
3064
|
+
timeToResolveMetric.timeUnixNano = OneUptimeDate.toUnixNano(
|
|
3065
|
+
timeToResolveMetric.time,
|
|
3066
|
+
);
|
|
3067
|
+
timeToResolveMetric.metricPointType = MetricPointType.Sum;
|
|
3068
|
+
timeToResolveMetric.retentionDate = incidentMetricRetentionDate;
|
|
3004
3069
|
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3070
|
+
itemsToSave.push(timeToResolveMetric);
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3008
3073
|
|
|
3009
|
-
|
|
3074
|
+
/*
|
|
3075
|
+
* Incident duration — write-once, finalized at resolution.
|
|
3076
|
+
*
|
|
3077
|
+
* The previous implementation recomputed duration as
|
|
3078
|
+
* (latest state transition − start) on every refresh, so an open
|
|
3079
|
+
* incident's duration grew and had to be deleted + re-inserted each
|
|
3080
|
+
* time. With no deletes we emit a single final duration once the
|
|
3081
|
+
* incident reaches a resolved state: (first resolution − start). An
|
|
3082
|
+
* incident that is never resolved has no duration row (its lifetime is
|
|
3083
|
+
* not yet final), which keeps the "Avg Duration" widget meaningful.
|
|
3084
|
+
*/
|
|
3085
|
+
if (isIncidentResolved && resolvedIncidentStateTimeline) {
|
|
3086
|
+
// register the metric type so the catalog stays complete across refreshes.
|
|
3087
|
+
const metricType: MetricType = new MetricType();
|
|
3088
|
+
metricType.name = IncidentMetricType.IncidentDuration;
|
|
3089
|
+
metricType.description = "Duration of the incident";
|
|
3090
|
+
metricType.unit = "seconds";
|
|
3091
|
+
metricTypesMap[IncidentMetricType.IncidentDuration] = metricType;
|
|
3092
|
+
|
|
3093
|
+
if (!existingMetricNames.has(IncidentMetricType.IncidentDuration)) {
|
|
3094
|
+
const incidentEndsAt: Date =
|
|
3095
|
+
resolvedIncidentStateTimeline.startsAt ||
|
|
3096
|
+
OneUptimeDate.getCurrentDate();
|
|
3097
|
+
|
|
3098
|
+
const incidentDurationMetric: Metric = new Metric();
|
|
3099
|
+
|
|
3100
|
+
incidentDurationMetric.projectId = incident.projectId;
|
|
3101
|
+
incidentDurationMetric.primaryEntityId = incident.id!;
|
|
3102
|
+
incidentDurationMetric.primaryEntityType = ServiceType.Incident;
|
|
3103
|
+
incidentDurationMetric.name = IncidentMetricType.IncidentDuration;
|
|
3104
|
+
incidentDurationMetric.value = OneUptimeDate.getDifferenceInSeconds(
|
|
3105
|
+
incidentEndsAt,
|
|
3106
|
+
incidentStartsAt,
|
|
3107
|
+
);
|
|
3108
|
+
incidentDurationMetric.attributes = { ...baseMetricAttributes };
|
|
3109
|
+
incidentDurationMetric.attributeKeys = TelemetryUtil.getAttributeKeys(
|
|
3110
|
+
incidentDurationMetric.attributes,
|
|
3111
|
+
);
|
|
3010
3112
|
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
incidentStartsAt,
|
|
3018
|
-
);
|
|
3019
|
-
incidentDurationMetric.attributes = { ...baseMetricAttributes };
|
|
3020
|
-
incidentDurationMetric.attributeKeys = TelemetryUtil.getAttributeKeys(
|
|
3021
|
-
incidentDurationMetric.attributes,
|
|
3022
|
-
);
|
|
3113
|
+
incidentDurationMetric.time = incidentEndsAt;
|
|
3114
|
+
incidentDurationMetric.timeUnixNano = OneUptimeDate.toUnixNano(
|
|
3115
|
+
incidentDurationMetric.time,
|
|
3116
|
+
);
|
|
3117
|
+
incidentDurationMetric.metricPointType = MetricPointType.Sum;
|
|
3118
|
+
incidentDurationMetric.retentionDate = incidentMetricRetentionDate;
|
|
3023
3119
|
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
incident.createdAt ||
|
|
3028
|
-
OneUptimeDate.getCurrentDate();
|
|
3029
|
-
incidentDurationMetric.timeUnixNano = OneUptimeDate.toUnixNano(
|
|
3030
|
-
incidentDurationMetric.time,
|
|
3031
|
-
);
|
|
3032
|
-
incidentDurationMetric.metricPointType = MetricPointType.Sum;
|
|
3033
|
-
incidentDurationMetric.retentionDate = incidentMetricRetentionDate;
|
|
3120
|
+
itemsToSave.push(incidentDurationMetric);
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3034
3123
|
|
|
3035
|
-
|
|
3124
|
+
// time-in-state metrics — emit one metric per state transition that has a completed duration
|
|
3125
|
+
for (const timeline of incidentStateTimelines) {
|
|
3126
|
+
if (!timeline.startsAt || !timeline.endsAt) {
|
|
3127
|
+
continue;
|
|
3128
|
+
}
|
|
3036
3129
|
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3130
|
+
// write-once: each completed state transition is emitted exactly once.
|
|
3131
|
+
const timeInStateKey: string = buildTimeInStateKey(
|
|
3132
|
+
timeline.incidentStateId?.toString(),
|
|
3133
|
+
timeline.startsAt,
|
|
3134
|
+
);
|
|
3135
|
+
if (existingTimeInStateKeys.has(timeInStateKey)) {
|
|
3136
|
+
continue;
|
|
3137
|
+
}
|
|
3042
3138
|
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
}
|
|
3139
|
+
const stateName: string =
|
|
3140
|
+
timeline.incidentState?.name?.toString() || "Unknown";
|
|
3046
3141
|
|
|
3047
|
-
|
|
3048
|
-
for (const timeline of incidentStateTimelines) {
|
|
3049
|
-
if (!timeline.startsAt || !timeline.endsAt) {
|
|
3050
|
-
continue;
|
|
3051
|
-
}
|
|
3142
|
+
const timeInStateMetric: Metric = new Metric();
|
|
3052
3143
|
|
|
3053
|
-
|
|
3054
|
-
|
|
3144
|
+
timeInStateMetric.projectId = incident.projectId;
|
|
3145
|
+
timeInStateMetric.primaryEntityId = incident.id!;
|
|
3146
|
+
timeInStateMetric.primaryEntityType = ServiceType.Incident;
|
|
3147
|
+
timeInStateMetric.name = IncidentMetricType.TimeInState;
|
|
3148
|
+
timeInStateMetric.value = OneUptimeDate.getDifferenceInSeconds(
|
|
3149
|
+
timeline.endsAt,
|
|
3150
|
+
timeline.startsAt,
|
|
3151
|
+
);
|
|
3152
|
+
timeInStateMetric.attributes = {
|
|
3153
|
+
...baseMetricAttributes,
|
|
3154
|
+
incidentStateName: stateName,
|
|
3155
|
+
incidentStateId: timeline.incidentStateId?.toString(),
|
|
3156
|
+
isCreatedState:
|
|
3157
|
+
timeline.incidentState?.isCreatedState?.toString() || "false",
|
|
3158
|
+
isAcknowledgedState:
|
|
3159
|
+
timeline.incidentState?.isAcknowledgedState?.toString() || "false",
|
|
3160
|
+
isResolvedState:
|
|
3161
|
+
timeline.incidentState?.isResolvedState?.toString() || "false",
|
|
3162
|
+
};
|
|
3163
|
+
timeInStateMetric.attributeKeys = TelemetryUtil.getAttributeKeys(
|
|
3164
|
+
timeInStateMetric.attributes,
|
|
3165
|
+
);
|
|
3055
3166
|
|
|
3056
|
-
|
|
3167
|
+
timeInStateMetric.time = timeline.startsAt;
|
|
3168
|
+
timeInStateMetric.timeUnixNano = OneUptimeDate.toUnixNano(
|
|
3169
|
+
timeInStateMetric.time,
|
|
3170
|
+
);
|
|
3171
|
+
timeInStateMetric.metricPointType = MetricPointType.Sum;
|
|
3172
|
+
timeInStateMetric.retentionDate = incidentMetricRetentionDate;
|
|
3057
3173
|
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
timeInStateMetric.primaryEntityType = ServiceType.Incident;
|
|
3061
|
-
timeInStateMetric.name = IncidentMetricType.TimeInState;
|
|
3062
|
-
timeInStateMetric.value = OneUptimeDate.getDifferenceInSeconds(
|
|
3063
|
-
timeline.endsAt,
|
|
3064
|
-
timeline.startsAt,
|
|
3065
|
-
);
|
|
3066
|
-
timeInStateMetric.attributes = {
|
|
3067
|
-
...baseMetricAttributes,
|
|
3068
|
-
incidentStateName: stateName,
|
|
3069
|
-
incidentStateId: timeline.incidentStateId?.toString(),
|
|
3070
|
-
isCreatedState:
|
|
3071
|
-
timeline.incidentState?.isCreatedState?.toString() || "false",
|
|
3072
|
-
isAcknowledgedState:
|
|
3073
|
-
timeline.incidentState?.isAcknowledgedState?.toString() || "false",
|
|
3074
|
-
isResolvedState:
|
|
3075
|
-
timeline.incidentState?.isResolvedState?.toString() || "false",
|
|
3076
|
-
};
|
|
3077
|
-
timeInStateMetric.attributeKeys = TelemetryUtil.getAttributeKeys(
|
|
3078
|
-
timeInStateMetric.attributes,
|
|
3079
|
-
);
|
|
3174
|
+
itemsToSave.push(timeInStateMetric);
|
|
3175
|
+
}
|
|
3080
3176
|
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3177
|
+
// add metric type for time-in-state to map (only once)
|
|
3178
|
+
if (
|
|
3179
|
+
incidentStateTimelines.some((t: IncidentStateTimeline) => {
|
|
3180
|
+
return t.startsAt && t.endsAt;
|
|
3181
|
+
})
|
|
3182
|
+
) {
|
|
3183
|
+
const timeInStateMetricType: MetricType = new MetricType();
|
|
3184
|
+
timeInStateMetricType.name = IncidentMetricType.TimeInState;
|
|
3185
|
+
timeInStateMetricType.description =
|
|
3186
|
+
"Time spent in each incident state (e.g. Created, Investigating, Acknowledged)";
|
|
3187
|
+
timeInStateMetricType.unit = "seconds";
|
|
3188
|
+
metricTypesMap[timeInStateMetricType.name] = timeInStateMetricType;
|
|
3189
|
+
}
|
|
3087
3190
|
|
|
3088
|
-
|
|
3089
|
-
|
|
3191
|
+
// write-once: a refresh that finds nothing new inserts nothing.
|
|
3192
|
+
if (itemsToSave.length > 0) {
|
|
3193
|
+
await MetricService.createMany({
|
|
3194
|
+
items: itemsToSave,
|
|
3195
|
+
props: {
|
|
3196
|
+
isRoot: true,
|
|
3197
|
+
},
|
|
3198
|
+
});
|
|
3199
|
+
}
|
|
3090
3200
|
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3201
|
+
TelemetryUtil.indexMetricNameServiceNameMap({
|
|
3202
|
+
metricNameServiceNameMap: metricTypesMap,
|
|
3203
|
+
projectId: incident.projectId,
|
|
3204
|
+
}).catch((err: Error) => {
|
|
3205
|
+
logger.error(err, {
|
|
3206
|
+
projectId: incident.projectId?.toString(),
|
|
3207
|
+
incidentId: incident.id?.toString(),
|
|
3208
|
+
} as LogAttributes);
|
|
3209
|
+
});
|
|
3210
|
+
} finally {
|
|
3211
|
+
if (metricRefreshMutex) {
|
|
3212
|
+
try {
|
|
3213
|
+
await Semaphore.release(metricRefreshMutex);
|
|
3214
|
+
} catch (err) {
|
|
3215
|
+
logger.error(
|
|
3216
|
+
err as Error,
|
|
3217
|
+
{
|
|
3218
|
+
projectId: incident.projectId?.toString(),
|
|
3219
|
+
incidentId: incident.id?.toString(),
|
|
3220
|
+
} as LogAttributes,
|
|
3221
|
+
);
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3103
3224
|
}
|
|
3104
|
-
|
|
3105
|
-
await MetricService.createMany({
|
|
3106
|
-
items: itemsToSave,
|
|
3107
|
-
props: {
|
|
3108
|
-
isRoot: true,
|
|
3109
|
-
},
|
|
3110
|
-
});
|
|
3111
|
-
|
|
3112
|
-
TelemetryUtil.indexMetricNameServiceNameMap({
|
|
3113
|
-
metricNameServiceNameMap: metricTypesMap,
|
|
3114
|
-
projectId: incident.projectId,
|
|
3115
|
-
}).catch((err: Error) => {
|
|
3116
|
-
logger.error(err, {
|
|
3117
|
-
projectId: incident.projectId?.toString(),
|
|
3118
|
-
incidentId: incident.id?.toString(),
|
|
3119
|
-
} as LogAttributes);
|
|
3120
|
-
});
|
|
3121
3225
|
}
|
|
3122
3226
|
|
|
3123
3227
|
@CaptureSpan()
|