@oneuptime/common 10.4.10 → 10.4.12

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.
Files changed (174) hide show
  1. package/Models/AnalyticsModels/ExceptionInstance.ts +47 -1
  2. package/Models/AnalyticsModels/Log.ts +47 -1
  3. package/Models/AnalyticsModels/Metric.ts +1 -7
  4. package/Models/AnalyticsModels/Profile.ts +47 -1
  5. package/Models/AnalyticsModels/ProfileSample.ts +47 -1
  6. package/Models/AnalyticsModels/Span.ts +47 -1
  7. package/Models/DatabaseModels/Alert.ts +332 -0
  8. package/Models/DatabaseModels/DockerHost.ts +83 -0
  9. package/Models/DatabaseModels/Host.ts +83 -0
  10. package/Models/DatabaseModels/Incident.ts +332 -0
  11. package/Models/DatabaseModels/Index.ts +0 -2
  12. package/Models/DatabaseModels/KubernetesCluster.ts +83 -0
  13. package/Models/DatabaseModels/RunbookAgent.ts +16 -2
  14. package/Server/EnvironmentConfig.ts +16 -0
  15. package/Server/Infrastructure/ClickhouseConfig.ts +14 -0
  16. package/Server/Infrastructure/ClickhouseDatabase.ts +20 -3
  17. package/Server/Infrastructure/InMemoryTTLCache.ts +61 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1779199346010-AddTelemetryRetentionConfig.ts +1 -1
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/1779277271302-DropServiceDependencyTable.ts +44 -0
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/1779282769946-AddTelemetryRetentionToHostDockerKubernetes.ts +50 -0
  21. package/Server/Infrastructure/Postgres/SchemaMigrations/1779302536475-AttachKubernetesAndDockerToIncidentAndAlert.ts +253 -0
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1779303924241-AttachServiceToIncidentAndAlert.ts +60 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  24. package/Server/Middleware/TelemetryIngest.ts +6 -38
  25. package/Server/Middleware/UserAuthorization.ts +1 -11
  26. package/Server/Services/AlertService.ts +2 -4
  27. package/Server/Services/AnalyticsDatabaseService.ts +33 -1
  28. package/Server/Services/IncidentService.ts +2 -4
  29. package/Server/Services/Index.ts +0 -2
  30. package/Server/Services/LogAggregationService.ts +54 -6
  31. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +6 -0
  32. package/Server/Services/OnCallDutyPolicyEscalationRuleService.ts +1 -0
  33. package/Server/Services/OnCallDutyPolicyScheduleService.ts +17 -0
  34. package/Server/Services/OpenTelemetryIngestService.ts +132 -0
  35. package/Server/Services/TelemetryIngestionKeyService.ts +90 -1
  36. package/Server/Services/TraceAggregationService.ts +83 -8
  37. package/Server/Utils/Monitor/MonitorMetricUtil.ts +2 -4
  38. package/Server/Utils/Telemetry/Telemetry.ts +38 -0
  39. package/Tests/Server/Middleware/UserAuthorization.test.ts +5 -7
  40. package/Types/Dashboard/DashboardComponentType.ts +0 -1
  41. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +2 -0
  42. package/Types/Dashboard/DashboardComponents/DashboardTableComponent.ts +74 -1
  43. package/Types/Dashboard/DashboardTemplates.ts +164 -51
  44. package/Types/Monitor/MonitorType.ts +1 -1
  45. package/Types/OnCallDutyPolicy/UserOverrideUtil.ts +36 -9
  46. package/Types/Permission.ts +0 -46
  47. package/Types/Telemetry/ServiceType.ts +19 -0
  48. package/UI/Components/Forms/Validation.ts +2 -2
  49. package/UI/Components/LogsViewer/LogsViewer.tsx +135 -17
  50. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +84 -1
  51. package/UI/Components/Telemetry/TelemetryRetentionConfigForm.tsx +0 -1
  52. package/Utils/Dashboard/Components/DashboardTableComponent.ts +80 -17
  53. package/Utils/Dashboard/Components/Index.ts +0 -7
  54. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +41 -1
  55. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
  56. package/build/dist/Models/AnalyticsModels/Log.js +41 -1
  57. package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
  58. package/build/dist/Models/AnalyticsModels/Metric.js +0 -7
  59. package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
  60. package/build/dist/Models/AnalyticsModels/Profile.js +41 -1
  61. package/build/dist/Models/AnalyticsModels/Profile.js.map +1 -1
  62. package/build/dist/Models/AnalyticsModels/ProfileSample.js +41 -1
  63. package/build/dist/Models/AnalyticsModels/ProfileSample.js.map +1 -1
  64. package/build/dist/Models/AnalyticsModels/Span.js +41 -1
  65. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  66. package/build/dist/Models/DatabaseModels/Alert.js +324 -0
  67. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  68. package/build/dist/Models/DatabaseModels/DockerHost.js +84 -0
  69. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
  70. package/build/dist/Models/DatabaseModels/Host.js +84 -0
  71. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  72. package/build/dist/Models/DatabaseModels/Incident.js +324 -0
  73. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  74. package/build/dist/Models/DatabaseModels/Index.js +0 -2
  75. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  76. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +84 -0
  77. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  78. package/build/dist/Models/DatabaseModels/RunbookAgent.js +16 -2
  79. package/build/dist/Models/DatabaseModels/RunbookAgent.js.map +1 -1
  80. package/build/dist/Server/EnvironmentConfig.js +8 -0
  81. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  82. package/build/dist/Server/Infrastructure/ClickhouseConfig.js +9 -1
  83. package/build/dist/Server/Infrastructure/ClickhouseConfig.js.map +1 -1
  84. package/build/dist/Server/Infrastructure/ClickhouseDatabase.js +12 -3
  85. package/build/dist/Server/Infrastructure/ClickhouseDatabase.js.map +1 -1
  86. package/build/dist/Server/Infrastructure/InMemoryTTLCache.js +49 -0
  87. package/build/dist/Server/Infrastructure/InMemoryTTLCache.js.map +1 -0
  88. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779199346010-AddTelemetryRetentionConfig.js.map +1 -1
  89. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779277271302-DropServiceDependencyTable.js +42 -0
  90. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779277271302-DropServiceDependencyTable.js.map +1 -0
  91. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779282769946-AddTelemetryRetentionToHostDockerKubernetes.js +22 -0
  92. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779282769946-AddTelemetryRetentionToHostDockerKubernetes.js.map +1 -0
  93. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779302536475-AttachKubernetesAndDockerToIncidentAndAlert.js +100 -0
  94. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779302536475-AttachKubernetesAndDockerToIncidentAndAlert.js.map +1 -0
  95. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779303924241-AttachServiceToIncidentAndAlert.js +28 -0
  96. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779303924241-AttachServiceToIncidentAndAlert.js.map +1 -0
  97. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  98. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  99. package/build/dist/Server/Middleware/TelemetryIngest.js +2 -26
  100. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  101. package/build/dist/Server/Middleware/UserAuthorization.js +1 -7
  102. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  103. package/build/dist/Server/Services/AlertService.js +2 -1
  104. package/build/dist/Server/Services/AlertService.js.map +1 -1
  105. package/build/dist/Server/Services/AnalyticsDatabaseService.js +23 -2
  106. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  107. package/build/dist/Server/Services/IncidentService.js +2 -1
  108. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  109. package/build/dist/Server/Services/Index.js +0 -2
  110. package/build/dist/Server/Services/Index.js.map +1 -1
  111. package/build/dist/Server/Services/LogAggregationService.js +46 -4
  112. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  113. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +28 -24
  114. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  115. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js +1 -1
  116. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js.map +1 -1
  117. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +18 -2
  118. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  119. package/build/dist/Server/Services/OpenTelemetryIngestService.js +103 -0
  120. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  121. package/build/dist/Server/Services/TelemetryIngestionKeyService.js +83 -0
  122. package/build/dist/Server/Services/TelemetryIngestionKeyService.js.map +1 -1
  123. package/build/dist/Server/Services/TraceAggregationService.js +66 -6
  124. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
  125. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js +2 -1
  126. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js.map +1 -1
  127. package/build/dist/Server/Utils/Telemetry/Telemetry.js +26 -0
  128. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  129. package/build/dist/Tests/Server/Middleware/UserAuthorization.test.js +3 -7
  130. package/build/dist/Tests/Server/Middleware/UserAuthorization.test.js.map +1 -1
  131. package/build/dist/Types/Dashboard/DashboardComponentType.js +0 -1
  132. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  133. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +2 -0
  134. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  135. package/build/dist/Types/Dashboard/DashboardComponents/DashboardTableComponent.js +13 -1
  136. package/build/dist/Types/Dashboard/DashboardComponents/DashboardTableComponent.js.map +1 -1
  137. package/build/dist/Types/Dashboard/DashboardTemplates.js +142 -42
  138. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  139. package/build/dist/Types/Monitor/MonitorType.js +1 -1
  140. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  141. package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js +27 -7
  142. package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js.map +1 -1
  143. package/build/dist/Types/Permission.js +0 -40
  144. package/build/dist/Types/Permission.js.map +1 -1
  145. package/build/dist/Types/Telemetry/ServiceType.js +20 -0
  146. package/build/dist/Types/Telemetry/ServiceType.js.map +1 -0
  147. package/build/dist/UI/Components/Forms/Validation.js +2 -2
  148. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  149. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +106 -16
  150. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  151. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +67 -1
  152. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  153. package/build/dist/UI/Components/Telemetry/TelemetryRetentionConfigForm.js.map +1 -1
  154. package/build/dist/Utils/Dashboard/Components/DashboardTableComponent.js +68 -16
  155. package/build/dist/Utils/Dashboard/Components/DashboardTableComponent.js.map +1 -1
  156. package/build/dist/Utils/Dashboard/Components/Index.js +0 -4
  157. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  158. package/package.json +1 -2
  159. package/Models/DatabaseModels/ServiceDependency.ts +0 -529
  160. package/Server/Services/ServiceDependencyService.ts +0 -48
  161. package/Types/Dashboard/DashboardComponents/DashboardHostMetricChartComponent.ts +0 -27
  162. package/Typings/elkjs.d.ts +0 -30
  163. package/UI/Components/Graphs/ServiceDependencyGraph.tsx +0 -286
  164. package/Utils/Dashboard/Components/DashboardHostMetricChartComponent.ts +0 -132
  165. package/build/dist/Models/DatabaseModels/ServiceDependency.js +0 -545
  166. package/build/dist/Models/DatabaseModels/ServiceDependency.js.map +0 -1
  167. package/build/dist/Server/Services/ServiceDependencyService.js +0 -47
  168. package/build/dist/Server/Services/ServiceDependencyService.js.map +0 -1
  169. package/build/dist/Types/Dashboard/DashboardComponents/DashboardHostMetricChartComponent.js +0 -11
  170. package/build/dist/Types/Dashboard/DashboardComponents/DashboardHostMetricChartComponent.js.map +0 -1
  171. package/build/dist/UI/Components/Graphs/ServiceDependencyGraph.js +0 -206
  172. package/build/dist/UI/Components/Graphs/ServiceDependencyGraph.js.map +0 -1
  173. package/build/dist/Utils/Dashboard/Components/DashboardHostMetricChartComponent.js +0 -113
  174. package/build/dist/Utils/Dashboard/Components/DashboardHostMetricChartComponent.js.map +0 -1
@@ -8,6 +8,7 @@ import Includes from "../../Types/BaseDatabase/Includes";
8
8
  import AnalyticsTableName from "../../Types/AnalyticsDatabase/AnalyticsTableName";
9
9
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
10
10
  import { DbJSONResponse, Results } from "./AnalyticsDatabaseService";
11
+ import ServiceType from "../../Types/Telemetry/ServiceType";
11
12
 
12
13
  export interface HistogramBucket {
13
14
  time: string;
@@ -92,6 +93,19 @@ export class LogAggregationService {
92
93
  "traceId",
93
94
  "spanId",
94
95
  ]);
96
+ /*
97
+ * Virtual facet keys that don't correspond to real ClickHouse columns —
98
+ * they all read out of `serviceId` filtered by `serviceType`. The
99
+ * discriminator was added so host / docker host / k8s cluster telemetry
100
+ * could reuse the `serviceId` slot instead of synthesising phantom
101
+ * Service rows; these facets surface each resource type independently.
102
+ */
103
+ private static readonly RESOURCE_FACET_KEYS: Map<string, ServiceType> =
104
+ new Map([
105
+ ["hostId", ServiceType.Host],
106
+ ["dockerHostId", ServiceType.DockerHost],
107
+ ["kubernetesClusterId", ServiceType.KubernetesCluster],
108
+ ]);
95
109
  private static readonly ATTRIBUTE_KEY_PATTERN: RegExp = /^[a-zA-Z0-9._:/-]+$/;
96
110
  private static readonly MAX_FACET_KEY_LENGTH: number = 256;
97
111
 
@@ -192,13 +206,25 @@ export class LogAggregationService {
192
206
 
193
207
  LogAggregationService.validateFacetKey(request.facetKey);
194
208
 
195
- const isTopLevelColumn: boolean = LogAggregationService.isTopLevelColumn(
196
- request.facetKey,
197
- );
209
+ const resourceServiceType: ServiceType | undefined =
210
+ LogAggregationService.RESOURCE_FACET_KEYS.get(request.facetKey);
211
+ const isResourceFacet: boolean = resourceServiceType !== undefined;
212
+ const isTopLevelColumn: boolean =
213
+ isResourceFacet ||
214
+ LogAggregationService.isTopLevelColumn(request.facetKey);
198
215
 
199
216
  const statement: Statement = new Statement();
200
217
 
201
- if (isTopLevelColumn) {
218
+ if (isResourceFacet) {
219
+ /*
220
+ * Virtual facet — group serviceId values whose row carries the
221
+ * matching ServiceType discriminator (Host / DockerHost /
222
+ * KubernetesCluster).
223
+ */
224
+ statement.append(
225
+ SQL`SELECT toString(serviceId) AS val, count() AS cnt FROM ${LogAggregationService.TABLE_NAME}`,
226
+ );
227
+ } else if (isTopLevelColumn) {
202
228
  statement.append(
203
229
  SQL`SELECT toString(${request.facetKey}) AS val, count() AS cnt FROM ${LogAggregationService.TABLE_NAME}`,
204
230
  );
@@ -224,7 +250,26 @@ export class LogAggregationService {
224
250
  }}`,
225
251
  );
226
252
 
227
- if (!isTopLevelColumn) {
253
+ if (isResourceFacet) {
254
+ statement.append(
255
+ SQL` AND serviceType = ${{
256
+ type: TableColumnType.Text,
257
+ value: resourceServiceType as string,
258
+ }}`,
259
+ );
260
+ } else if (request.facetKey === "serviceId") {
261
+ /*
262
+ * Constrain the canonical Services facet to rows that actually
263
+ * belong to a Service. NULL / empty serviceType covers legacy
264
+ * rows ingested before the discriminator existed.
265
+ */
266
+ statement.append(
267
+ SQL` AND (serviceType = '' OR serviceType = ${{
268
+ type: TableColumnType.Text,
269
+ value: ServiceType.OpenTelemetry as string,
270
+ }})`,
271
+ );
272
+ } else if (!isTopLevelColumn) {
228
273
  statement.append(
229
274
  SQL` AND JSONHas(attributes, ${{
230
275
  type: TableColumnType.Text,
@@ -944,7 +989,10 @@ export class LogAggregationService {
944
989
  throw new BadDataException("Invalid facetKey");
945
990
  }
946
991
 
947
- if (LogAggregationService.isTopLevelColumn(facetKey)) {
992
+ if (
993
+ LogAggregationService.isTopLevelColumn(facetKey) ||
994
+ LogAggregationService.RESOURCE_FACET_KEYS.has(facetKey)
995
+ ) {
948
996
  return;
949
997
  }
950
998
 
@@ -77,6 +77,9 @@ export class Service extends DatabaseService<Model> {
77
77
  const userOnSchedule: ObjectID | null =
78
78
  await OnCallDutyPolicyScheduleService.getCurrentUserIdInSchedule(
79
79
  createdModel.onCallDutyPolicyScheduleId,
80
+ {
81
+ onCallDutyPolicyId: createdModel.onCallDutyPolicy?.id || undefined,
82
+ },
80
83
  );
81
84
 
82
85
  if (!userOnSchedule) {
@@ -277,6 +280,9 @@ export class Service extends DatabaseService<Model> {
277
280
  const userOnSchedule: ObjectID | null =
278
281
  await OnCallDutyPolicyScheduleService.getCurrentUserIdInSchedule(
279
282
  deletedItem.onCallDutyPolicyScheduleId!,
283
+ {
284
+ onCallDutyPolicyId: deletedItem.onCallDutyPolicy?.id || undefined,
285
+ },
280
286
  );
281
287
 
282
288
  if (!userOnSchedule) {
@@ -460,6 +460,7 @@ export class Service extends DatabaseService<Model> {
460
460
  const userIdInSchedule: ObjectID | null =
461
461
  await OnCallDutyPolicyScheduleService.getCurrentUserIdInSchedule(
462
462
  scheduleRule.onCallDutyPolicyScheduleId!,
463
+ { onCallDutyPolicyId: options.onCallPolicyId },
463
464
  );
464
465
 
465
466
  if (!userIdInSchedule) {
@@ -1052,17 +1052,28 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
1052
1052
  scheduleUserIds: Array<ObjectID>;
1053
1053
  windowStart: Date;
1054
1054
  windowEnd: Date;
1055
+ currentOnCallDutyPolicyId?: ObjectID | undefined;
1055
1056
  }): Promise<Array<UserOverrideRecord>> {
1056
1057
  if (data.scheduleUserIds.length === 0) {
1057
1058
  return [];
1058
1059
  }
1059
1060
 
1061
+ /*
1062
+ * When a policy context is provided, include overrides scoped to that
1063
+ * policy plus global overrides. Without a policy context (e.g. schedule
1064
+ * roster refresh, dashboard preview, incoming-call routing), only global
1065
+ * overrides apply — a policy-specific override from one policy must not
1066
+ * affect schedule resolution for a different policy.
1067
+ */
1060
1068
  const overrides: Array<OnCallDutyPolicyUserOverride> =
1061
1069
  await OnCallDutyPolicyUserOverrideService.findBy({
1062
1070
  query: {
1063
1071
  projectId: data.projectId,
1064
1072
  startsAt: QueryHelper.lessThanEqualTo(data.windowEnd),
1065
1073
  endsAt: QueryHelper.greaterThanEqualTo(data.windowStart),
1074
+ onCallDutyPolicyId: data.currentOnCallDutyPolicyId
1075
+ ? QueryHelper.equalToOrNull(data.currentOnCallDutyPolicyId)
1076
+ : QueryHelper.isNull(),
1066
1077
  },
1067
1078
  select: {
1068
1079
  startsAt: true,
@@ -1106,6 +1117,7 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
1106
1117
  public async getEventByIndexInSchedule(data: {
1107
1118
  scheduleId: ObjectID;
1108
1119
  getNumberOfEvents: number; // which event would you like to get. First event, second event, etc.
1120
+ onCallDutyPolicyId?: ObjectID | undefined;
1109
1121
  }): Promise<Array<CalendarEvent>> {
1110
1122
  logger.debug(
1111
1123
  "getEventByIndexInSchedule called with data: " + JSON.stringify(data),
@@ -1169,12 +1181,14 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
1169
1181
  scheduleUserIds,
1170
1182
  windowStart: currentStartTime,
1171
1183
  windowEnd: currentEndTime,
1184
+ currentOnCallDutyPolicyId: data.onCallDutyPolicyId,
1172
1185
  });
1173
1186
 
1174
1187
  if (overrides.length > 0) {
1175
1188
  events = UserOverrideUtil.applyOverridesToEvents({
1176
1189
  events,
1177
1190
  overrides,
1191
+ currentOnCallDutyPolicyId: data.onCallDutyPolicyId?.toString(),
1178
1192
  });
1179
1193
  }
1180
1194
  }
@@ -1189,6 +1203,7 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
1189
1203
  @CaptureSpan()
1190
1204
  public async getCurrentUserIdInSchedule(
1191
1205
  scheduleId: ObjectID,
1206
+ options?: { onCallDutyPolicyId?: ObjectID | undefined } | undefined,
1192
1207
  ): Promise<ObjectID | null> {
1193
1208
  const { layerProps, projectId, scheduleUserIds } =
1194
1209
  await this.getScheduleLayerProps({
@@ -1223,12 +1238,14 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
1223
1238
  scheduleUserIds,
1224
1239
  windowStart: currentStartTime,
1225
1240
  windowEnd: currentEndTime,
1241
+ currentOnCallDutyPolicyId: options?.onCallDutyPolicyId,
1226
1242
  });
1227
1243
 
1228
1244
  if (overrides.length > 0) {
1229
1245
  events = UserOverrideUtil.applyOverridesToEvents({
1230
1246
  events,
1231
1247
  overrides,
1248
+ currentOnCallDutyPolicyId: options?.onCallDutyPolicyId?.toString(),
1232
1249
  });
1233
1250
  }
1234
1251
  }
@@ -16,6 +16,13 @@ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
16
16
  import SortOrder from "../../Types/BaseDatabase/SortOrder";
17
17
  import logger from "../Utils/Logger";
18
18
  import TelemetryRetentionConfig from "../../Types/Telemetry/TelemetryRetentionConfig";
19
+ import ServiceType from "../../Types/Telemetry/ServiceType";
20
+ import Host from "../../Models/DatabaseModels/Host";
21
+ import DockerHost from "../../Models/DatabaseModels/DockerHost";
22
+ import KubernetesCluster from "../../Models/DatabaseModels/KubernetesCluster";
23
+ import HostService from "./HostService";
24
+ import DockerHostService from "./DockerHostService";
25
+ import KubernetesClusterService from "./KubernetesClusterService";
19
26
 
20
27
  export enum OtelAggregationTemporality {
21
28
  Cumulative = "AGGREGATION_TEMPORALITY_CUMULATIVE",
@@ -25,6 +32,14 @@ export enum OtelAggregationTemporality {
25
32
  export interface TelemetryServiceMetadata {
26
33
  serviceName: string;
27
34
  serviceId: ObjectID;
35
+ /*
36
+ * Discriminator stamped on every analytics row so the read side
37
+ * knows which Postgres table the `serviceId` actually points at
38
+ * (real Service, Host, DockerHost, KubernetesCluster, Monitor, …).
39
+ * Defaults to OpenTelemetry for legacy ingest paths that go through
40
+ * `telemetryServiceFromName`.
41
+ */
42
+ serviceType: ServiceType;
28
43
  dataRententionInDays: number;
29
44
  serviceRetentionConfig: TelemetryRetentionConfig | null;
30
45
  serviceRetentionInDays: number | null;
@@ -166,6 +181,7 @@ export default class OTelIngestService {
166
181
  return {
167
182
  serviceName: data.serviceName,
168
183
  serviceId: svc.id!,
184
+ serviceType: ServiceType.OpenTelemetry,
169
185
  dataRententionInDays:
170
186
  serviceLevelRetention || projectContext.projectRetentionInDays,
171
187
  serviceRetentionConfig: svc.telemetryRetentionConfig ?? null,
@@ -225,6 +241,122 @@ export default class OTelIngestService {
225
241
 
226
242
  return buildMetadata(service);
227
243
  }
244
+
245
+ /*
246
+ * Builds a TelemetryServiceMetadata for a non-Service resource —
247
+ * Host, DockerHost, KubernetesCluster, Monitor. These resources
248
+ * own telemetry directly via their own Postgres id (stamped into
249
+ * the analytics row's `serviceId` column) and do not have a paired
250
+ * Service row. Per-resource retention overrides (when set on the
251
+ * Host / DockerHost / KubernetesCluster row) are honoured here so
252
+ * the resource owner can keep host telemetry longer/shorter than
253
+ * the project default. Monitor / unknown resource types fall back
254
+ * to the project retention.
255
+ */
256
+ @CaptureSpan()
257
+ public static async buildResourceMetadataForNonService(data: {
258
+ serviceName: string;
259
+ resourceId: ObjectID;
260
+ serviceType: ServiceType;
261
+ projectId: ObjectID;
262
+ }): Promise<TelemetryServiceMetadata> {
263
+ const projectContext: ProjectRetentionContext =
264
+ await this.getProjectRetentionContext(data.projectId);
265
+
266
+ const resourceRetention: {
267
+ retainTelemetryDataForDays: number | null;
268
+ telemetryRetentionConfig: TelemetryRetentionConfig | null;
269
+ } = await this.getResourceRetention(data.resourceId, data.serviceType);
270
+
271
+ return {
272
+ serviceName: data.serviceName,
273
+ serviceId: data.resourceId,
274
+ serviceType: data.serviceType,
275
+ dataRententionInDays:
276
+ resourceRetention.retainTelemetryDataForDays ||
277
+ projectContext.projectRetentionInDays,
278
+ serviceRetentionConfig: resourceRetention.telemetryRetentionConfig,
279
+ serviceRetentionInDays: resourceRetention.retainTelemetryDataForDays,
280
+ projectRetentionConfig: projectContext.projectRetentionConfig,
281
+ projectRetentionInDays: projectContext.projectRetentionInDays,
282
+ };
283
+ }
284
+
285
+ /*
286
+ * Look up per-resource retention overrides for non-Service telemetry.
287
+ * One small SELECT per batch — the caller caches the resulting
288
+ * TelemetryServiceMetadata under `serviceDictionary[serviceName]`
289
+ * so steady-state ingest skips this lookup after the first row.
290
+ */
291
+ @CaptureSpan()
292
+ private static async getResourceRetention(
293
+ resourceId: ObjectID,
294
+ serviceType: ServiceType,
295
+ ): Promise<{
296
+ retainTelemetryDataForDays: number | null;
297
+ telemetryRetentionConfig: TelemetryRetentionConfig | null;
298
+ }> {
299
+ try {
300
+ if (serviceType === ServiceType.Host) {
301
+ const host: Host | null = await HostService.findOneById({
302
+ id: resourceId,
303
+ select: {
304
+ retainTelemetryDataForDays: true,
305
+ telemetryRetentionConfig: true,
306
+ },
307
+ props: { isRoot: true },
308
+ });
309
+ return {
310
+ retainTelemetryDataForDays: host?.retainTelemetryDataForDays ?? null,
311
+ telemetryRetentionConfig: host?.telemetryRetentionConfig ?? null,
312
+ };
313
+ }
314
+ if (serviceType === ServiceType.DockerHost) {
315
+ const dockerHost: DockerHost | null =
316
+ await DockerHostService.findOneById({
317
+ id: resourceId,
318
+ select: {
319
+ retainTelemetryDataForDays: true,
320
+ telemetryRetentionConfig: true,
321
+ },
322
+ props: { isRoot: true },
323
+ });
324
+ return {
325
+ retainTelemetryDataForDays:
326
+ dockerHost?.retainTelemetryDataForDays ?? null,
327
+ telemetryRetentionConfig:
328
+ dockerHost?.telemetryRetentionConfig ?? null,
329
+ };
330
+ }
331
+ if (serviceType === ServiceType.KubernetesCluster) {
332
+ const cluster: KubernetesCluster | null =
333
+ await KubernetesClusterService.findOneById({
334
+ id: resourceId,
335
+ select: {
336
+ retainTelemetryDataForDays: true,
337
+ telemetryRetentionConfig: true,
338
+ },
339
+ props: { isRoot: true },
340
+ });
341
+ return {
342
+ retainTelemetryDataForDays:
343
+ cluster?.retainTelemetryDataForDays ?? null,
344
+ telemetryRetentionConfig: cluster?.telemetryRetentionConfig ?? null,
345
+ };
346
+ }
347
+ } catch (err) {
348
+ logger.warn(
349
+ `Per-resource retention lookup failed for ${serviceType} ${resourceId.toString()}: ${
350
+ err instanceof Error ? err.message : String(err)
351
+ }`,
352
+ );
353
+ }
354
+ return {
355
+ retainTelemetryDataForDays: null,
356
+ telemetryRetentionConfig: null,
357
+ };
358
+ }
359
+
228
360
  @CaptureSpan()
229
361
  public static getMetricFromDatapoint(data: {
230
362
  dbMetric: Metric;
@@ -1,11 +1,30 @@
1
1
  import CreateBy from "../Types/Database/CreateBy";
2
- import { OnCreate } from "../Types/Database/Hooks";
2
+ import DeleteBy from "../Types/Database/DeleteBy";
3
+ import UpdateBy from "../Types/Database/UpdateBy";
4
+ import { OnCreate, OnDelete, OnUpdate } from "../Types/Database/Hooks";
3
5
  import DatabaseService from "./DatabaseService";
4
6
  import ObjectID from "../../Types/ObjectID";
5
7
  import Model from "../../Models/DatabaseModels/TelemetryIngestionKey";
6
8
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
9
+ import InMemoryTTLCache from "../Infrastructure/InMemoryTTLCache";
10
+ import { createHash } from "crypto";
11
+
12
+ /*
13
+ * 60s is the worst-case staleness on any single API node after a key is
14
+ * revoked from the dashboard. We invalidate in-process immediately on
15
+ * delete/update; this TTL is the upper bound for *other* processes.
16
+ */
17
+ const POSITIVE_TTL_MS: number = 60 * 1000;
18
+ /*
19
+ * Short TTL on misses so an invalid-token flood can't pin entries in the
20
+ * bounded cache for long while still absorbing repeat hits.
21
+ */
22
+ const NEGATIVE_TTL_MS: number = 10 * 1000;
7
23
 
8
24
  export class Service extends DatabaseService<Model> {
25
+ private projectIdCache: InMemoryTTLCache<string | null> =
26
+ new InMemoryTTLCache(10_000);
27
+
9
28
  public constructor() {
10
29
  super(Model);
11
30
  }
@@ -20,6 +39,76 @@ export class Service extends DatabaseService<Model> {
20
39
 
21
40
  return { createBy, carryForward: null };
22
41
  }
42
+
43
+ @CaptureSpan()
44
+ protected override async onBeforeDelete(
45
+ deleteBy: DeleteBy<Model>,
46
+ ): Promise<OnDelete<Model>> {
47
+ /*
48
+ * We don't know which secretKey(s) are being deleted without an extra
49
+ * query; clear the whole cache. Key deletes are rare so this is cheap.
50
+ */
51
+ this.projectIdCache.clear();
52
+ return { deleteBy, carryForward: null };
53
+ }
54
+
55
+ @CaptureSpan()
56
+ protected override async onBeforeUpdate(
57
+ updateBy: UpdateBy<Model>,
58
+ ): Promise<OnUpdate<Model>> {
59
+ /*
60
+ * Same reasoning as onBeforeDelete. secretKey is not user-editable today
61
+ * but projectId could change, so be conservative.
62
+ */
63
+ this.projectIdCache.clear();
64
+ return { updateBy, carryForward: null };
65
+ }
66
+
67
+ /**
68
+ * Resolve an ingestion token to its projectId, with a short-lived
69
+ * in-process cache to keep the hot ingest path off Postgres. Returns null
70
+ * for unknown, malformed, or revoked tokens (also cached, for a shorter
71
+ * TTL).
72
+ */
73
+ @CaptureSpan()
74
+ public async getProjectIdFromSecretKey(
75
+ secretKey: string,
76
+ ): Promise<ObjectID | null> {
77
+ const cacheKey: string = createHash("sha256")
78
+ .update(secretKey)
79
+ .digest("hex");
80
+
81
+ const cached: string | null | undefined = this.projectIdCache.get(cacheKey);
82
+ if (cached !== undefined) {
83
+ return cached === null ? null : new ObjectID(cached);
84
+ }
85
+
86
+ let secretKeyObjectId: ObjectID;
87
+ try {
88
+ secretKeyObjectId = new ObjectID(secretKey);
89
+ } catch {
90
+ this.projectIdCache.set(cacheKey, null, NEGATIVE_TTL_MS);
91
+ return null;
92
+ }
93
+
94
+ const token: Model | null = await this.findOneBy({
95
+ query: { secretKey: secretKeyObjectId },
96
+ select: { projectId: true },
97
+ props: { isRoot: true },
98
+ });
99
+
100
+ const projectId: ObjectID | undefined = token?.projectId as
101
+ | ObjectID
102
+ | undefined;
103
+
104
+ if (!projectId) {
105
+ this.projectIdCache.set(cacheKey, null, NEGATIVE_TTL_MS);
106
+ return null;
107
+ }
108
+
109
+ this.projectIdCache.set(cacheKey, projectId.toString(), POSITIVE_TTL_MS);
110
+ return projectId;
111
+ }
23
112
  }
24
113
 
25
114
  export default new Service();
@@ -9,6 +9,7 @@ import AnalyticsTableName from "../../Types/AnalyticsDatabase/AnalyticsTableName
9
9
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
10
10
  import { DbJSONResponse, Results } from "./AnalyticsDatabaseService";
11
11
  import logger from "../Utils/Logger";
12
+ import ServiceType from "../../Types/Telemetry/ServiceType";
12
13
 
13
14
  export interface HistogramBucket {
14
15
  time: string;
@@ -69,6 +70,17 @@ export class TraceAggregationService {
69
70
  "statusCode",
70
71
  "isRootSpan",
71
72
  ]);
73
+ /*
74
+ * Virtual facet keys — same scheme as LogAggregationService. The
75
+ * `serviceId` slot is reused for host / docker host / k8s cluster ids,
76
+ * disambiguated by the `serviceType` discriminator.
77
+ */
78
+ private static readonly RESOURCE_FACET_KEYS: Map<string, ServiceType> =
79
+ new Map([
80
+ ["hostId", ServiceType.Host],
81
+ ["dockerHostId", ServiceType.DockerHost],
82
+ ["kubernetesClusterId", ServiceType.KubernetesCluster],
83
+ ]);
72
84
  private static readonly ATTRIBUTE_KEY_PATTERN: RegExp = /^[a-zA-Z0-9._:/-]+$/;
73
85
  private static readonly MAX_FACET_KEY_LENGTH: number = 256;
74
86
 
@@ -156,14 +168,25 @@ export class TraceAggregationService {
156
168
  TraceAggregationService.validateFacetKey(facetKey);
157
169
  }
158
170
 
171
+ const resourceKeys: Array<string> = request.facetKeys.filter(
172
+ (key: string): boolean => {
173
+ return TraceAggregationService.RESOURCE_FACET_KEYS.has(key);
174
+ },
175
+ );
159
176
  const topLevelKeys: Array<string> = request.facetKeys.filter(
160
177
  (key: string): boolean => {
161
- return TraceAggregationService.isTopLevelColumn(key);
178
+ return (
179
+ TraceAggregationService.isTopLevelColumn(key) &&
180
+ !TraceAggregationService.RESOURCE_FACET_KEYS.has(key)
181
+ );
162
182
  },
163
183
  );
164
184
  const attributeKeys: Array<string> = request.facetKeys.filter(
165
185
  (key: string): boolean => {
166
- return !TraceAggregationService.isTopLevelColumn(key);
186
+ return (
187
+ !TraceAggregationService.isTopLevelColumn(key) &&
188
+ !TraceAggregationService.RESOURCE_FACET_KEYS.has(key)
189
+ );
167
190
  },
168
191
  );
169
192
 
@@ -171,6 +194,13 @@ export class TraceAggregationService {
171
194
  if (topLevelKeys.length > 0) {
172
195
  selectColumns.push(...topLevelKeys);
173
196
  }
197
+ if (resourceKeys.length > 0) {
198
+ // Virtual facets read out of serviceId disambiguated by serviceType.
199
+ if (!selectColumns.includes("serviceId")) {
200
+ selectColumns.push("serviceId");
201
+ }
202
+ selectColumns.push("serviceType");
203
+ }
174
204
  if (attributeKeys.length > 0) {
175
205
  selectColumns.push("attributes");
176
206
  }
@@ -244,6 +274,27 @@ export class TraceAggregationService {
244
274
  map.set(value, (map.get(value) || 0) + 1);
245
275
  }
246
276
 
277
+ if (resourceKeys.length > 0) {
278
+ const rowServiceType: string =
279
+ row["serviceType"] === undefined || row["serviceType"] === null
280
+ ? ""
281
+ : String(row["serviceType"]);
282
+ const rowServiceId: unknown = row["serviceId"];
283
+ if (rowServiceId !== undefined && rowServiceId !== null) {
284
+ const value: string = String(rowServiceId);
285
+ if (value.length > 0) {
286
+ for (const key of resourceKeys) {
287
+ const expected: ServiceType | undefined =
288
+ TraceAggregationService.RESOURCE_FACET_KEYS.get(key);
289
+ if (expected && rowServiceType === expected) {
290
+ const map: Map<string, number> = counts[key]!;
291
+ map.set(value, (map.get(value) || 0) + 1);
292
+ }
293
+ }
294
+ }
295
+ }
296
+ }
297
+
247
298
  if (attributeKeys.length > 0) {
248
299
  const attrs: unknown = row["attributes"];
249
300
  let parsed: Record<string, unknown> | null = null;
@@ -368,13 +419,20 @@ export class TraceAggregationService {
368
419
 
369
420
  TraceAggregationService.validateFacetKey(request.facetKey);
370
421
 
371
- const isTopLevelColumn: boolean = TraceAggregationService.isTopLevelColumn(
372
- request.facetKey,
373
- );
422
+ const resourceServiceType: ServiceType | undefined =
423
+ TraceAggregationService.RESOURCE_FACET_KEYS.get(request.facetKey);
424
+ const isResourceFacet: boolean = resourceServiceType !== undefined;
425
+ const isTopLevelColumn: boolean =
426
+ isResourceFacet ||
427
+ TraceAggregationService.isTopLevelColumn(request.facetKey);
374
428
 
375
429
  const statement: Statement = new Statement();
376
430
 
377
- if (isTopLevelColumn) {
431
+ if (isResourceFacet) {
432
+ statement.append(
433
+ SQL`SELECT toString(serviceId) AS val, count() AS cnt FROM ${TraceAggregationService.TABLE_NAME}`,
434
+ );
435
+ } else if (isTopLevelColumn) {
378
436
  statement.append(
379
437
  SQL`SELECT toString(${request.facetKey}) AS val, count() AS cnt FROM ${TraceAggregationService.TABLE_NAME}`,
380
438
  );
@@ -400,7 +458,21 @@ export class TraceAggregationService {
400
458
  }}`,
401
459
  );
402
460
 
403
- if (!isTopLevelColumn) {
461
+ if (isResourceFacet) {
462
+ statement.append(
463
+ SQL` AND serviceType = ${{
464
+ type: TableColumnType.Text,
465
+ value: resourceServiceType as string,
466
+ }}`,
467
+ );
468
+ } else if (request.facetKey === "serviceId") {
469
+ statement.append(
470
+ SQL` AND (serviceType = '' OR serviceType = ${{
471
+ type: TableColumnType.Text,
472
+ value: ServiceType.OpenTelemetry as string,
473
+ }})`,
474
+ );
475
+ } else if (!isTopLevelColumn) {
404
476
  statement.append(
405
477
  SQL` AND JSONHas(attributes, ${{
406
478
  type: TableColumnType.Text,
@@ -539,7 +611,10 @@ export class TraceAggregationService {
539
611
  throw new BadDataException("Invalid facetKey");
540
612
  }
541
613
 
542
- if (TraceAggregationService.isTopLevelColumn(facetKey)) {
614
+ if (
615
+ TraceAggregationService.isTopLevelColumn(facetKey) ||
616
+ TraceAggregationService.RESOURCE_FACET_KEYS.has(facetKey)
617
+ ) {
543
618
  return;
544
619
  }
545
620
 
@@ -5,10 +5,8 @@ import MetricService from "../../Services/MetricService";
5
5
  import GlobalConfigService from "../../Services/GlobalConfigService";
6
6
  import GlobalConfig from "../../../Models/DatabaseModels/GlobalConfig";
7
7
  import DataToProcess from "./DataToProcess";
8
- import {
9
- MetricPointType,
10
- ServiceType,
11
- } from "../../../Models/AnalyticsModels/Metric";
8
+ import { MetricPointType } from "../../../Models/AnalyticsModels/Metric";
9
+ import ServiceType from "../../../Types/Telemetry/ServiceType";
12
10
  import MetricType from "../../../Models/DatabaseModels/MetricType";
13
11
  import BasicInfrastructureMetrics, {
14
12
  NetworkInterfaceMetrics,
@@ -135,6 +135,44 @@ export default class TelemetryUtil {
135
135
  };
136
136
  }
137
137
 
138
+ /*
139
+ * Cross-cutting resource stamps. Spread alongside the service stamp on
140
+ * every analytics row so a single ClickHouse query can find "all
141
+ * telemetry from host X" / "from docker host Y" / "from cluster Z"
142
+ * regardless of which Service the row primarily belongs to. Without
143
+ * these, host context would only be queryable as the raw OTel
144
+ * `resource.host.name` string with no stable id link.
145
+ */
146
+ public static getAttributesForHostIdAndHostName(data: {
147
+ hostId: ObjectID;
148
+ hostName: string;
149
+ }): Dictionary<AttributeType> {
150
+ return {
151
+ "oneuptime.host.id": data.hostId.toString(),
152
+ "oneuptime.host.name": data.hostName,
153
+ };
154
+ }
155
+
156
+ public static getAttributesForDockerHostIdAndHostName(data: {
157
+ dockerHostId: ObjectID;
158
+ hostName: string;
159
+ }): Dictionary<AttributeType> {
160
+ return {
161
+ "oneuptime.docker.host.id": data.dockerHostId.toString(),
162
+ "oneuptime.docker.host.name": data.hostName,
163
+ };
164
+ }
165
+
166
+ public static getAttributesForKubernetesClusterIdAndName(data: {
167
+ kubernetesClusterId: ObjectID;
168
+ clusterName: string;
169
+ }): Dictionary<AttributeType> {
170
+ return {
171
+ "oneuptime.kubernetes.cluster.id": data.kubernetesClusterId.toString(),
172
+ "oneuptime.kubernetes.cluster.name": data.clusterName,
173
+ };
174
+ }
175
+
138
176
  @CaptureSpan()
139
177
  public static getAttributes(data: {
140
178
  prefixKeysWithString: string;