@oneuptime/common 12.0.12 → 12.0.13

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 (234) hide show
  1. package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +32 -5
  2. package/Models/DatabaseModels/NetworkDevice.ts +64 -0
  3. package/Models/DatabaseModels/StatusPage.ts +59 -0
  4. package/Models/DatabaseModels/StatusPageOidc.ts +2 -0
  5. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +19 -2
  6. package/Server/API/AIChatAPI.ts +55 -56
  7. package/Server/API/CommonAPI.ts +89 -0
  8. package/Server/API/MicrosoftTeamsAPI.ts +20 -9
  9. package/Server/API/SlackAPI.ts +13 -10
  10. package/Server/API/StatusPageAPI.ts +2197 -2128
  11. package/Server/API/TelemetryAPI.ts +72 -3
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.ts +30 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.ts +47 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  15. package/Server/Middleware/UserAuthorization.ts +11 -2
  16. package/Server/Services/ApiKeyPermissionService.ts +116 -2
  17. package/Server/Services/DatabaseService.ts +17 -6
  18. package/Server/Services/LogAggregationService.ts +34 -0
  19. package/Server/Services/LogService.ts +21 -0
  20. package/Server/Services/NetworkSiteService.ts +12 -0
  21. package/Server/Services/SpanService.ts +21 -0
  22. package/Server/Services/TraceAggregationService.ts +15 -0
  23. package/Server/Services/WorkspaceNotificationRuleService.ts +172 -220
  24. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +9 -0
  25. package/Server/Types/Database/Permissions/AccessControlPermission.ts +47 -16
  26. package/Server/Utils/APIKey/AccessPermission.ts +16 -40
  27. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +144 -0
  28. package/Server/Utils/LogRedaction.ts +576 -0
  29. package/Server/Utils/Logger.ts +123 -46
  30. package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +94 -23
  31. package/Server/Utils/Monitor/MonitorResource.ts +29 -9
  32. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +28 -7
  33. package/Server/Utils/SessionReplay/SessionReplayGateCacheStore.ts +56 -10
  34. package/Server/Utils/StartServer.ts +30 -9
  35. package/Server/Utils/StatusPageSearchEngineIndexing.ts +33 -0
  36. package/Server/Utils/Telemetry/ResourceEntityFilter.ts +441 -0
  37. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +488 -46
  38. package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +300 -0
  39. package/Tests/App/StatusPage/StatusPageIndexPageRobotsMeta.test.ts +130 -0
  40. package/Tests/Models/DatabaseModels/DatabaseBaseModelToJSONObjectCache.test.ts +180 -0
  41. package/Tests/Models/StatusPageEnableSearchEngineIndexing.test.ts +157 -0
  42. package/Tests/Server/API/AIChatCancelAndFeedback.test.ts +23 -1
  43. package/Tests/Server/API/AIChatRouteAuthorization.test.ts +785 -0
  44. package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +21 -8
  45. package/Tests/Server/API/CommonAPIAuthGuard.test.ts +290 -0
  46. package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +7 -1
  47. package/Tests/Server/API/StatusPageOverviewCache.test.ts +474 -0
  48. package/Tests/Server/API/StatusPageSeoSearchEngineIndexing.test.ts +189 -0
  49. package/Tests/Server/Infrastructure/InMemoryTTLCache.test.ts +158 -0
  50. package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +9 -60
  51. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +24 -15
  52. package/Tests/Server/Middleware/UserAuthorization.test.ts +79 -0
  53. package/Tests/Server/Services/AddNetworkDeviceReachabilityColumnsMigration.test.ts +332 -0
  54. package/Tests/Server/Services/ApiKeyPermissionService.test.ts +347 -0
  55. package/Tests/Server/Services/DatabaseServiceUpdateDebugLogging.test.ts +282 -0
  56. package/Tests/Server/Services/NetworkSiteService.test.ts +56 -2
  57. package/Tests/Server/Services/TelemetryResourceFacetFilters.test.ts +267 -0
  58. package/Tests/Server/Services/WorkspaceNotificationRuleTestRuleChannels.test.ts +702 -0
  59. package/Tests/Server/Types/Database/Permissions/AccessControlPermission.test.ts +165 -0
  60. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +48 -36
  61. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +213 -0
  62. package/Tests/Server/Utils/LogRedaction.test.ts +415 -0
  63. package/Tests/Server/Utils/LoggerCredentialLeak.test.ts +245 -0
  64. package/Tests/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.test.ts +374 -4
  65. package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +455 -0
  66. package/Tests/Server/Utils/Monitor/NetworkDeviceReachabilityRoundTrip.test.ts +369 -0
  67. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +86 -8
  68. package/Tests/Server/Utils/PrivacyFilterUtil.test.ts +64 -0
  69. package/Tests/Server/Utils/SessionReplay/SessionReplayGateCacheStore.test.ts +207 -0
  70. package/Tests/Server/Utils/StartServerBodyVerify.test.ts +335 -0
  71. package/Tests/Server/Utils/StatusPageSearchEngineIndexing.test.ts +60 -0
  72. package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +435 -0
  73. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelSend.test.ts +607 -62
  74. package/Tests/Server/Utils/Workspace/MicrosoftTeamsInstalledTeamIdSpace.test.ts +778 -0
  75. package/Tests/Server/Utils/Workspace/MicrosoftTeamsTeamInstalls.test.ts +44 -10
  76. package/Tests/Types/API/HostnameFromAuthority.test.ts +164 -0
  77. package/Tests/Types/Database/AccessControl/ColumnAccessControlCache.test.ts +344 -0
  78. package/Tests/Types/Database/DatabasePropertyFindOperator.test.ts +518 -0
  79. package/Tests/Types/Database/TableColumnMetadataCache.test.ts +286 -0
  80. package/Tests/Types/Monitor/MonitorStep.test.ts +100 -0
  81. package/Tests/Types/Monitor/MonitorTypeKeywords.test.ts +360 -0
  82. package/Tests/Types/StatusPage/SearchEngineIndexing.test.ts +86 -0
  83. package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +254 -0
  84. package/Tests/UI/Components/CardSelect.test.tsx +1021 -0
  85. package/Tests/UI/Components/MasterPage.test.tsx +36 -1
  86. package/Tests/Utils/Monitor/MonitorMetricType.test.ts +66 -1
  87. package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +115 -6
  88. package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +610 -0
  89. package/Tests/Utils/NetworkSite/SiteStatusRollupUtil.test.ts +114 -45
  90. package/Types/API/Hostname.ts +79 -0
  91. package/Types/Database/AccessControl/ColumnAccessControl.ts +33 -11
  92. package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +33 -11
  93. package/Types/Database/DatabaseProperty.ts +38 -0
  94. package/Types/Database/TableColumn.ts +34 -7
  95. package/Types/Events/Recurring.ts +16 -1
  96. package/Types/Monitor/MonitorStep.ts +13 -1
  97. package/Types/Monitor/MonitorType.ts +345 -40
  98. package/Types/Monitor/SSLMonitor/SslMonitorResponse.ts +43 -0
  99. package/Types/StatusPage/SearchEngineIndexing.ts +61 -0
  100. package/Types/Telemetry/ResourceEntityFacet.ts +211 -0
  101. package/UI/Components/CardSelect/CardSelect.tsx +714 -105
  102. package/UI/Components/EmptyState/EmptyState.tsx +8 -1
  103. package/UI/Components/Footer/Footer.tsx +32 -6
  104. package/UI/Components/Forms/Fields/FormField.tsx +3 -0
  105. package/UI/Components/Forms/Types/Field.ts +8 -0
  106. package/UI/Components/Loader/PageLoader.tsx +29 -8
  107. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +26 -4
  108. package/UI/Components/Markdown.tsx/LazyMarkdownViewer.tsx +17 -1
  109. package/UI/Components/MasterPage/MasterPage.tsx +24 -11
  110. package/Utils/Monitor/MonitorMetricType.ts +16 -1
  111. package/Utils/Monitor/NetworkTopologyUtil.ts +60 -8
  112. package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +262 -0
  113. package/Utils/NetworkSite/SiteStatusRollupUtil.ts +41 -24
  114. package/Utils/Telemetry/CrossSignalScope.ts +75 -4
  115. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +20 -4
  116. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
  117. package/build/dist/Models/DatabaseModels/NetworkDevice.js +66 -0
  118. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  119. package/build/dist/Models/DatabaseModels/StatusPage.js +60 -0
  120. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  121. package/build/dist/Models/DatabaseModels/StatusPageOidc.js +2 -0
  122. package/build/dist/Models/DatabaseModels/StatusPageOidc.js.map +1 -1
  123. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  124. package/build/dist/Server/API/AIChatAPI.js +48 -35
  125. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  126. package/build/dist/Server/API/CommonAPI.js +59 -0
  127. package/build/dist/Server/API/CommonAPI.js.map +1 -1
  128. package/build/dist/Server/API/MicrosoftTeamsAPI.js +20 -6
  129. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  130. package/build/dist/Server/API/SlackAPI.js +13 -10
  131. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  132. package/build/dist/Server/API/StatusPageAPI.js +745 -673
  133. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  134. package/build/dist/Server/API/TelemetryAPI.js +34 -3
  135. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  136. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js +23 -0
  137. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js.map +1 -0
  138. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.js +34 -0
  139. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.js.map +1 -0
  140. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  141. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  142. package/build/dist/Server/Middleware/UserAuthorization.js +6 -2
  143. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  144. package/build/dist/Server/Services/ApiKeyPermissionService.js +90 -1
  145. package/build/dist/Server/Services/ApiKeyPermissionService.js.map +1 -1
  146. package/build/dist/Server/Services/DatabaseService.js +15 -6
  147. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  148. package/build/dist/Server/Services/LogAggregationService.js +2 -0
  149. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  150. package/build/dist/Server/Services/LogService.js +16 -0
  151. package/build/dist/Server/Services/LogService.js.map +1 -1
  152. package/build/dist/Server/Services/NetworkSiteService.js +12 -0
  153. package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
  154. package/build/dist/Server/Services/SpanService.js +16 -0
  155. package/build/dist/Server/Services/SpanService.js.map +1 -1
  156. package/build/dist/Server/Services/TraceAggregationService.js +2 -0
  157. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
  158. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +137 -164
  159. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  160. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +9 -0
  161. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  162. package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js +21 -13
  163. package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js.map +1 -1
  164. package/build/dist/Server/Utils/APIKey/AccessPermission.js +9 -32
  165. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  166. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +87 -0
  167. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  168. package/build/dist/Server/Utils/LogRedaction.js +449 -0
  169. package/build/dist/Server/Utils/LogRedaction.js.map +1 -0
  170. package/build/dist/Server/Utils/Logger.js +98 -45
  171. package/build/dist/Server/Utils/Logger.js.map +1 -1
  172. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +65 -13
  173. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
  174. package/build/dist/Server/Utils/Monitor/MonitorResource.js +20 -3
  175. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  176. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +27 -7
  177. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  178. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js +29 -10
  179. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js.map +1 -1
  180. package/build/dist/Server/Utils/StartServer.js +10 -7
  181. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  182. package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js +24 -0
  183. package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js.map +1 -0
  184. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +311 -0
  185. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -0
  186. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +366 -37
  187. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  188. package/build/dist/Types/API/Hostname.js +63 -0
  189. package/build/dist/Types/API/Hostname.js.map +1 -1
  190. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +20 -6
  191. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
  192. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +20 -6
  193. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
  194. package/build/dist/Types/Database/DatabaseProperty.js +38 -0
  195. package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
  196. package/build/dist/Types/Database/TableColumn.js +24 -6
  197. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  198. package/build/dist/Types/Events/Recurring.js +13 -1
  199. package/build/dist/Types/Events/Recurring.js.map +1 -1
  200. package/build/dist/Types/Monitor/MonitorStep.js +9 -1
  201. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  202. package/build/dist/Types/Monitor/MonitorType.js +330 -31
  203. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  204. package/build/dist/Types/StatusPage/SearchEngineIndexing.js +55 -0
  205. package/build/dist/Types/StatusPage/SearchEngineIndexing.js.map +1 -0
  206. package/build/dist/Types/Telemetry/ResourceEntityFacet.js +156 -0
  207. package/build/dist/Types/Telemetry/ResourceEntityFacet.js.map +1 -0
  208. package/build/dist/UI/Components/CardSelect/CardSelect.js +360 -40
  209. package/build/dist/UI/Components/CardSelect/CardSelect.js.map +1 -1
  210. package/build/dist/UI/Components/EmptyState/EmptyState.js +1 -1
  211. package/build/dist/UI/Components/EmptyState/EmptyState.js.map +1 -1
  212. package/build/dist/UI/Components/Footer/Footer.js +7 -4
  213. package/build/dist/UI/Components/Footer/Footer.js.map +1 -1
  214. package/build/dist/UI/Components/Forms/Fields/FormField.js +1 -1
  215. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  216. package/build/dist/UI/Components/Loader/PageLoader.js +15 -5
  217. package/build/dist/UI/Components/Loader/PageLoader.js.map +1 -1
  218. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +16 -3
  219. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  220. package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js +13 -1
  221. package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js.map +1 -1
  222. package/build/dist/UI/Components/MasterPage/MasterPage.js +10 -2
  223. package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
  224. package/build/dist/Utils/Monitor/MonitorMetricType.js +13 -1
  225. package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
  226. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +34 -7
  227. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  228. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +177 -0
  229. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -0
  230. package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js +23 -27
  231. package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js.map +1 -1
  232. package/build/dist/Utils/Telemetry/CrossSignalScope.js +44 -4
  233. package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
  234. package/package.json +1 -1
@@ -95,6 +95,10 @@ import ResourceFacetResolver, {
95
95
  ResolvedFacetValue,
96
96
  ResourceFacetSpec,
97
97
  } from "../Utils/Telemetry/ResourceFacetResolver";
98
+ import ResourceEntityFilter, {
99
+ ResourceEntityScope,
100
+ } from "../Utils/Telemetry/ResourceEntityFilter";
101
+ import { ResourceEntityFacetSelections } from "../../Types/Telemetry/ResourceEntityFacet";
98
102
  import Label from "../../Models/DatabaseModels/Label";
99
103
  import RumApplication from "../../Models/DatabaseModels/RumApplication";
100
104
  import RumApplicationService from "../Services/RumApplicationService";
@@ -456,6 +460,9 @@ router.post(
456
460
  ? (body["attributes"] as Record<string, string | Array<string>>)
457
461
  : undefined;
458
462
 
463
+ const resourceScopes: Array<ResourceEntityScope> | undefined =
464
+ await resolveResourceScopesFromBody(body, databaseProps.tenantId);
465
+
459
466
  const request: HistogramRequest = {
460
467
  projectId: databaseProps.tenantId,
461
468
  startTime,
@@ -463,6 +470,7 @@ router.post(
463
470
  bucketSizeInMinutes,
464
471
  serviceIds,
465
472
  entityKeys,
473
+ resourceScopes,
466
474
  severityTexts,
467
475
  bodySearchText,
468
476
  traceIds,
@@ -573,6 +581,9 @@ router.post(
573
581
  */
574
582
  const projectId: ObjectID = databaseProps.tenantId;
575
583
 
584
+ const resourceScopes: Array<ResourceEntityScope> | undefined =
585
+ await resolveResourceScopesFromBody(body, projectId);
586
+
576
587
  /*
577
588
  * Run facet queries in parallel so a slow individual facet can't
578
589
  * starve the endpoint. Per-facet errors degrade gracefully to [].
@@ -592,6 +603,7 @@ router.post(
592
603
  limit,
593
604
  serviceIds,
594
605
  entityKeys,
606
+ resourceScopes,
595
607
  severityTexts,
596
608
  bodySearchText,
597
609
  traceIds,
@@ -656,6 +668,41 @@ router.post(
656
668
  },
657
669
  );
658
670
 
671
+ /**
672
+ * Resolve the `resourceFilters` field — the explorers' non-Service resource
673
+ * facet selections (Kubernetes cluster / host / docker host / podman host),
674
+ * sent as Postgres ids — into the scopes the aggregation services compile.
675
+ *
676
+ * Kept out of the sync body parsers because turning an id into an entity
677
+ * key needs a Postgres round trip: the id names a row whose identifying
678
+ * value (`clusterIdentifier` / `hostIdentifier`) is what ingest hashed into
679
+ * `entityKeys`.
680
+ *
681
+ * Returns undefined when nothing was selected so the request field stays
682
+ * absent rather than becoming an empty array.
683
+ */
684
+ async function resolveResourceScopesFromBody(
685
+ body: JSONObject,
686
+ projectId: ObjectID,
687
+ ): Promise<Array<ResourceEntityScope> | undefined> {
688
+ const selections: ResourceEntityFacetSelections =
689
+ ResourceEntityFilter.parseSelections(
690
+ body["resourceFilters"] as JSONObject | undefined,
691
+ );
692
+
693
+ if (Object.keys(selections).length === 0) {
694
+ return undefined;
695
+ }
696
+
697
+ const scopes: Array<ResourceEntityScope> =
698
+ await ResourceEntityFilter.resolveScopes({
699
+ projectId,
700
+ selections,
701
+ });
702
+
703
+ return scopes.length > 0 ? scopes : undefined;
704
+ }
705
+
659
706
  /*
660
707
  * Shared body parsing for every trace aggregation endpoint (histogram,
661
708
  * facets, analytics). Defensive about shapes: arrays are validated and
@@ -844,7 +891,13 @@ router.post(
844
891
  (body["bucketSizeInMinutes"] as number) ||
845
892
  computeDefaultBucketSize(startTime, endTime);
846
893
 
847
- const traceFilters: TraceFilters = parseTraceFilterBody(body);
894
+ const traceFilters: TraceFilters = {
895
+ ...parseTraceFilterBody(body),
896
+ resourceScopes: await resolveResourceScopesFromBody(
897
+ body,
898
+ databaseProps.tenantId,
899
+ ),
900
+ };
848
901
 
849
902
  const request: TraceHistogramRequest = {
850
903
  projectId: databaseProps.tenantId,
@@ -904,7 +957,13 @@ router.post(
904
957
 
905
958
  const limit: number = (body["limit"] as number) || 500;
906
959
 
907
- const traceFilters: TraceFilters = parseTraceFilterBody(body);
960
+ const traceFilters: TraceFilters = {
961
+ ...parseTraceFilterBody(body),
962
+ resourceScopes: await resolveResourceScopesFromBody(
963
+ body,
964
+ databaseProps.tenantId,
965
+ ),
966
+ };
908
967
 
909
968
  /*
910
969
  * Per-facet partial-match filter applied at the Postgres source-of-truth
@@ -1195,7 +1254,13 @@ router.post(
1195
1254
  ? Math.min(Math.max(Math.trunc(rawLimit), 1), 1000)
1196
1255
  : undefined;
1197
1256
 
1198
- const traceFilters: TraceFilters = parseTraceFilterBody(body);
1257
+ const traceFilters: TraceFilters = {
1258
+ ...parseTraceFilterBody(body),
1259
+ resourceScopes: await resolveResourceScopesFromBody(
1260
+ body,
1261
+ databaseProps.tenantId,
1262
+ ),
1263
+ };
1199
1264
 
1200
1265
  const request: TraceAnalyticsRequest = {
1201
1266
  projectId: databaseProps.tenantId,
@@ -1800,6 +1865,9 @@ router.post(
1800
1865
  ? (body["limit"] as number)
1801
1866
  : undefined;
1802
1867
 
1868
+ const resourceScopes: Array<ResourceEntityScope> | undefined =
1869
+ await resolveResourceScopesFromBody(body, databaseProps.tenantId);
1870
+
1803
1871
  const request: AnalyticsRequest = {
1804
1872
  projectId: databaseProps.tenantId,
1805
1873
  startTime,
@@ -1810,6 +1878,7 @@ router.post(
1810
1878
  aggregation,
1811
1879
  aggregationField,
1812
1880
  serviceIds,
1881
+ resourceScopes,
1813
1882
  severityTexts,
1814
1883
  bodySearchText,
1815
1884
  traceIds,
@@ -0,0 +1,30 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ /*
4
+ * Status page owners can now keep their page out of Google/Bing while it stays
5
+ * reachable by link. Indexing stays on unless it is turned off, so the column
6
+ * defaults to true.
7
+ *
8
+ * ADD COLUMN ... NOT NULL DEFAULT true populates every existing row with true
9
+ * in the same statement, so no backfill is needed and every status page that
10
+ * exists today keeps being indexed exactly as it is now. That matters because
11
+ * the renderers treat "not false" as indexable: a NULL row would read as
12
+ * indexable anyway, but there is no reason to leave that to interpretation.
13
+ */
14
+ export class AddEnableSearchEngineIndexingToStatusPage1787500000000
15
+ implements MigrationInterface
16
+ {
17
+ public name = "AddEnableSearchEngineIndexingToStatusPage1787500000000";
18
+
19
+ public async up(queryRunner: QueryRunner): Promise<void> {
20
+ await queryRunner.query(
21
+ `ALTER TABLE "StatusPage" ADD "enableSearchEngineIndexing" boolean NOT NULL DEFAULT true`,
22
+ );
23
+ }
24
+
25
+ public async down(queryRunner: QueryRunner): Promise<void> {
26
+ await queryRunner.query(
27
+ `ALTER TABLE "StatusPage" DROP COLUMN "enableSearchEngineIndexing"`,
28
+ );
29
+ }
30
+ }
@@ -0,0 +1,47 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ /*
4
+ * Reachability becomes the OUTCOME of the last poll instead of the age of
5
+ * the last success.
6
+ *
7
+ * "lastSeenAt within 15 minutes = up" could not tell "the device did not
8
+ * answer" apart from "the probe has not asked recently", so any fleet whose
9
+ * real poll cadence exceeded 15 minutes — and any device configured with a
10
+ * polling interval of 15 minutes or more — read as permanently down while
11
+ * its own interface inventory, written by those same successful walks, read
12
+ * as up.
13
+ *
14
+ * lastPolledAt records every ATTEMPT and isReachable records its result;
15
+ * lastSeenAt keeps its existing meaning (last successful walk).
16
+ *
17
+ * Backfill: an existing row with a lastSeenAt did answer the last walk we
18
+ * have any record of — there is no record of a failure — so it is seeded
19
+ * reachable, with that timestamp as its last attempt. Rows that were never
20
+ * seen stay NULL and read as Pending, exactly as they did before.
21
+ */
22
+ export class AddNetworkDeviceReachabilityColumns1787600000000
23
+ implements MigrationInterface
24
+ {
25
+ public name = "AddNetworkDeviceReachabilityColumns1787600000000";
26
+
27
+ public async up(queryRunner: QueryRunner): Promise<void> {
28
+ await queryRunner.query(
29
+ `ALTER TABLE "NetworkDevice" ADD "lastPolledAt" TIMESTAMP WITH TIME ZONE`,
30
+ );
31
+ await queryRunner.query(
32
+ `ALTER TABLE "NetworkDevice" ADD "isReachable" boolean`,
33
+ );
34
+ await queryRunner.query(
35
+ `UPDATE "NetworkDevice" SET "lastPolledAt" = "lastSeenAt", "isReachable" = true WHERE "lastSeenAt" IS NOT NULL`,
36
+ );
37
+ }
38
+
39
+ public async down(queryRunner: QueryRunner): Promise<void> {
40
+ await queryRunner.query(
41
+ `ALTER TABLE "NetworkDevice" DROP COLUMN "isReachable"`,
42
+ );
43
+ await queryRunner.query(
44
+ `ALTER TABLE "NetworkDevice" DROP COLUMN "lastPolledAt"`,
45
+ );
46
+ }
47
+ }
@@ -528,6 +528,8 @@ import { AddAIConversationPageContext1787100000000 } from "./1787100000000-AddAI
528
528
  import { AddAIChatMessageFeedback1787200000000 } from "./1787200000000-AddAIChatMessageFeedback";
529
529
  import { AddEpisodeMemberNotifyIndexes1787300000000 } from "./1787300000000-AddEpisodeMemberNotifyIndexes";
530
530
  import { AddDeviceRoleAndDeclaredLinkParent1787400000000 } from "./1787400000000-AddDeviceRoleAndDeclaredLinkParent";
531
+ import { AddEnableSearchEngineIndexingToStatusPage1787500000000 } from "./1787500000000-AddEnableSearchEngineIndexingToStatusPage";
532
+ import { AddNetworkDeviceReachabilityColumns1787600000000 } from "./1787600000000-AddNetworkDeviceReachabilityColumns";
531
533
 
532
534
  export default [
533
535
  InitialMigration,
@@ -1060,4 +1062,6 @@ export default [
1060
1062
  AddAIChatMessageFeedback1787200000000,
1061
1063
  AddEpisodeMemberNotifyIndexes1787300000000,
1062
1064
  AddDeviceRoleAndDeclaredLinkParent1787400000000,
1065
+ AddEnableSearchEngineIndexingToStatusPage1787500000000,
1066
+ AddNetworkDeviceReachabilityColumns1787600000000,
1063
1067
  ];
@@ -449,8 +449,17 @@ export default class UserMiddleware {
449
449
  globalValue,
450
450
  null,
451
451
  );
452
- res.set("global-permissions", globalValue);
453
- res.set("global-permissions-hash", globalPermissionsHash);
452
+
453
+ if (
454
+ !(
455
+ req.headers &&
456
+ req.headers["global-permissions-hash"] &&
457
+ req.headers["global-permissions-hash"] === globalPermissionsHash
458
+ )
459
+ ) {
460
+ res.set("global-permissions", globalValue);
461
+ res.set("global-permissions-hash", globalPermissionsHash);
462
+ }
454
463
  }
455
464
 
456
465
  // set project permissions hash.
@@ -1,20 +1,120 @@
1
1
  import CreateBy from "../Types/Database/CreateBy";
2
- import { OnCreate, OnUpdate } from "../Types/Database/Hooks";
2
+ import DeleteBy from "../Types/Database/DeleteBy";
3
+ import { OnCreate, OnDelete, OnUpdate } from "../Types/Database/Hooks";
3
4
  import UpdateBy from "../Types/Database/UpdateBy";
4
5
  import DatabaseService from "./DatabaseService";
5
- import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
6
+ import LIMIT_MAX, { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
6
7
  import BadDataException from "../../Types/Exception/BadDataException";
7
8
  import Model from "../../Models/DatabaseModels/ApiKeyPermission";
9
+ import Label from "../../Models/DatabaseModels/Label";
10
+ import ObjectID from "../../Types/ObjectID";
11
+ import Permission from "../../Types/Permission";
8
12
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
13
+ import InMemoryTTLCache from "../Infrastructure/InMemoryTTLCache";
14
+
15
+ /*
16
+ * 60s is the worst-case staleness on any single API node after a key's
17
+ * permissions are edited. We invalidate in-process immediately on
18
+ * create/update/delete; this TTL is the upper bound for *other* processes
19
+ * (same bound the ApiKeyService key cache accepted).
20
+ */
21
+ const PERMISSIONS_TTL_MS: number = 60 * 1000;
22
+
23
+ interface CachedApiKeyPermissionRow {
24
+ permission: Permission;
25
+ labelIds: Array<string>;
26
+ isBlockPermission: boolean | undefined;
27
+ }
28
+
29
+ export interface ApiKeyPermissionRow {
30
+ permission: Permission;
31
+ labelIds: Array<ObjectID>;
32
+ isBlockPermission: boolean | undefined;
33
+ }
34
+
9
35
  export class Service extends DatabaseService<Model> {
36
+ /*
37
+ * Cache of `apiKeyId -> permission rows`. The API-key auth path builds the
38
+ * request's tenant permissions from these rows on every request; without it
39
+ * that's a Postgres findBy (with a labels join) per request.
40
+ */
41
+ private permissionCache: InMemoryTTLCache<Array<CachedApiKeyPermissionRow>> =
42
+ new InMemoryTTLCache(10_000);
43
+
10
44
  public constructor() {
11
45
  super(Model);
12
46
  }
13
47
 
48
+ public clearCache(): void {
49
+ this.permissionCache.clear();
50
+ }
51
+
52
+ /**
53
+ * Resolves the permission rows granted to an API key, with a short-lived
54
+ * in-process cache. Returns freshly built objects on every call (hit or
55
+ * miss), so callers never share mutable state with the cache. Use this from
56
+ * the auth hot path instead of calling `findBy` directly.
57
+ */
58
+ @CaptureSpan()
59
+ public async findPermissionsByApiKeyId(
60
+ apiKeyId: ObjectID,
61
+ ): Promise<Array<ApiKeyPermissionRow>> {
62
+ const cacheKey: string = apiKeyId.toString();
63
+
64
+ let cachedRows: Array<CachedApiKeyPermissionRow> | undefined =
65
+ this.permissionCache.get(cacheKey);
66
+
67
+ if (cachedRows === undefined) {
68
+ const rows: Array<Model> = await this.findBy({
69
+ query: {
70
+ apiKeyId: apiKeyId,
71
+ },
72
+ select: {
73
+ permission: true,
74
+ labels: {
75
+ _id: true,
76
+ },
77
+ isBlockPermission: true,
78
+ },
79
+ limit: LIMIT_MAX,
80
+ skip: 0,
81
+ props: {
82
+ isRoot: true,
83
+ },
84
+ });
85
+
86
+ cachedRows = rows.map((row: Model): CachedApiKeyPermissionRow => {
87
+ return {
88
+ permission: row.permission!,
89
+ labelIds: (row.labels ?? []).map((label: Label) => {
90
+ return label.id!.toString();
91
+ }),
92
+ isBlockPermission: row.isBlockPermission,
93
+ };
94
+ });
95
+
96
+ this.permissionCache.set(cacheKey, cachedRows, PERMISSIONS_TTL_MS);
97
+ }
98
+
99
+ return cachedRows.map(
100
+ (row: CachedApiKeyPermissionRow): ApiKeyPermissionRow => {
101
+ return {
102
+ permission: row.permission,
103
+ labelIds: row.labelIds.map((id: string) => {
104
+ return new ObjectID(id);
105
+ }),
106
+ isBlockPermission: row.isBlockPermission,
107
+ };
108
+ },
109
+ );
110
+ }
111
+
14
112
  @CaptureSpan()
15
113
  protected override async onBeforeCreate(
16
114
  createBy: CreateBy<Model>,
17
115
  ): Promise<OnCreate<Model>> {
116
+ this.clearCache();
117
+
18
118
  if (!createBy.data.apiKeyId) {
19
119
  throw new BadDataException("API Key ID is required to create permission");
20
120
  }
@@ -92,6 +192,12 @@ export class Service extends DatabaseService<Model> {
92
192
  protected override async onBeforeUpdate(
93
193
  updateBy: UpdateBy<Model>,
94
194
  ): Promise<OnUpdate<Model>> {
195
+ /*
196
+ * We don't know which keys are being updated without a query; updates
197
+ * are rare so clearing is cheap.
198
+ */
199
+ this.clearCache();
200
+
95
201
  if (updateBy.data.labels && updateBy.data.labels.length > 0) {
96
202
  const existingPermissions: Array<Model> = await this.findBy({
97
203
  query: updateBy.query,
@@ -153,6 +259,14 @@ export class Service extends DatabaseService<Model> {
153
259
 
154
260
  return { updateBy, carryForward: null };
155
261
  }
262
+
263
+ @CaptureSpan()
264
+ protected override async onBeforeDelete(
265
+ deleteBy: DeleteBy<Model>,
266
+ ): Promise<OnDelete<Model>> {
267
+ this.clearCache();
268
+ return { deleteBy, carryForward: null };
269
+ }
156
270
  }
157
271
 
158
272
  export default new Service();
@@ -34,6 +34,7 @@ import Encryption from "../Utils/Encryption";
34
34
  import PasswordHash from "../Utils/PasswordHash";
35
35
  import PostgresErrorTranslator from "../Utils/Database/PostgresErrorTranslator";
36
36
  import logger, { LogAttributes } from "../Utils/Logger";
37
+ import ConfigLogLevel from "../Types/ConfigLogLevel";
37
38
  import BaseService from "./BaseService";
38
39
  import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
39
40
  import { WorkflowRoute } from "../../ServiceRoute";
@@ -2268,6 +2269,14 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
2268
2269
  */
2269
2270
  const affectedItems: Array<TBaseModel> = [];
2270
2271
 
2272
+ /*
2273
+ * The per-item debug payload below is a pretty-printed JSON.stringify
2274
+ * of every matched row; skip building it entirely unless the log level
2275
+ * is DEBUG, since logger.debug() no-ops at any other level.
2276
+ */
2277
+ const isDebugLogEnabled: boolean =
2278
+ logger.getLogLevel() === ConfigLogLevel.DEBUG;
2279
+
2271
2280
  for (const item of items) {
2272
2281
  /*
2273
2282
  * _id must be set AFTER the spread: update data can carry an
@@ -2282,12 +2291,14 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
2282
2291
  _id: item._id!,
2283
2292
  } as any;
2284
2293
 
2285
- logger.debug("Updated Item", {
2286
- projectId: updateBy.props.tenantId?.toString(),
2287
- } as LogAttributes);
2288
- logger.debug(JSON.stringify(updatedItem, null, 2), {
2289
- projectId: updateBy.props.tenantId?.toString(),
2290
- } as LogAttributes);
2294
+ if (isDebugLogEnabled) {
2295
+ logger.debug("Updated Item", {
2296
+ projectId: updateBy.props.tenantId?.toString(),
2297
+ } as LogAttributes);
2298
+ logger.debug(JSON.stringify(updatedItem, null, 2), {
2299
+ projectId: updateBy.props.tenantId?.toString(),
2300
+ } as LogAttributes);
2301
+ }
2291
2302
 
2292
2303
  if (hasRelationUpdates) {
2293
2304
  await this.getRepository().save(updatedItem);
@@ -10,6 +10,10 @@ import AnalyticsTableName from "../../Types/AnalyticsDatabase/AnalyticsTableName
10
10
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
11
11
  import { DbJSONResponse, Results } from "./AnalyticsDatabaseService";
12
12
  import ServiceType from "../../Types/Telemetry/ServiceType";
13
+ import {
14
+ appendResourceScopeFilters,
15
+ ResourceEntityScope,
16
+ } from "../Utils/Telemetry/ResourceEntityFilter";
13
17
 
14
18
  export interface HistogramBucket {
15
19
  time: string;
@@ -27,6 +31,15 @@ export interface HistogramRequest {
27
31
  bucketSizeInMinutes: number;
28
32
  serviceIds?: Array<ObjectID> | undefined;
29
33
  entityKeys?: Array<string> | undefined;
34
+ /*
35
+ * Resource-facet selections (Kubernetes cluster / host / docker host /
36
+ * podman host) already resolved to their entity keys. One scope per
37
+ * facet: the branches inside a scope OR (a row proves membership either
38
+ * by being primary-keyed on the resource or by carrying its entity key),
39
+ * and the scopes AND with each other so two facets intersect. See
40
+ * ResourceEntityFilter.
41
+ */
42
+ resourceScopes?: Array<ResourceEntityScope> | undefined;
30
43
  severityTexts?: Array<string> | undefined;
31
44
  bodySearchText?: string | undefined;
32
45
  traceIds?: Array<string> | undefined;
@@ -49,6 +62,15 @@ export interface FacetRequest {
49
62
  limit?: number | undefined;
50
63
  serviceIds?: Array<ObjectID> | undefined;
51
64
  entityKeys?: Array<string> | undefined;
65
+ /*
66
+ * Resource-facet selections (Kubernetes cluster / host / docker host /
67
+ * podman host) already resolved to their entity keys. One scope per
68
+ * facet: the branches inside a scope OR (a row proves membership either
69
+ * by being primary-keyed on the resource or by carrying its entity key),
70
+ * and the scopes AND with each other so two facets intersect. See
71
+ * ResourceEntityFilter.
72
+ */
73
+ resourceScopes?: Array<ResourceEntityScope> | undefined;
52
74
  severityTexts?: Array<string> | undefined;
53
75
  bodySearchText?: string | undefined;
54
76
  traceIds?: Array<string> | undefined;
@@ -70,6 +92,15 @@ export interface AnalyticsRequest {
70
92
  aggregation: AnalyticsAggregation;
71
93
  aggregationField?: string | undefined;
72
94
  serviceIds?: Array<ObjectID> | undefined;
95
+ /*
96
+ * Resource-facet selections (Kubernetes cluster / host / docker host /
97
+ * podman host) already resolved to their entity keys. One scope per
98
+ * facet: the branches inside a scope OR (a row proves membership either
99
+ * by being primary-keyed on the resource or by carrying its entity key),
100
+ * and the scopes AND with each other so two facets intersect. See
101
+ * ResourceEntityFilter.
102
+ */
103
+ resourceScopes?: Array<ResourceEntityScope> | undefined;
73
104
  severityTexts?: Array<string> | undefined;
74
105
  bodySearchText?: string | undefined;
75
106
  traceIds?: Array<string> | undefined;
@@ -764,6 +795,7 @@ export class LogAggregationService {
764
795
  HistogramRequest,
765
796
  | "serviceIds"
766
797
  | "entityKeys"
798
+ | "resourceScopes"
767
799
  | "severityTexts"
768
800
  | "bodySearchText"
769
801
  | "traceIds"
@@ -794,6 +826,8 @@ export class LogAggregationService {
794
826
  );
795
827
  }
796
828
 
829
+ appendResourceScopeFilters(statement, request.resourceScopes);
830
+
797
831
  if (request.severityTexts && request.severityTexts.length > 0) {
798
832
  statement.append(
799
833
  SQL` AND severityText IN (${{
@@ -1,11 +1,32 @@
1
1
  import ClickhouseDatabase from "../Infrastructure/ClickhouseDatabase";
2
2
  import AnalyticsDatabaseService from "./AnalyticsDatabaseService";
3
3
  import Log from "../../Models/AnalyticsModels/Log";
4
+ import FindBy from "../Types/AnalyticsDatabase/FindBy";
5
+ import { OnFind } from "../Types/AnalyticsDatabase/Hooks";
6
+ import ResourceEntityFilter from "../Utils/Telemetry/ResourceEntityFilter";
4
7
 
5
8
  export class LogService extends AnalyticsDatabaseService<Log> {
6
9
  public constructor(clickhouseDatabase?: ClickhouseDatabase | undefined) {
7
10
  super({ modelType: Log, database: clickhouseDatabase });
8
11
  }
12
+
13
+ /*
14
+ * Resolve the logs explorer's resource-facet selections (a Kubernetes
15
+ * cluster, a host, ...) before the query is compiled. They arrive as
16
+ * Postgres ids under `resourceFilters`; matching them needs the
17
+ * resource's entity key, which only Postgres can supply. See
18
+ * ResourceEntityFilter.
19
+ */
20
+ protected override async onBeforeFind(
21
+ findBy: FindBy<Log>,
22
+ ): Promise<OnFind<Log>> {
23
+ await ResourceEntityFilter.rewriteAnalyticsQuery({
24
+ query: findBy.query as unknown as Record<string, unknown>,
25
+ projectId: findBy.props?.tenantId,
26
+ });
27
+
28
+ return { findBy, carryForward: null };
29
+ }
9
30
  }
10
31
 
11
32
  export default new LogService();
@@ -849,7 +849,16 @@ export class Service extends DatabaseService<Model> {
849
849
  select: {
850
850
  _id: true,
851
851
  currentMonitorStatusId: true,
852
+ /*
853
+ * All four reachability inputs. isReachable is the one that decides
854
+ * up/down; the other three only size the "polling has stopped
855
+ * entirely" backstop. Selecting lastSeenAt alone silently drops the
856
+ * rollup back to the old freshness rule.
857
+ */
858
+ isReachable: true,
859
+ lastPolledAt: true,
852
860
  lastSeenAt: true,
861
+ pollingIntervalInMinutes: true,
853
862
  },
854
863
  limit: LIMIT_MAX,
855
864
  skip: 0,
@@ -908,7 +917,10 @@ export class Service extends DatabaseService<Model> {
908
917
  monitorStatusPriority: statusId
909
918
  ? priorityByStatusId.get(statusId)
910
919
  : undefined,
920
+ isReachable: device.isReachable,
921
+ lastPolledAt: device.lastPolledAt,
911
922
  lastSeenAt: device.lastSeenAt,
923
+ pollingIntervalInMinutes: device.pollingIntervalInMinutes,
912
924
  };
913
925
  },
914
926
  );
@@ -14,6 +14,9 @@ import SortOrder from "../../Types/BaseDatabase/SortOrder";
14
14
  import InBetween from "../../Types/BaseDatabase/InBetween";
15
15
  import Includes from "../../Types/BaseDatabase/Includes";
16
16
  import ObjectID from "../../Types/ObjectID";
17
+ import FindBy from "../Types/AnalyticsDatabase/FindBy";
18
+ import { OnFind } from "../Types/AnalyticsDatabase/Hooks";
19
+ import ResourceEntityFilter from "../Utils/Telemetry/ResourceEntityFilter";
17
20
 
18
21
  /**
19
22
  * Columns the proj_hist_by_minute projection can answer with. If a count
@@ -35,6 +38,24 @@ export class SpanService extends AnalyticsDatabaseService<Span> {
35
38
  super({ modelType: Span, database: clickhouseDatabase });
36
39
  }
37
40
 
41
+ /*
42
+ * Resolve the traces explorer's resource-facet selections (a Kubernetes
43
+ * cluster, a host, ...) before the query is compiled. They arrive as
44
+ * Postgres ids under `resourceFilters`; matching them needs the
45
+ * resource's entity key, which only Postgres can supply. See
46
+ * ResourceEntityFilter.
47
+ */
48
+ protected override async onBeforeFind(
49
+ findBy: FindBy<Span>,
50
+ ): Promise<OnFind<Span>> {
51
+ await ResourceEntityFilter.rewriteAnalyticsQuery({
52
+ query: findBy.query as unknown as Record<string, unknown>,
53
+ projectId: findBy.props?.tenantId,
54
+ });
55
+
56
+ return { findBy, carryForward: null };
57
+ }
58
+
38
59
  /**
39
60
  * Normalize a JSON-deserialized date value to a Date instance. When a query
40
61
  * crosses the API boundary, InBetween's startValue/endValue come back as ISO
@@ -11,6 +11,10 @@ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
11
11
  import { DbJSONResponse, Results } from "./AnalyticsDatabaseService";
12
12
  import logger from "../Utils/Logger";
13
13
  import ServiceType from "../../Types/Telemetry/ServiceType";
14
+ import {
15
+ appendResourceScopeFilters,
16
+ ResourceEntityScope,
17
+ } from "../Utils/Telemetry/ResourceEntityFilter";
14
18
  import ResourceFacetResolver, {
15
19
  ResolvedFacetValue,
16
20
  ResourceFacetSpec,
@@ -33,6 +37,15 @@ export type TraceAttributeFilters = Record<string, TraceAttributeFilterValue>;
33
37
  export interface TraceFilters {
34
38
  serviceIds?: Array<ObjectID> | undefined;
35
39
  entityKeys?: Array<string> | undefined;
40
+ /*
41
+ * Resource-facet selections (Kubernetes cluster / host / docker host /
42
+ * podman host) already resolved to their entity keys. One scope per
43
+ * facet: the branches inside a scope OR (a span proves membership either
44
+ * by being primary-keyed on the resource or by carrying its entity key),
45
+ * and the scopes AND with each other so two facets intersect. See
46
+ * ResourceEntityFilter.
47
+ */
48
+ resourceScopes?: Array<ResourceEntityScope> | undefined;
36
49
  statusCodes?: Array<number> | undefined;
37
50
  spanKinds?: Array<string> | undefined;
38
51
  spanNames?: Array<string> | undefined;
@@ -1008,6 +1021,8 @@ export class TraceAggregationService {
1008
1021
  );
1009
1022
  }
1010
1023
 
1024
+ appendResourceScopeFilters(statement, request.resourceScopes);
1025
+
1011
1026
  if (request.statusCodes && request.statusCodes.length > 0) {
1012
1027
  statement.append(
1013
1028
  SQL` AND statusCode IN (${{