@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
|
@@ -49,7 +49,7 @@ import ServiceType from "../../Types/Telemetry/ServiceType";
|
|
|
49
49
|
import OneUptimeDate from "../../Types/Date";
|
|
50
50
|
import TelemetryUtil from "../Utils/Telemetry/Telemetry";
|
|
51
51
|
import logger from "../Utils/Logger";
|
|
52
|
-
import
|
|
52
|
+
import Semaphore from "../Infrastructure/Semaphore";
|
|
53
53
|
import IncidentFeedService from "./IncidentFeedService";
|
|
54
54
|
import IncidentSlaService from "./IncidentSlaService";
|
|
55
55
|
import { setIsPublicForMarkdownImages } from "../Utils/InlineImageAccessTokenSync";
|
|
@@ -1941,7 +1941,7 @@ ${incidentSeverity.name}
|
|
|
1941
1941
|
return Service.DEFAULT_METRIC_RETENTION_DAYS;
|
|
1942
1942
|
}
|
|
1943
1943
|
async refreshIncidentMetrics(data) {
|
|
1944
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
1944
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
|
|
1945
1945
|
const incident = await this.findOneById({
|
|
1946
1946
|
id: data.incidentId,
|
|
1947
1947
|
select: {
|
|
@@ -2061,254 +2061,340 @@ ${incidentSeverity.name}
|
|
|
2061
2061
|
});
|
|
2062
2062
|
const firstIncidentStateTimeline = incidentStateTimelines[0];
|
|
2063
2063
|
/*
|
|
2064
|
-
*
|
|
2065
|
-
*
|
|
2066
|
-
*
|
|
2067
|
-
*
|
|
2068
|
-
*
|
|
2069
|
-
*
|
|
2070
|
-
*
|
|
2071
|
-
*
|
|
2072
|
-
*
|
|
2073
|
-
*
|
|
2074
|
-
*
|
|
2075
|
-
*
|
|
2064
|
+
* Serialize concurrent refreshes for this incident across pods. The
|
|
2065
|
+
* write-once guard below is read-then-insert (findBy existing, then a
|
|
2066
|
+
* conditional createMany). Both call sites fire refresh fire-and-forget
|
|
2067
|
+
* after releasing the per-incident state-timeline mutex, so two
|
|
2068
|
+
* close-together transitions (e.g. auto-acknowledge then auto-resolve)
|
|
2069
|
+
* could interleave: both read a snapshot missing a metric and both
|
|
2070
|
+
* insert it. The Metric table is a plain MergeTree with no row collapse,
|
|
2071
|
+
* so those duplicates would be permanent and re-inflate the very Sum/Avg
|
|
2072
|
+
* widgets this change fixes. A best-effort distributed lock keyed on the
|
|
2073
|
+
* incident makes the read+insert atomic; if the lock can't be taken we
|
|
2074
|
+
* still proceed (the existence check alone covers the common sequential
|
|
2075
|
+
* case).
|
|
2076
2076
|
*/
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2077
|
+
let metricRefreshMutex = null;
|
|
2078
|
+
try {
|
|
2079
|
+
metricRefreshMutex = await Semaphore.lock({
|
|
2080
|
+
key: data.incidentId.toString(),
|
|
2081
|
+
namespace: "IncidentService.refreshIncidentMetrics",
|
|
2082
|
+
lockTimeout: 30000,
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
catch (err) {
|
|
2086
|
+
logger.error(err, {
|
|
2087
|
+
projectId: (_a = incident.projectId) === null || _a === void 0 ? void 0 : _a.toString(),
|
|
2088
|
+
incidentId: (_b = incident.id) === null || _b === void 0 ? void 0 : _b.toString(),
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
try {
|
|
2092
|
+
/*
|
|
2093
|
+
* Write-once metrics — no deletes.
|
|
2094
|
+
*
|
|
2095
|
+
* The raw Metric table (`MetricItemV3`) feeds an AggregatingMergeTree
|
|
2096
|
+
* materialized view (`MetricItemAggMV1m_mv`) that accumulates
|
|
2097
|
+
* `sumState`/`countState` per (projectId, name, primaryEntityId,
|
|
2098
|
+
* minute). The MV trigger fires only on INSERT — an `ALTER ... DELETE`
|
|
2099
|
+
* mutation on the source rolls back nothing in the MV. So the old
|
|
2100
|
+
* "delete every metric for this incident, then re-insert" refresh both
|
|
2101
|
+
* (a) inflated those rollups (the Sum-of-IncidentCount widget read high)
|
|
2102
|
+
* and (b) issued one heavy `ALTER ... DELETE` mutation per state
|
|
2103
|
+
* transition per incident — a mutation storm.
|
|
2104
|
+
*
|
|
2105
|
+
* Every incident metric has a single eventually-final value:
|
|
2106
|
+
* IncidentCount is constant `value = 1`; MTTA/MTTR/duration/time-in-state
|
|
2107
|
+
* are each fixed once the relevant state transition has happened. So we
|
|
2108
|
+
* load what we've already emitted for this incident and insert each
|
|
2109
|
+
* metric exactly once, when it becomes final. Repeat refreshes that find
|
|
2110
|
+
* nothing new are pure no-ops (no insert, no mutation).
|
|
2111
|
+
*/
|
|
2112
|
+
const existingMetrics = await MetricService.findBy({
|
|
2113
|
+
query: {
|
|
2114
|
+
projectId: incident.projectId,
|
|
2115
|
+
primaryEntityId: data.incidentId,
|
|
2116
|
+
},
|
|
2117
|
+
select: {
|
|
2118
|
+
name: true,
|
|
2119
|
+
time: true,
|
|
2120
|
+
attributes: true,
|
|
2121
|
+
},
|
|
2122
|
+
skip: 0,
|
|
2123
|
+
limit: LIMIT_PER_PROJECT,
|
|
2124
|
+
props: {
|
|
2125
|
+
isRoot: true,
|
|
2126
|
+
},
|
|
2127
|
+
});
|
|
2128
|
+
const existingMetricNames = new Set(existingMetrics
|
|
2129
|
+
.map((metric) => {
|
|
2130
|
+
return metric.name;
|
|
2131
|
+
})
|
|
2132
|
+
.filter(Boolean));
|
|
2133
|
+
/*
|
|
2134
|
+
* Identity of a TimeInState row: the state plus the instant that state
|
|
2135
|
+
* began (a state can be entered more than once over a re-opened
|
|
2136
|
+
* incident). Second granularity is robust against DateTime64 round-trip
|
|
2137
|
+
* jitter and two distinct entries into the same state within one second
|
|
2138
|
+
* never happen.
|
|
2139
|
+
*/
|
|
2140
|
+
const buildTimeInStateKey = (stateId, startsAt) => {
|
|
2141
|
+
return `${stateId || ""}|${startsAt ? OneUptimeDate.toUnixTimestamp(startsAt) : ""}`;
|
|
2142
|
+
};
|
|
2143
|
+
const existingTimeInStateKeys = new Set(existingMetrics
|
|
2144
|
+
.filter((metric) => {
|
|
2145
|
+
return metric.name === IncidentMetricType.TimeInState;
|
|
2146
|
+
})
|
|
2147
|
+
.map((metric) => {
|
|
2109
2148
|
var _a;
|
|
2110
|
-
return (_a =
|
|
2111
|
-
})
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2149
|
+
return buildTimeInStateKey((_a = metric.attributes) === null || _a === void 0 ? void 0 : _a["incidentStateId"], metric.time);
|
|
2150
|
+
}));
|
|
2151
|
+
const itemsToSave = [];
|
|
2152
|
+
const metricRetentionDays = await this.getMetricRetentionDays();
|
|
2153
|
+
const incidentMetricRetentionDate = OneUptimeDate.addRemoveDays(OneUptimeDate.getCurrentDate(), metricRetentionDays);
|
|
2154
|
+
// now we need to create new metrics for this incident - TimeToAcknowledge, TimeToResolve, IncidentCount, IncidentDuration
|
|
2155
|
+
const incidentStartsAt = (firstIncidentStateTimeline === null || firstIncidentStateTimeline === void 0 ? void 0 : firstIncidentStateTimeline.startsAt) ||
|
|
2156
|
+
incident.declaredAt ||
|
|
2157
|
+
incident.createdAt ||
|
|
2158
|
+
OneUptimeDate.getCurrentDate();
|
|
2159
|
+
const metricTypesMap = {};
|
|
2160
|
+
/*
|
|
2161
|
+
* common attributes shared by all incident metrics
|
|
2162
|
+
* All values must be strings for ClickHouse Map(String, String) storage.
|
|
2163
|
+
* Arrays are joined as comma-separated strings.
|
|
2164
|
+
*/
|
|
2165
|
+
const baseMetricAttributes = {
|
|
2166
|
+
incidentId: data.incidentId.toString(),
|
|
2167
|
+
projectId: incident.projectId.toString(),
|
|
2168
|
+
monitorIds: (((_c = incident.monitors) === null || _c === void 0 ? void 0 : _c.map((monitor) => {
|
|
2169
|
+
var _a;
|
|
2170
|
+
return (_a = monitor._id) === null || _a === void 0 ? void 0 : _a.toString();
|
|
2171
|
+
}).filter(Boolean)) || []).join(", "),
|
|
2172
|
+
monitorNames: (((_d = incident.monitors) === null || _d === void 0 ? void 0 : _d.map((monitor) => {
|
|
2173
|
+
var _a;
|
|
2174
|
+
return (_a = monitor.name) === null || _a === void 0 ? void 0 : _a.toString();
|
|
2175
|
+
}).filter(Boolean)) || []).join(", "),
|
|
2176
|
+
incidentSeverityId: (_f = (_e = incident.incidentSeverity) === null || _e === void 0 ? void 0 : _e._id) === null || _f === void 0 ? void 0 : _f.toString(),
|
|
2177
|
+
incidentSeverityName: (_h = (_g = incident.incidentSeverity) === null || _g === void 0 ? void 0 : _g.name) === null || _h === void 0 ? void 0 : _h.toString(),
|
|
2178
|
+
ownerUserIds: ownerUserIds.join(", "),
|
|
2179
|
+
ownerUserNames: ownerUserNames.join(", "),
|
|
2180
|
+
ownerTeamIds: ownerTeamIds.join(", "),
|
|
2181
|
+
ownerTeamNames: ownerTeamNames.join(", "),
|
|
2182
|
+
};
|
|
2183
|
+
/*
|
|
2184
|
+
* Only emit IncidentCount on the very first refresh (i.e. when no
|
|
2185
|
+
* existing IncidentCount row is present for this primaryEntityId). See
|
|
2186
|
+
* the write-once note above — emitting it on every refresh would
|
|
2187
|
+
* accumulate phantom `sumState` entries in the MV. By keeping the
|
|
2188
|
+
* original row alive and never re-emitting, the dashboard Sum stays
|
|
2189
|
+
* equal to the true count of distinct incidents.
|
|
2190
|
+
*/
|
|
2191
|
+
if (!existingMetricNames.has(IncidentMetricType.IncidentCount)) {
|
|
2192
|
+
const incidentCountMetric = new Metric();
|
|
2193
|
+
incidentCountMetric.projectId = incident.projectId;
|
|
2194
|
+
incidentCountMetric.primaryEntityId = incident.id;
|
|
2195
|
+
incidentCountMetric.primaryEntityType = ServiceType.Incident;
|
|
2196
|
+
incidentCountMetric.name = IncidentMetricType.IncidentCount;
|
|
2197
|
+
incidentCountMetric.value = 1;
|
|
2198
|
+
incidentCountMetric.attributes = Object.assign({}, baseMetricAttributes);
|
|
2199
|
+
incidentCountMetric.attributeKeys = TelemetryUtil.getAttributeKeys(incidentCountMetric.attributes);
|
|
2200
|
+
incidentCountMetric.time = incidentStartsAt;
|
|
2201
|
+
incidentCountMetric.timeUnixNano = OneUptimeDate.toUnixNano(incidentCountMetric.time);
|
|
2202
|
+
incidentCountMetric.metricPointType = MetricPointType.Sum;
|
|
2203
|
+
incidentCountMetric.retentionDate = incidentMetricRetentionDate;
|
|
2204
|
+
itemsToSave.push(incidentCountMetric);
|
|
2205
|
+
}
|
|
2206
|
+
// Always register the metric type so it shows up in the type catalog.
|
|
2207
|
+
const metricType = new MetricType();
|
|
2208
|
+
metricType.name = IncidentMetricType.IncidentCount;
|
|
2209
|
+
metricType.description = "Number of incidents created";
|
|
2210
|
+
metricType.unit = "";
|
|
2211
|
+
metricType.services = [];
|
|
2212
|
+
metricTypesMap[IncidentMetricType.IncidentCount] = metricType;
|
|
2213
|
+
// is the incident acknowledged?
|
|
2214
|
+
const isIncidentAcknowledged = incidentStateTimelines.some((timeline) => {
|
|
2167
2215
|
var _a;
|
|
2168
2216
|
return (_a = timeline.incidentState) === null || _a === void 0 ? void 0 : _a.isAcknowledgedState;
|
|
2169
2217
|
});
|
|
2170
|
-
if (
|
|
2171
|
-
const
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2218
|
+
if (isIncidentAcknowledged) {
|
|
2219
|
+
const ackIncidentStateTimeline = incidentStateTimelines.find((timeline) => {
|
|
2220
|
+
var _a;
|
|
2221
|
+
return (_a = timeline.incidentState) === null || _a === void 0 ? void 0 : _a.isAcknowledgedState;
|
|
2222
|
+
});
|
|
2223
|
+
if (ackIncidentStateTimeline) {
|
|
2224
|
+
// register the metric type so the catalog stays complete across refreshes.
|
|
2225
|
+
const metricType = new MetricType();
|
|
2226
|
+
metricType.name = IncidentMetricType.TimeToAcknowledge;
|
|
2227
|
+
metricType.description = "Time taken to acknowledge the incident";
|
|
2228
|
+
metricType.unit = "seconds";
|
|
2229
|
+
metricTypesMap[IncidentMetricType.TimeToAcknowledge] = metricType;
|
|
2230
|
+
// write-once: MTTA is fixed once the incident is first acknowledged.
|
|
2231
|
+
if (!existingMetricNames.has(IncidentMetricType.TimeToAcknowledge)) {
|
|
2232
|
+
const timeToAcknowledgeMetric = new Metric();
|
|
2233
|
+
timeToAcknowledgeMetric.projectId = incident.projectId;
|
|
2234
|
+
timeToAcknowledgeMetric.primaryEntityId = incident.id;
|
|
2235
|
+
timeToAcknowledgeMetric.primaryEntityType = ServiceType.Incident;
|
|
2236
|
+
timeToAcknowledgeMetric.name = IncidentMetricType.TimeToAcknowledge;
|
|
2237
|
+
timeToAcknowledgeMetric.value =
|
|
2238
|
+
OneUptimeDate.getDifferenceInSeconds((ackIncidentStateTimeline === null || ackIncidentStateTimeline === void 0 ? void 0 : ackIncidentStateTimeline.startsAt) ||
|
|
2239
|
+
OneUptimeDate.getCurrentDate(), incidentStartsAt);
|
|
2240
|
+
timeToAcknowledgeMetric.attributes = Object.assign({}, baseMetricAttributes);
|
|
2241
|
+
timeToAcknowledgeMetric.attributeKeys =
|
|
2242
|
+
TelemetryUtil.getAttributeKeys(timeToAcknowledgeMetric.attributes);
|
|
2243
|
+
timeToAcknowledgeMetric.time =
|
|
2244
|
+
(ackIncidentStateTimeline === null || ackIncidentStateTimeline === void 0 ? void 0 : ackIncidentStateTimeline.startsAt) ||
|
|
2245
|
+
incident.declaredAt ||
|
|
2246
|
+
incident.createdAt ||
|
|
2247
|
+
OneUptimeDate.getCurrentDate();
|
|
2248
|
+
timeToAcknowledgeMetric.timeUnixNano = OneUptimeDate.toUnixNano(timeToAcknowledgeMetric.time);
|
|
2249
|
+
timeToAcknowledgeMetric.metricPointType = MetricPointType.Sum;
|
|
2250
|
+
timeToAcknowledgeMetric.retentionDate = incidentMetricRetentionDate;
|
|
2251
|
+
itemsToSave.push(timeToAcknowledgeMetric);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2195
2254
|
}
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
});
|
|
2202
|
-
if (isIncidentResolved) {
|
|
2255
|
+
// time to resolve
|
|
2256
|
+
const isIncidentResolved = incidentStateTimelines.some((timeline) => {
|
|
2257
|
+
var _a;
|
|
2258
|
+
return (_a = timeline.incidentState) === null || _a === void 0 ? void 0 : _a.isResolvedState;
|
|
2259
|
+
});
|
|
2203
2260
|
const resolvedIncidentStateTimeline = incidentStateTimelines.find((timeline) => {
|
|
2204
2261
|
var _a;
|
|
2205
2262
|
return (_a = timeline.incidentState) === null || _a === void 0 ? void 0 : _a.isResolvedState;
|
|
2206
2263
|
});
|
|
2207
|
-
if (resolvedIncidentStateTimeline) {
|
|
2208
|
-
|
|
2209
|
-
timeToResolveMetric.projectId = incident.projectId;
|
|
2210
|
-
timeToResolveMetric.primaryEntityId = incident.id;
|
|
2211
|
-
timeToResolveMetric.primaryEntityType = ServiceType.Incident;
|
|
2212
|
-
timeToResolveMetric.name = IncidentMetricType.TimeToResolve;
|
|
2213
|
-
timeToResolveMetric.value = OneUptimeDate.getDifferenceInSeconds((resolvedIncidentStateTimeline === null || resolvedIncidentStateTimeline === void 0 ? void 0 : resolvedIncidentStateTimeline.startsAt) ||
|
|
2214
|
-
OneUptimeDate.getCurrentDate(), incidentStartsAt);
|
|
2215
|
-
timeToResolveMetric.attributes = Object.assign({}, baseMetricAttributes);
|
|
2216
|
-
timeToResolveMetric.attributeKeys = TelemetryUtil.getAttributeKeys(timeToResolveMetric.attributes);
|
|
2217
|
-
timeToResolveMetric.time =
|
|
2218
|
-
(resolvedIncidentStateTimeline === null || resolvedIncidentStateTimeline === void 0 ? void 0 : resolvedIncidentStateTimeline.startsAt) ||
|
|
2219
|
-
incident.declaredAt ||
|
|
2220
|
-
incident.createdAt ||
|
|
2221
|
-
OneUptimeDate.getCurrentDate();
|
|
2222
|
-
timeToResolveMetric.timeUnixNano = OneUptimeDate.toUnixNano(timeToResolveMetric.time);
|
|
2223
|
-
timeToResolveMetric.metricPointType = MetricPointType.Sum;
|
|
2224
|
-
timeToResolveMetric.retentionDate = incidentMetricRetentionDate;
|
|
2225
|
-
itemsToSave.push(timeToResolveMetric);
|
|
2226
|
-
// add metric type for this to map.
|
|
2264
|
+
if (isIncidentResolved && resolvedIncidentStateTimeline) {
|
|
2265
|
+
// register the metric type so the catalog stays complete across refreshes.
|
|
2227
2266
|
const metricType = new MetricType();
|
|
2228
|
-
metricType.name =
|
|
2267
|
+
metricType.name = IncidentMetricType.TimeToResolve;
|
|
2229
2268
|
metricType.description = "Time taken to resolve the incident";
|
|
2230
2269
|
metricType.unit = "seconds";
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2270
|
+
metricTypesMap[IncidentMetricType.TimeToResolve] = metricType;
|
|
2271
|
+
// write-once: MTTR is fixed once the incident is first resolved.
|
|
2272
|
+
if (!existingMetricNames.has(IncidentMetricType.TimeToResolve)) {
|
|
2273
|
+
const timeToResolveMetric = new Metric();
|
|
2274
|
+
timeToResolveMetric.projectId = incident.projectId;
|
|
2275
|
+
timeToResolveMetric.primaryEntityId = incident.id;
|
|
2276
|
+
timeToResolveMetric.primaryEntityType = ServiceType.Incident;
|
|
2277
|
+
timeToResolveMetric.name = IncidentMetricType.TimeToResolve;
|
|
2278
|
+
timeToResolveMetric.value = OneUptimeDate.getDifferenceInSeconds((resolvedIncidentStateTimeline === null || resolvedIncidentStateTimeline === void 0 ? void 0 : resolvedIncidentStateTimeline.startsAt) ||
|
|
2279
|
+
OneUptimeDate.getCurrentDate(), incidentStartsAt);
|
|
2280
|
+
timeToResolveMetric.attributes = Object.assign({}, baseMetricAttributes);
|
|
2281
|
+
timeToResolveMetric.attributeKeys = TelemetryUtil.getAttributeKeys(timeToResolveMetric.attributes);
|
|
2282
|
+
timeToResolveMetric.time =
|
|
2283
|
+
(resolvedIncidentStateTimeline === null || resolvedIncidentStateTimeline === void 0 ? void 0 : resolvedIncidentStateTimeline.startsAt) ||
|
|
2284
|
+
incident.declaredAt ||
|
|
2285
|
+
incident.createdAt ||
|
|
2286
|
+
OneUptimeDate.getCurrentDate();
|
|
2287
|
+
timeToResolveMetric.timeUnixNano = OneUptimeDate.toUnixNano(timeToResolveMetric.time);
|
|
2288
|
+
timeToResolveMetric.metricPointType = MetricPointType.Sum;
|
|
2289
|
+
timeToResolveMetric.retentionDate = incidentMetricRetentionDate;
|
|
2290
|
+
itemsToSave.push(timeToResolveMetric);
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
/*
|
|
2294
|
+
* Incident duration — write-once, finalized at resolution.
|
|
2295
|
+
*
|
|
2296
|
+
* The previous implementation recomputed duration as
|
|
2297
|
+
* (latest state transition − start) on every refresh, so an open
|
|
2298
|
+
* incident's duration grew and had to be deleted + re-inserted each
|
|
2299
|
+
* time. With no deletes we emit a single final duration once the
|
|
2300
|
+
* incident reaches a resolved state: (first resolution − start). An
|
|
2301
|
+
* incident that is never resolved has no duration row (its lifetime is
|
|
2302
|
+
* not yet final), which keeps the "Avg Duration" widget meaningful.
|
|
2303
|
+
*/
|
|
2304
|
+
if (isIncidentResolved && resolvedIncidentStateTimeline) {
|
|
2305
|
+
// register the metric type so the catalog stays complete across refreshes.
|
|
2306
|
+
const metricType = new MetricType();
|
|
2307
|
+
metricType.name = IncidentMetricType.IncidentDuration;
|
|
2308
|
+
metricType.description = "Duration of the incident";
|
|
2309
|
+
metricType.unit = "seconds";
|
|
2310
|
+
metricTypesMap[IncidentMetricType.IncidentDuration] = metricType;
|
|
2311
|
+
if (!existingMetricNames.has(IncidentMetricType.IncidentDuration)) {
|
|
2312
|
+
const incidentEndsAt = resolvedIncidentStateTimeline.startsAt ||
|
|
2313
|
+
OneUptimeDate.getCurrentDate();
|
|
2314
|
+
const incidentDurationMetric = new Metric();
|
|
2315
|
+
incidentDurationMetric.projectId = incident.projectId;
|
|
2316
|
+
incidentDurationMetric.primaryEntityId = incident.id;
|
|
2317
|
+
incidentDurationMetric.primaryEntityType = ServiceType.Incident;
|
|
2318
|
+
incidentDurationMetric.name = IncidentMetricType.IncidentDuration;
|
|
2319
|
+
incidentDurationMetric.value = OneUptimeDate.getDifferenceInSeconds(incidentEndsAt, incidentStartsAt);
|
|
2320
|
+
incidentDurationMetric.attributes = Object.assign({}, baseMetricAttributes);
|
|
2321
|
+
incidentDurationMetric.attributeKeys = TelemetryUtil.getAttributeKeys(incidentDurationMetric.attributes);
|
|
2322
|
+
incidentDurationMetric.time = incidentEndsAt;
|
|
2323
|
+
incidentDurationMetric.timeUnixNano = OneUptimeDate.toUnixNano(incidentDurationMetric.time);
|
|
2324
|
+
incidentDurationMetric.metricPointType = MetricPointType.Sum;
|
|
2325
|
+
incidentDurationMetric.retentionDate = incidentMetricRetentionDate;
|
|
2326
|
+
itemsToSave.push(incidentDurationMetric);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
// time-in-state metrics — emit one metric per state transition that has a completed duration
|
|
2330
|
+
for (const timeline of incidentStateTimelines) {
|
|
2331
|
+
if (!timeline.startsAt || !timeline.endsAt) {
|
|
2332
|
+
continue;
|
|
2333
|
+
}
|
|
2334
|
+
// write-once: each completed state transition is emitted exactly once.
|
|
2335
|
+
const timeInStateKey = buildTimeInStateKey((_j = timeline.incidentStateId) === null || _j === void 0 ? void 0 : _j.toString(), timeline.startsAt);
|
|
2336
|
+
if (existingTimeInStateKeys.has(timeInStateKey)) {
|
|
2337
|
+
continue;
|
|
2338
|
+
}
|
|
2339
|
+
const stateName = ((_l = (_k = timeline.incidentState) === null || _k === void 0 ? void 0 : _k.name) === null || _l === void 0 ? void 0 : _l.toString()) || "Unknown";
|
|
2340
|
+
const timeInStateMetric = new Metric();
|
|
2341
|
+
timeInStateMetric.projectId = incident.projectId;
|
|
2342
|
+
timeInStateMetric.primaryEntityId = incident.id;
|
|
2343
|
+
timeInStateMetric.primaryEntityType = ServiceType.Incident;
|
|
2344
|
+
timeInStateMetric.name = IncidentMetricType.TimeInState;
|
|
2345
|
+
timeInStateMetric.value = OneUptimeDate.getDifferenceInSeconds(timeline.endsAt, timeline.startsAt);
|
|
2346
|
+
timeInStateMetric.attributes = Object.assign(Object.assign({}, baseMetricAttributes), { incidentStateName: stateName, incidentStateId: (_m = timeline.incidentStateId) === null || _m === void 0 ? void 0 : _m.toString(), isCreatedState: ((_p = (_o = timeline.incidentState) === null || _o === void 0 ? void 0 : _o.isCreatedState) === null || _p === void 0 ? void 0 : _p.toString()) || "false", isAcknowledgedState: ((_r = (_q = timeline.incidentState) === null || _q === void 0 ? void 0 : _q.isAcknowledgedState) === null || _r === void 0 ? void 0 : _r.toString()) || "false", isResolvedState: ((_t = (_s = timeline.incidentState) === null || _s === void 0 ? void 0 : _s.isResolvedState) === null || _t === void 0 ? void 0 : _t.toString()) || "false" });
|
|
2347
|
+
timeInStateMetric.attributeKeys = TelemetryUtil.getAttributeKeys(timeInStateMetric.attributes);
|
|
2348
|
+
timeInStateMetric.time = timeline.startsAt;
|
|
2349
|
+
timeInStateMetric.timeUnixNano = OneUptimeDate.toUnixNano(timeInStateMetric.time);
|
|
2350
|
+
timeInStateMetric.metricPointType = MetricPointType.Sum;
|
|
2351
|
+
timeInStateMetric.retentionDate = incidentMetricRetentionDate;
|
|
2352
|
+
itemsToSave.push(timeInStateMetric);
|
|
2353
|
+
}
|
|
2354
|
+
// add metric type for time-in-state to map (only once)
|
|
2355
|
+
if (incidentStateTimelines.some((t) => {
|
|
2356
|
+
return t.startsAt && t.endsAt;
|
|
2357
|
+
})) {
|
|
2358
|
+
const timeInStateMetricType = new MetricType();
|
|
2359
|
+
timeInStateMetricType.name = IncidentMetricType.TimeInState;
|
|
2360
|
+
timeInStateMetricType.description =
|
|
2361
|
+
"Time spent in each incident state (e.g. Created, Investigating, Acknowledged)";
|
|
2362
|
+
timeInStateMetricType.unit = "seconds";
|
|
2363
|
+
metricTypesMap[timeInStateMetricType.name] = timeInStateMetricType;
|
|
2364
|
+
}
|
|
2365
|
+
// write-once: a refresh that finds nothing new inserts nothing.
|
|
2366
|
+
if (itemsToSave.length > 0) {
|
|
2367
|
+
await MetricService.createMany({
|
|
2368
|
+
items: itemsToSave,
|
|
2369
|
+
props: {
|
|
2370
|
+
isRoot: true,
|
|
2371
|
+
},
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
TelemetryUtil.indexMetricNameServiceNameMap({
|
|
2375
|
+
metricNameServiceNameMap: metricTypesMap,
|
|
2376
|
+
projectId: incident.projectId,
|
|
2377
|
+
}).catch((err) => {
|
|
2378
|
+
var _a, _b;
|
|
2379
|
+
logger.error(err, {
|
|
2380
|
+
projectId: (_a = incident.projectId) === null || _a === void 0 ? void 0 : _a.toString(),
|
|
2381
|
+
incidentId: (_b = incident.id) === null || _b === void 0 ? void 0 : _b.toString(),
|
|
2382
|
+
});
|
|
2310
2383
|
});
|
|
2311
|
-
}
|
|
2384
|
+
}
|
|
2385
|
+
finally {
|
|
2386
|
+
if (metricRefreshMutex) {
|
|
2387
|
+
try {
|
|
2388
|
+
await Semaphore.release(metricRefreshMutex);
|
|
2389
|
+
}
|
|
2390
|
+
catch (err) {
|
|
2391
|
+
logger.error(err, {
|
|
2392
|
+
projectId: (_u = incident.projectId) === null || _u === void 0 ? void 0 : _u.toString(),
|
|
2393
|
+
incidentId: (_v = incident.id) === null || _v === void 0 ? void 0 : _v.toString(),
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2312
2398
|
}
|
|
2313
2399
|
async getWorkspaceChannelForIncident(data) {
|
|
2314
2400
|
const incident = await this.findOneById({
|