@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
@@ -0,0 +1,33 @@
1
+ import { ExpressResponse } from "./Express";
2
+ import {
3
+ NOINDEX_ROBOTS_DIRECTIVE,
4
+ X_ROBOTS_TAG_HEADER_NAME,
5
+ } from "../../Types/StatusPage/SearchEngineIndexing";
6
+
7
+ /**
8
+ * Sets X-Robots-Tag: noindex, nofollow when the status page has search engine
9
+ * indexing turned off, and sets nothing at all when it is on.
10
+ *
11
+ * The "sets nothing" half is the point. An explicit `index, follow` header
12
+ * would be the default behaviour restated, and it would also override a
13
+ * noindex that an operator put in front of OneUptime (a CDN rule, an ingress
14
+ * annotation) - headers do not merge, the last writer wins. Staying silent
15
+ * leaves those in place.
16
+ *
17
+ * Callers must invoke this before the response is sent; a header set after
18
+ * headers are flushed is dropped by Node, silently.
19
+ */
20
+ export default function applyStatusPageRobotsHeader(
21
+ res: ExpressResponse,
22
+ isSearchEngineIndexingEnabled: boolean,
23
+ ): void {
24
+ if (isSearchEngineIndexingEnabled) {
25
+ return;
26
+ }
27
+
28
+ if (res.headersSent) {
29
+ return;
30
+ }
31
+
32
+ res.set(X_ROBOTS_TAG_HEADER_NAME, NOINDEX_ROBOTS_DIRECTIVE);
33
+ }
@@ -0,0 +1,441 @@
1
+ import ObjectID from "../../../Types/ObjectID";
2
+ import PositiveNumber from "../../../Types/PositiveNumber";
3
+ import { JSONObject } from "../../../Types/JSON";
4
+ import HostModel from "../../../Models/DatabaseModels/Host";
5
+ import DockerHostModel from "../../../Models/DatabaseModels/DockerHost";
6
+ import PodmanHostModel from "../../../Models/DatabaseModels/PodmanHost";
7
+ import KubernetesClusterModel from "../../../Models/DatabaseModels/KubernetesCluster";
8
+ import HostService from "../../Services/HostService";
9
+ import DockerHostService from "../../Services/DockerHostService";
10
+ import PodmanHostService from "../../Services/PodmanHostService";
11
+ import KubernetesClusterService from "../../Services/KubernetesClusterService";
12
+ import {
13
+ keyForHost,
14
+ keyForKubernetesCluster,
15
+ } from "../../../Utils/Telemetry/EntityKey";
16
+ import {
17
+ ResourceEntityFacetSelections,
18
+ isResourceEntityFacetKey,
19
+ parseResourceEntityFacetSelections,
20
+ } from "../../../Types/Telemetry/ResourceEntityFacet";
21
+ import Includes from "../../../Types/BaseDatabase/Includes";
22
+ import TableColumnType from "../../../Types/AnalyticsDatabase/TableColumnType";
23
+ import { SQL, Statement } from "../AnalyticsDatabase/Statement";
24
+ import logger from "../Logger";
25
+ import CaptureSpan from "./CaptureSpan";
26
+
27
+ /**
28
+ * One resource facet's selection, compiled into the three ways a signal row
29
+ * can prove membership of that resource. The three are OR-ed together; a
30
+ * row matching any of them belongs to the selected resource.
31
+ *
32
+ * - `entityIds` -> `primaryEntityId IN (...)`. Agent-ingested telemetry
33
+ * has the resource as its primary entity, so its id is
34
+ * literally in the column. Also the only thing that
35
+ * matches rows written before `entityKeys` existed.
36
+ * - `entityKeys` -> `hasAny(entityKeys, [...])`. The general membership
37
+ * read: OTLP telemetry primary-keyed on its Service
38
+ * still carries the host / cluster key here.
39
+ * - `attribute*` -> `attributes['resource.<attr>'] IN (...)`. The
40
+ * pre-`entityKeys` fallback, mirroring the entityScope
41
+ * contract the resource detail pages already use, so
42
+ * rows ingested before the column shipped (no backfill
43
+ * by decision) keep matching.
44
+ */
45
+ export interface ResourceEntityScope {
46
+ entityIds: Array<string>;
47
+ entityKeys: Array<string>;
48
+ attributeKey?: string | undefined;
49
+ attributeValues?: Array<string> | undefined;
50
+ }
51
+
52
+ interface ResourceFacetDefinition {
53
+ /** Signal attribute the identifying value is stamped under. */
54
+ attributeKey: string;
55
+ /** Read-side entity-key helper, mirroring the ingest-side resolver. */
56
+ entityKeyFor: (projectId: string, identifier: string) => string;
57
+ findIdentifiers: (data: {
58
+ projectId: ObjectID;
59
+ ids: Array<ObjectID>;
60
+ }) => Promise<Array<string>>;
61
+ }
62
+
63
+ /*
64
+ * `Host` / `DockerHost` / `PodmanHost` all key on the canonicalized
65
+ * `host.name` (see `HostService.findOrCreateByHostIdentifier` and its
66
+ * Docker / Podman siblings), which is exactly the Host entity's identity —
67
+ * so all three resolve through `keyForHost`. A Kubernetes cluster keys on
68
+ * `k8s.cluster.name` (`InventoryItem.k8sClusterIdentity`).
69
+ *
70
+ * The Docker / Podman facets need this as much as the Kubernetes one does:
71
+ * `getServiceNameFromAttributes` synthesises a per-container service name
72
+ * for agent-collected container telemetry, so those rows are primary-keyed
73
+ * on a Service too and an id-only predicate finds almost nothing.
74
+ *
75
+ * Resolving them to the Host entity means "Docker Host web-1" also matches
76
+ * non-container telemetry from web-1 when the Infrastructure agent runs
77
+ * alongside. That is the machine the facet names, so the wider match is the
78
+ * honest reading — and it is the only identity an OTLP resource carries;
79
+ * DockerHost / PodmanHost are inventory-mirrored types that no resource
80
+ * ever declares (see EntityType).
81
+ */
82
+ const FACET_DEFINITIONS: Record<string, ResourceFacetDefinition> = {
83
+ hostId: {
84
+ attributeKey: "resource.host.name",
85
+ entityKeyFor: keyForHost,
86
+ findIdentifiers: async (data: {
87
+ projectId: ObjectID;
88
+ ids: Array<ObjectID>;
89
+ }): Promise<Array<string>> => {
90
+ const rows: Array<HostModel> = await HostService.findBy({
91
+ query: { projectId: data.projectId, _id: new Includes(data.ids) },
92
+ select: { hostIdentifier: true },
93
+ limit: new PositiveNumber(data.ids.length),
94
+ skip: new PositiveNumber(0),
95
+ props: { isRoot: true },
96
+ });
97
+
98
+ return rows.map((row: HostModel): string => {
99
+ return row.hostIdentifier || "";
100
+ });
101
+ },
102
+ },
103
+ dockerHostId: {
104
+ attributeKey: "resource.host.name",
105
+ entityKeyFor: keyForHost,
106
+ findIdentifiers: async (data: {
107
+ projectId: ObjectID;
108
+ ids: Array<ObjectID>;
109
+ }): Promise<Array<string>> => {
110
+ const rows: Array<DockerHostModel> = await DockerHostService.findBy({
111
+ query: { projectId: data.projectId, _id: new Includes(data.ids) },
112
+ select: { hostIdentifier: true },
113
+ limit: new PositiveNumber(data.ids.length),
114
+ skip: new PositiveNumber(0),
115
+ props: { isRoot: true },
116
+ });
117
+
118
+ return rows.map((row: DockerHostModel): string => {
119
+ return row.hostIdentifier || "";
120
+ });
121
+ },
122
+ },
123
+ podmanHostId: {
124
+ attributeKey: "resource.host.name",
125
+ entityKeyFor: keyForHost,
126
+ findIdentifiers: async (data: {
127
+ projectId: ObjectID;
128
+ ids: Array<ObjectID>;
129
+ }): Promise<Array<string>> => {
130
+ const rows: Array<PodmanHostModel> = await PodmanHostService.findBy({
131
+ query: { projectId: data.projectId, _id: new Includes(data.ids) },
132
+ select: { hostIdentifier: true },
133
+ limit: new PositiveNumber(data.ids.length),
134
+ skip: new PositiveNumber(0),
135
+ props: { isRoot: true },
136
+ });
137
+
138
+ return rows.map((row: PodmanHostModel): string => {
139
+ return row.hostIdentifier || "";
140
+ });
141
+ },
142
+ },
143
+ kubernetesClusterId: {
144
+ attributeKey: "resource.k8s.cluster.name",
145
+ entityKeyFor: keyForKubernetesCluster,
146
+ findIdentifiers: async (data: {
147
+ projectId: ObjectID;
148
+ ids: Array<ObjectID>;
149
+ }): Promise<Array<string>> => {
150
+ const rows: Array<KubernetesClusterModel> =
151
+ await KubernetesClusterService.findBy({
152
+ query: { projectId: data.projectId, _id: new Includes(data.ids) },
153
+ select: { clusterIdentifier: true },
154
+ limit: new PositiveNumber(data.ids.length),
155
+ skip: new PositiveNumber(0),
156
+ props: { isRoot: true },
157
+ });
158
+
159
+ return rows.map((row: KubernetesClusterModel): string => {
160
+ return row.clusterIdentifier || "";
161
+ });
162
+ },
163
+ },
164
+ };
165
+
166
+ /**
167
+ * Append the resolved resource scopes to a hand-written aggregation
168
+ * statement (the Log / Trace histogram, facet and analytics queries, which
169
+ * build SQL directly rather than through StatementGenerator).
170
+ *
171
+ * Emits one `AND ( ... OR ... )` group per scope, mirroring exactly what
172
+ * StatementGenerator compiles for the list query — the chart and the list
173
+ * must agree on which rows a facet selects, or the histogram tells a
174
+ * different story than the rows underneath it.
175
+ *
176
+ * A scope with no usable branch appends nothing rather than an empty
177
+ * `IN ()`: an unresolvable filter must not silently match zero rows.
178
+ */
179
+ export function appendResourceScopeFilters(
180
+ statement: Statement,
181
+ scopes: Array<ResourceEntityScope> | undefined,
182
+ ): void {
183
+ if (!scopes || scopes.length === 0) {
184
+ return;
185
+ }
186
+
187
+ for (const scope of scopes) {
188
+ if (!scope) {
189
+ continue;
190
+ }
191
+
192
+ const branches: Array<Statement> = [];
193
+
194
+ const entityIds: Array<string> = (scope.entityIds || []).filter(
195
+ (id: string): boolean => {
196
+ return typeof id === "string" && id.length > 0;
197
+ },
198
+ );
199
+
200
+ if (entityIds.length > 0) {
201
+ branches.push(
202
+ SQL`primaryEntityId IN (${{
203
+ type: TableColumnType.ObjectID,
204
+ value: new Includes(entityIds),
205
+ }})`,
206
+ );
207
+ }
208
+
209
+ const entityKeys: Array<string> = (scope.entityKeys || []).filter(
210
+ (entityKey: string): boolean => {
211
+ return typeof entityKey === "string" && entityKey.length > 0;
212
+ },
213
+ );
214
+
215
+ if (entityKeys.length > 0) {
216
+ branches.push(
217
+ SQL`hasAny(entityKeys, ${{
218
+ type: TableColumnType.ArrayText,
219
+ value: entityKeys,
220
+ }})`,
221
+ );
222
+ }
223
+
224
+ const attributeValues: Array<string> = (scope.attributeValues || []).filter(
225
+ (attributeValue: string): boolean => {
226
+ return typeof attributeValue === "string" && attributeValue.length > 0;
227
+ },
228
+ );
229
+
230
+ if (scope.attributeKey && attributeValues.length > 0) {
231
+ branches.push(
232
+ SQL`attributes[${{
233
+ type: TableColumnType.Text,
234
+ value: scope.attributeKey,
235
+ }}] IN (${{
236
+ type: TableColumnType.Text,
237
+ value: new Includes(attributeValues),
238
+ }})`,
239
+ );
240
+ }
241
+
242
+ if (branches.length === 0) {
243
+ continue;
244
+ }
245
+
246
+ statement.append(SQL` AND (`);
247
+
248
+ for (const [index, branch] of branches.entries()) {
249
+ if (index > 0) {
250
+ statement.append(SQL` OR `);
251
+ }
252
+ statement.append(branch);
253
+ }
254
+
255
+ statement.append(SQL`)`);
256
+ }
257
+ }
258
+
259
+ export default class ResourceEntityFilter {
260
+ /**
261
+ * Read the `resourceFilters` field off a request body (or an analytics
262
+ * query record) into the canonical selection shape. Delegates to the
263
+ * isomorphic parser so the browser and the server agree on exactly which
264
+ * selections are well-formed.
265
+ */
266
+ public static parseSelections(
267
+ source: JSONObject | undefined | null,
268
+ ): ResourceEntityFacetSelections {
269
+ return parseResourceEntityFacetSelections(source);
270
+ }
271
+
272
+ /**
273
+ * Compile each facet's selected ids into a scope. One scope per facet, so
274
+ * the caller ANDs them: selecting a cluster and a host means "in that
275
+ * cluster AND on that host", which is what a reader expects of two
276
+ * independent filter groups.
277
+ *
278
+ * The identifier lookup is best-effort: if Postgres is unreachable or an
279
+ * id no longer resolves, the scope keeps its `entityIds` branch and
280
+ * simply loses the entity-key one. That degrades to today's behavior
281
+ * rather than dropping the constraint (which would silently widen the
282
+ * result set).
283
+ */
284
+ @CaptureSpan()
285
+ public static async resolveScopes(data: {
286
+ projectId: ObjectID;
287
+ selections: ResourceEntityFacetSelections;
288
+ }): Promise<Array<ResourceEntityScope>> {
289
+ const facetKeys: Array<string> = Object.keys(data.selections).filter(
290
+ (key: string): boolean => {
291
+ return (
292
+ isResourceEntityFacetKey(key) &&
293
+ (data.selections[key] || []).length > 0
294
+ );
295
+ },
296
+ );
297
+
298
+ if (facetKeys.length === 0) {
299
+ return [];
300
+ }
301
+
302
+ return Promise.all(
303
+ facetKeys.map(async (facetKey: string): Promise<ResourceEntityScope> => {
304
+ return ResourceEntityFilter.resolveOne({
305
+ projectId: data.projectId,
306
+ facetKey,
307
+ ids: data.selections[facetKey] || [],
308
+ });
309
+ }),
310
+ );
311
+ }
312
+
313
+ /**
314
+ * Query key the client puts the raw selected ids under, and the key the
315
+ * compiled scopes are written to. Kept together so the rewrite below is
316
+ * the only thing that has to know the pair.
317
+ */
318
+ public static readonly QUERY_FILTER_KEY: string = "resourceFilters";
319
+ public static readonly QUERY_SCOPE_KEY: string = "resourceEntityScopes";
320
+
321
+ /**
322
+ * Rewrite an analytics list query in place: consume the client-sent
323
+ * `resourceFilters` ids and replace them with the resolved
324
+ * `resourceEntityScopes` that StatementGenerator compiles.
325
+ *
326
+ * The compiled key is always rebuilt from scratch — a client cannot
327
+ * hand-craft one, so the only way a scope reaches the SQL is through this
328
+ * resolver. (It could only ever narrow rows, but leaving one authoritative
329
+ * writer keeps the predicate auditable.)
330
+ *
331
+ * Without a tenant the identifier lookup cannot be project-scoped, so the
332
+ * scope keeps its id branch and drops the entity-key one: narrower than
333
+ * the request asked for, never wider.
334
+ */
335
+ @CaptureSpan()
336
+ public static async rewriteAnalyticsQuery(data: {
337
+ query: Record<string, unknown> | undefined;
338
+ projectId?: ObjectID | undefined;
339
+ }): Promise<void> {
340
+ const query: Record<string, unknown> | undefined = data.query;
341
+
342
+ if (!query || typeof query !== "object") {
343
+ return;
344
+ }
345
+
346
+ const raw: unknown = query[ResourceEntityFilter.QUERY_FILTER_KEY];
347
+
348
+ delete query[ResourceEntityFilter.QUERY_FILTER_KEY];
349
+ delete query[ResourceEntityFilter.QUERY_SCOPE_KEY];
350
+
351
+ if (raw === undefined || raw === null) {
352
+ return;
353
+ }
354
+
355
+ const selections: ResourceEntityFacetSelections =
356
+ ResourceEntityFilter.parseSelections(raw as JSONObject);
357
+
358
+ if (Object.keys(selections).length === 0) {
359
+ return;
360
+ }
361
+
362
+ if (!data.projectId) {
363
+ query[ResourceEntityFilter.QUERY_SCOPE_KEY] = Object.keys(selections).map(
364
+ (facetKey: string): ResourceEntityScope => {
365
+ return { entityIds: selections[facetKey] || [], entityKeys: [] };
366
+ },
367
+ );
368
+ return;
369
+ }
370
+
371
+ const scopes: Array<ResourceEntityScope> =
372
+ await ResourceEntityFilter.resolveScopes({
373
+ projectId: data.projectId,
374
+ selections,
375
+ });
376
+
377
+ if (scopes.length > 0) {
378
+ query[ResourceEntityFilter.QUERY_SCOPE_KEY] = scopes;
379
+ }
380
+ }
381
+
382
+ private static async resolveOne(data: {
383
+ projectId: ObjectID;
384
+ facetKey: string;
385
+ ids: Array<string>;
386
+ }): Promise<ResourceEntityScope> {
387
+ const scope: ResourceEntityScope = {
388
+ entityIds: data.ids,
389
+ entityKeys: [],
390
+ };
391
+
392
+ const definition: ResourceFacetDefinition | undefined =
393
+ FACET_DEFINITIONS[data.facetKey];
394
+
395
+ if (!definition) {
396
+ return scope;
397
+ }
398
+
399
+ let identifiers: Array<string> = [];
400
+
401
+ try {
402
+ identifiers = await definition.findIdentifiers({
403
+ projectId: data.projectId,
404
+ ids: data.ids.map((id: string): ObjectID => {
405
+ return new ObjectID(id);
406
+ }),
407
+ });
408
+ } catch (err: unknown) {
409
+ logger.warn(
410
+ `Could not resolve ${data.facetKey} identifiers for the entity-key filter; falling back to primaryEntityId only: ${err}`,
411
+ );
412
+ return scope;
413
+ }
414
+
415
+ const uniqueIdentifiers: Array<string> = Array.from(
416
+ new Set(
417
+ identifiers.filter((identifier: string): boolean => {
418
+ return identifier.trim().length > 0;
419
+ }),
420
+ ),
421
+ );
422
+
423
+ if (uniqueIdentifiers.length === 0) {
424
+ return scope;
425
+ }
426
+
427
+ const projectIdString: string = data.projectId.toString();
428
+
429
+ scope.entityKeys = Array.from(
430
+ new Set(
431
+ uniqueIdentifiers.map((identifier: string): string => {
432
+ return definition.entityKeyFor(projectIdString, identifier);
433
+ }),
434
+ ),
435
+ );
436
+ scope.attributeKey = definition.attributeKey;
437
+ scope.attributeValues = uniqueIdentifiers;
438
+
439
+ return scope;
440
+ }
441
+ }