@oneuptime/common 11.5.9 → 11.5.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/AnalyticsModels/Index.ts +13 -0
- package/Models/AnalyticsModels/MetricItemAggMV1mByContainer.ts +178 -0
- package/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.ts +179 -0
- package/Models/AnalyticsModels/MetricItemAggMV1mByService.ts +179 -0
- package/Models/AnalyticsModels/NetworkFlow.ts +397 -0
- package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +18 -0
- package/Models/DatabaseModels/GlobalConfig.ts +36 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/MarketingConversion.ts +198 -0
- package/Models/DatabaseModels/MetricSavedView.ts +34 -0
- package/Models/DatabaseModels/MetricType.ts +74 -0
- package/Models/DatabaseModels/NetworkDevice.ts +332 -0
- package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +113 -0
- package/Models/DatabaseModels/NetworkInterface.ts +58 -0
- package/Models/DatabaseModels/Project.ts +37 -0
- package/Models/DatabaseModels/User.ts +37 -0
- package/Server/API/BaseAnalyticsAPI.ts +23 -0
- package/Server/API/EnterpriseLicenseAPI.ts +14 -0
- package/Server/EnvironmentConfig.ts +84 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784191414522-AddCounterSemanticsToMetricType.ts +38 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784191414600-AddViewTypeToMetricSavedView.ts +28 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784211212164-AddNetworkDeviceInventoryAndDiscoverySchedule.ts +108 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784218257664-AddEnterpriseLicenseNotificationColumns.ts +31 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784293516000-AddAttributionColumnsToUserAndProject.ts +29 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784298000000-AddMarketingConversionTable.ts +35 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784401962564-MigrationName.ts +101 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +14 -0
- package/Server/Services/AnalyticsDatabaseService.ts +70 -1
- package/Server/Services/IncidentService.ts +13 -0
- package/Server/Services/Index.ts +10 -0
- package/Server/Services/MarketingConversionService.ts +10 -0
- package/Server/Services/MetricItemAggMV1mByContainerService.ts +31 -0
- package/Server/Services/MetricItemAggMV1mByK8sClusterService.ts +31 -0
- package/Server/Services/MetricItemAggMV1mByServiceService.ts +31 -0
- package/Server/Services/MetricService.ts +970 -122
- package/Server/Services/MonitorService.ts +14 -0
- package/Server/Services/NetworkDeviceOwnerTeamService.ts +42 -0
- package/Server/Services/NetworkDeviceOwnerUserService.ts +106 -0
- package/Server/Services/NetworkFlowService.ts +11 -0
- package/Server/Services/OnCallDutyPolicyService.ts +11 -1
- package/Server/Services/ProjectService.ts +136 -0
- package/Server/Services/SpanService.ts +13 -9
- package/Server/Services/StatusPageService.ts +10 -0
- package/Server/Services/TeamMemberService.ts +11 -0
- package/Server/Services/UserService.ts +59 -0
- package/Server/Types/AnalyticsDatabase/AggregateBy.ts +37 -7
- package/Server/Utils/Attribution.ts +99 -0
- package/Server/Utils/Marketing/ConversionUploadProvider.ts +103 -0
- package/Server/Utils/Marketing/ConversionUploadProviders.ts +22 -0
- package/Server/Utils/Marketing/Providers/GoogleAds.ts +270 -0
- package/Server/Utils/Marketing/Providers/LinkedIn.ts +147 -0
- package/Server/Utils/Marketing/Providers/Meta.ts +148 -0
- package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +196 -0
- package/Server/Utils/Marketing/Providers/Reddit.ts +178 -0
- package/Server/Utils/Monitor/MonitorAlert.ts +71 -7
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +31 -19
- package/Server/Utils/Monitor/MonitorIncident.ts +71 -7
- package/Server/Utils/Monitor/MonitorMetricUtil.ts +57 -0
- package/Server/Utils/Monitor/MonitorTemplateUtil.ts +94 -0
- package/Server/Utils/Monitor/NetworkDeviceHydrationUtil.ts +15 -0
- package/Server/Utils/Monitor/NetworkInventoryUtil.ts +125 -14
- package/Server/Utils/ProductAnalytics.ts +133 -0
- package/Server/Utils/Telemetry/Telemetry.ts +39 -0
- package/Server/Utils/Telemetry/TelemetryEntity.ts +21 -0
- package/Tests/Server/API/BaseAnalyticsAPI.test.ts +72 -0
- package/Tests/Server/Infrastructure/SemaphorePermit.test.ts +9 -1
- package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +21 -2
- package/Tests/Server/Services/MetricEntityMVKeyParity.test.ts +173 -0
- package/Tests/Server/Services/MetricServiceAggregate.test.ts +1391 -0
- package/Tests/Server/Services/NetworkDeviceOwnerServices.test.ts +260 -0
- package/Tests/Server/Utils/Monitor/MonitorTemplateUtilNetworkDevice.test.ts +389 -0
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +764 -0
- package/Tests/Types/BaseDatabase/AggregationIntervalUtil.test.ts +111 -2
- package/Tests/Types/Database/EndsWith.test.ts +64 -0
- package/Tests/Types/Database/GreaterThan.test.ts +62 -0
- package/Tests/Types/Database/GreaterThanOrEqual.test.ts +65 -0
- package/Tests/Types/Database/GreaterThanOrNull.test.ts +63 -0
- package/Tests/Types/Database/Includes.test.ts +65 -0
- package/Tests/Types/Database/IncludesAll.test.ts +65 -0
- package/Tests/Types/Database/IncludesNone.test.ts +65 -0
- package/Tests/Types/Database/IsNull.test.ts +44 -0
- package/Tests/Types/Database/LessThan.test.ts +62 -0
- package/Tests/Types/Database/LessThanOrEqual.test.ts +62 -0
- package/Tests/Types/Database/LessThanOrNull.test.ts +62 -0
- package/Tests/Types/Database/MultiSearch.test.ts +81 -0
- package/Tests/Types/Database/NotContains.test.ts +64 -0
- package/Tests/Types/Database/NotNull.test.ts +44 -0
- package/Tests/Types/Database/StartsWith.test.ts +64 -0
- package/Tests/Types/Monitor/MonitorStepSqlMonitor.test.ts +83 -1
- package/Tests/Types/Monitor/SnmpV3EnumParsing.test.ts +247 -0
- package/Tests/Types/Monitor/SnmpVendorTemplate.test.ts +263 -0
- package/Tests/UI/Components/XAxis.test.ts +5 -3
- package/Tests/Utils/EnterpriseLicenseUsage.test.ts +76 -0
- package/Tests/Utils/Metrics/MetricExplorerUrl.test.ts +622 -0
- package/Tests/Utils/Metrics/MetricFormulaEvaluator.test.ts +474 -0
- package/Tests/Utils/Telemetry/HeartbeatAvailability.test.ts +52 -4
- package/Types/AnalyticsDatabase/AnalyticsTableName.ts +11 -0
- package/Types/BaseDatabase/AggregateBy.ts +31 -2
- package/Types/BaseDatabase/AggregatedResult.ts +22 -0
- package/Types/BaseDatabase/AggregationInterval.ts +11 -0
- package/Types/BaseDatabase/AggregationIntervalUtil.ts +30 -5
- package/Types/Dashboard/DashboardComponents/DashboardTraceChartComponent.ts +16 -0
- package/Types/Email/EmailTemplateType.ts +3 -0
- package/Types/Marketing/MarketingConversion.ts +11 -0
- package/Types/Metrics/MetricQueryConfigData.ts +21 -0
- package/Types/Metrics/MetricQueryData.ts +8 -0
- package/Types/Metrics/MetricViewData.ts +8 -0
- package/Types/Monitor/MonitorMetricType.ts +8 -0
- package/Types/Monitor/MonitorStep.ts +9 -0
- package/Types/Monitor/MonitorStepSqlMonitor.ts +12 -0
- package/Types/Monitor/MonitorType.ts +2 -1
- package/Types/Monitor/SSLMonitor/SslMonitorResponse.ts +17 -12
- package/Types/Monitor/SnmpMonitor/CdpNeighbor.ts +13 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +37 -3
- package/Types/Monitor/SnmpMonitor/SnmpAuthProtocol.ts +46 -0
- package/Types/Monitor/SnmpMonitor/SnmpEntityInfo.ts +15 -0
- package/Types/Monitor/SnmpMonitor/SnmpInterface.ts +8 -0
- package/Types/Monitor/SnmpMonitor/SnmpMonitorResponse.ts +17 -8
- package/Types/Monitor/SnmpMonitor/SnmpPrivProtocol.ts +53 -0
- package/Types/Monitor/SnmpMonitor/SnmpSecurityLevel.ts +43 -0
- package/Types/Monitor/SnmpMonitor/SnmpSystemInfo.ts +16 -0
- package/Types/Monitor/SnmpMonitor/SnmpVendorTemplate.ts +119 -3
- package/Types/NetFlow/NetworkFlowRecord.ts +25 -0
- package/Types/Syslog/SyslogMessage.ts +18 -0
- package/Types/Telemetry/TelemetrySavedViewState.ts +9 -0
- package/Types/Telemetry/TelemetrySavedViewType.ts +12 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +51 -0
- package/UI/Components/Charts/Bar/BarChart.tsx +51 -1
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +97 -26
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +328 -1
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +115 -0
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +328 -1
- package/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts +7 -0
- package/UI/Components/Charts/ChartLibrary/Types/FormattedReferenceRegion.ts +15 -0
- package/UI/Components/Charts/ChartLibrary/Types/FormattedTimeReferenceLine.ts +13 -0
- package/UI/Components/Charts/Line/LineChart.tsx +51 -0
- package/UI/Components/Charts/Types/ReferenceRegionProps.ts +12 -0
- package/UI/Components/Charts/Types/TimeReferenceLineProps.ts +12 -0
- package/UI/Components/Charts/Types/XAxis/XAxisPrecision.ts +1 -0
- package/UI/Components/Charts/Utils/DataPoint.ts +0 -0
- package/UI/Components/Charts/Utils/TimeAnnotation.ts +169 -0
- package/UI/Components/Charts/Utils/XAxis.ts +28 -4
- package/UI/Components/Icon/Icon.tsx +33 -13
- package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +9 -0
- package/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.ts +67 -0
- package/UI/Components/Page/ModelPage.tsx +6 -1
- package/UI/Components/TelemetryViewer/components/SavedViewsDropdown.tsx +33 -4
- package/UI/Styles/Theme.css +10 -0
- package/UI/Utils/User.ts +41 -2
- package/Utils/Analytics.ts +8 -6
- package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +74 -0
- package/Utils/Metrics/MetricExplorerUrl.ts +771 -0
- package/Utils/Metrics/MetricFormulaEvaluator.ts +410 -46
- package/Utils/Monitor/NetworkTopologyUtil.ts +261 -49
- package/Utils/ValueFormatter.ts +22 -1
- package/build/dist/Models/AnalyticsModels/Index.js +13 -0
- package/build/dist/Models/AnalyticsModels/Index.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js +159 -0
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js.map +1 -0
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js +158 -0
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js.map +1 -0
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js +160 -0
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js.map +1 -0
- package/build/dist/Models/AnalyticsModels/NetworkFlow.js +351 -0
- package/build/dist/Models/AnalyticsModels/NetworkFlow.js.map +1 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +19 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +38 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MarketingConversion.js +219 -0
- package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +1 -0
- package/build/dist/Models/DatabaseModels/MetricSavedView.js +35 -0
- package/build/dist/Models/DatabaseModels/MetricSavedView.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MetricType.js +76 -0
- package/build/dist/Models/DatabaseModels/MetricType.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +341 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +116 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkInterface.js +60 -0
- package/build/dist/Models/DatabaseModels/NetworkInterface.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +38 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/User.js +38 -0
- package/build/dist/Models/DatabaseModels/User.js.map +1 -1
- package/build/dist/Server/API/BaseAnalyticsAPI.js +19 -0
- package/build/dist/Server/API/BaseAnalyticsAPI.js.map +1 -1
- package/build/dist/Server/API/EnterpriseLicenseAPI.js +8 -0
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +51 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784191414522-AddCounterSemanticsToMetricType.js +27 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784191414522-AddCounterSemanticsToMetricType.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784191414600-AddViewTypeToMetricSavedView.js +21 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784191414600-AddViewTypeToMetricSavedView.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784211212164-AddNetworkDeviceInventoryAndDiscoverySchedule.js +47 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784211212164-AddNetworkDeviceInventoryAndDiscoverySchedule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784218257664-AddEnterpriseLicenseNotificationColumns.js +16 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784218257664-AddEnterpriseLicenseNotificationColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784293516000-AddAttributionColumnsToUserAndProject.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784293516000-AddAttributionColumnsToUserAndProject.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784298000000-AddMarketingConversionTable.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784298000000-AddMarketingConversionTable.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784401962564-MigrationName.js +40 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784401962564-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +58 -4
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +12 -0
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +10 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/MarketingConversionService.js +9 -0
- package/build/dist/Server/Services/MarketingConversionService.js.map +1 -0
- package/build/dist/Server/Services/MetricItemAggMV1mByContainerService.js +29 -0
- package/build/dist/Server/Services/MetricItemAggMV1mByContainerService.js.map +1 -0
- package/build/dist/Server/Services/MetricItemAggMV1mByK8sClusterService.js +29 -0
- package/build/dist/Server/Services/MetricItemAggMV1mByK8sClusterService.js.map +1 -0
- package/build/dist/Server/Services/MetricItemAggMV1mByServiceService.js +29 -0
- package/build/dist/Server/Services/MetricItemAggMV1mByServiceService.js.map +1 -0
- package/build/dist/Server/Services/MetricService.js +761 -125
- package/build/dist/Server/Services/MetricService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +18 -5
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceOwnerTeamService.js +44 -0
- package/build/dist/Server/Services/NetworkDeviceOwnerTeamService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceOwnerUserService.js +88 -0
- package/build/dist/Server/Services/NetworkDeviceOwnerUserService.js.map +1 -1
- package/build/dist/Server/Services/NetworkFlowService.js +9 -0
- package/build/dist/Server/Services/NetworkFlowService.js.map +1 -0
- package/build/dist/Server/Services/OnCallDutyPolicyService.js +15 -6
- package/build/dist/Server/Services/OnCallDutyPolicyService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +107 -6
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/SpanService.js +8 -6
- package/build/dist/Server/Services/SpanService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +10 -0
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +11 -0
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +52 -0
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Types/AnalyticsDatabase/AggregateBy.js +33 -7
- package/build/dist/Server/Types/AnalyticsDatabase/AggregateBy.js.map +1 -1
- package/build/dist/Server/Utils/Attribution.js +74 -0
- package/build/dist/Server/Utils/Attribution.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +40 -0
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js +20 -0
- package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +177 -0
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +113 -0
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js +109 -0
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +130 -0
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +126 -0
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +53 -9
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +21 -10
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +54 -11
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js +43 -3
- package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +68 -0
- package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkDeviceHydrationUtil.js +15 -0
- package/build/dist/Server/Utils/Monitor/NetworkDeviceHydrationUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +100 -15
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
- package/build/dist/Server/Utils/ProductAnalytics.js +111 -0
- package/build/dist/Server/Utils/ProductAnalytics.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/Telemetry.js +32 -5
- package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +17 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +11 -0
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
- package/build/dist/Types/BaseDatabase/AggregationInterval.js +11 -0
- package/build/dist/Types/BaseDatabase/AggregationInterval.js.map +1 -1
- package/build/dist/Types/BaseDatabase/AggregationIntervalUtil.js +33 -5
- package/build/dist/Types/BaseDatabase/AggregationIntervalUtil.js.map +1 -1
- package/build/dist/Types/Email/EmailTemplateType.js +2 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/Marketing/MarketingConversion.js +13 -0
- package/build/dist/Types/Marketing/MarketingConversion.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorMetricType.js +7 -0
- package/build/dist/Types/Monitor/MonitorMetricType.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStep.js +6 -0
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStepSqlMonitor.js +3 -0
- package/build/dist/Types/Monitor/MonitorStepSqlMonitor.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorType.js +2 -1
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/Types/Monitor/SnmpMonitor/CdpNeighbor.js +2 -0
- package/build/dist/Types/Monitor/SnmpMonitor/CdpNeighbor.js.map +1 -0
- package/build/dist/Types/Monitor/SnmpMonitor/SnmpAuthProtocol.js +41 -0
- package/build/dist/Types/Monitor/SnmpMonitor/SnmpAuthProtocol.js.map +1 -1
- package/build/dist/Types/Monitor/SnmpMonitor/SnmpEntityInfo.js +2 -0
- package/build/dist/Types/Monitor/SnmpMonitor/SnmpEntityInfo.js.map +1 -0
- package/build/dist/Types/Monitor/SnmpMonitor/SnmpPrivProtocol.js +48 -0
- package/build/dist/Types/Monitor/SnmpMonitor/SnmpPrivProtocol.js.map +1 -1
- package/build/dist/Types/Monitor/SnmpMonitor/SnmpSecurityLevel.js +38 -0
- package/build/dist/Types/Monitor/SnmpMonitor/SnmpSecurityLevel.js.map +1 -1
- package/build/dist/Types/Monitor/SnmpMonitor/SnmpSystemInfo.js +2 -0
- package/build/dist/Types/Monitor/SnmpMonitor/SnmpSystemInfo.js.map +1 -0
- package/build/dist/Types/Monitor/SnmpMonitor/SnmpVendorTemplate.js +96 -3
- package/build/dist/Types/Monitor/SnmpMonitor/SnmpVendorTemplate.js.map +1 -1
- package/build/dist/Types/NetFlow/NetworkFlowRecord.js +2 -0
- package/build/dist/Types/NetFlow/NetworkFlowRecord.js.map +1 -0
- package/build/dist/Types/Syslog/SyslogMessage.js +2 -0
- package/build/dist/Types/Syslog/SyslogMessage.js.map +1 -0
- package/build/dist/Types/Telemetry/TelemetrySavedViewState.js +0 -7
- package/build/dist/Types/Telemetry/TelemetrySavedViewState.js.map +1 -1
- package/build/dist/Types/Telemetry/TelemetrySavedViewType.js +13 -0
- package/build/dist/Types/Telemetry/TelemetrySavedViewType.js.map +1 -0
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +25 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +26 -2
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +36 -12
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +208 -6
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +52 -3
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +208 -6
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js +6 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/FormattedReferenceRegion.js +2 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/FormattedReferenceRegion.js.map +1 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/FormattedTimeReferenceLine.js +2 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/FormattedTimeReferenceLine.js.map +1 -0
- package/build/dist/UI/Components/Charts/Line/LineChart.js +25 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Types/ReferenceRegionProps.js +2 -0
- package/build/dist/UI/Components/Charts/Types/ReferenceRegionProps.js.map +1 -0
- package/build/dist/UI/Components/Charts/Types/TimeReferenceLineProps.js +2 -0
- package/build/dist/UI/Components/Charts/Types/TimeReferenceLineProps.js.map +1 -0
- package/build/dist/UI/Components/Charts/Types/XAxis/XAxisPrecision.js +1 -0
- package/build/dist/UI/Components/Charts/Types/XAxis/XAxisPrecision.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js +0 -0
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/TimeAnnotation.js +121 -0
- package/build/dist/UI/Components/Charts/Utils/TimeAnnotation.js.map +1 -0
- package/build/dist/UI/Components/Charts/Utils/XAxis.js +27 -4
- package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
- package/build/dist/UI/Components/Icon/Icon.js +15 -8
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js +58 -0
- package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js.map +1 -1
- package/build/dist/UI/Components/Page/ModelPage.js +7 -1
- package/build/dist/UI/Components/Page/ModelPage.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js +10 -4
- package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js.map +1 -1
- package/build/dist/UI/Utils/User.js +35 -2
- package/build/dist/UI/Utils/User.js.map +1 -1
- package/build/dist/Utils/Analytics.js +8 -5
- package/build/dist/Utils/Analytics.js.map +1 -1
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js +57 -0
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js.map +1 -1
- package/build/dist/Utils/Metrics/MetricExplorerUrl.js +418 -0
- package/build/dist/Utils/Metrics/MetricExplorerUrl.js.map +1 -0
- package/build/dist/Utils/Metrics/MetricFormulaEvaluator.js +296 -37
- package/build/dist/Utils/Metrics/MetricFormulaEvaluator.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +176 -40
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/ValueFormatter.js +20 -1
- package/build/dist/Utils/ValueFormatter.js.map +1 -1
- package/package.json +4 -3
|
@@ -18,10 +18,23 @@ import AggregationInterval from "../../Types/BaseDatabase/AggregationInterval";
|
|
|
18
18
|
import AggregatedResult from "../../Types/BaseDatabase/AggregatedResult";
|
|
19
19
|
import AnalyticsTableName from "../../Types/AnalyticsDatabase/AnalyticsTableName";
|
|
20
20
|
import TableColumnType from "../../Types/AnalyticsDatabase/TableColumnType";
|
|
21
|
+
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|
21
22
|
import BadDataException from "../../Types/Exception/BadDataException";
|
|
22
23
|
import { JSONObject } from "../../Types/JSON";
|
|
23
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
canonicalizeEntityValue,
|
|
26
|
+
keyForHost,
|
|
27
|
+
keyForKubernetesCluster,
|
|
28
|
+
keyForService,
|
|
29
|
+
} from "../../Utils/Telemetry/EntityKey";
|
|
30
|
+
import { keyForContainer } from "../Utils/Telemetry/TelemetryEntity";
|
|
31
|
+
import { getEntityBudget } from "../Utils/Telemetry/EntityRegistry";
|
|
32
|
+
import { EntityScopeQueryValue } from "../Utils/AnalyticsDatabase/StatementGenerator";
|
|
33
|
+
import TelemetryEntityService from "./TelemetryEntityService";
|
|
34
|
+
import TelemetryEntity from "../../Models/DatabaseModels/TelemetryEntity";
|
|
35
|
+
import EntityType from "../../Types/Telemetry/EntityType";
|
|
24
36
|
import ObjectID from "../../Types/ObjectID";
|
|
37
|
+
import PositiveNumber from "../../Types/PositiveNumber";
|
|
25
38
|
import logger, { LogAttributes } from "../Utils/Logger";
|
|
26
39
|
|
|
27
40
|
/*
|
|
@@ -46,6 +59,62 @@ const MAX_GROUP_BY_ATTRIBUTE_KEY_LENGTH: number = 256;
|
|
|
46
59
|
const POINT_TYPE_CACHE_TTL_MS: number = 10 * 60 * 1000;
|
|
47
60
|
const POINT_TYPE_CACHE_MAX_ENTRIES: number = 5000;
|
|
48
61
|
|
|
62
|
+
/*
|
|
63
|
+
* Deliberately short: the registry gains a row for a NEW namespaced
|
|
64
|
+
* service variant asynchronously (throttled reconcile), and until this
|
|
65
|
+
* cache expires a routed service query would keep serving the stale
|
|
66
|
+
* (smaller) key set. 60s bounds that staleness to about one dashboard
|
|
67
|
+
* refresh while still collapsing the burst of per-metric aggregates a
|
|
68
|
+
* single refresh fires.
|
|
69
|
+
*/
|
|
70
|
+
const SERVICE_ENTITY_KEYS_CACHE_TTL_MS: number = 60 * 1000;
|
|
71
|
+
const SERVICE_ENTITY_KEYS_CACHE_MAX_ENTRIES: number = 5000;
|
|
72
|
+
|
|
73
|
+
type EntityMVRoute = {
|
|
74
|
+
/** The resource attribute the entity detail pages filter by. */
|
|
75
|
+
attributeKey: string;
|
|
76
|
+
/** The per-entity 1-minute rollup that serves the filter. */
|
|
77
|
+
tableName: AnalyticsTableName;
|
|
78
|
+
/** Scalar entity-key column — same name on the raw table and the MV. */
|
|
79
|
+
keyColumn: string;
|
|
80
|
+
/**
|
|
81
|
+
* Read-side key derivation, byte-identical to the ingest stamp. Only
|
|
82
|
+
* single-attribute identities are derivable this way; `null` means the
|
|
83
|
+
* key set must come from the registry instead (service — ingest folds
|
|
84
|
+
* service.namespace into the key when present, so one name can map to
|
|
85
|
+
* several keys and a computed bare key would silently drop the
|
|
86
|
+
* namespaced variants' data).
|
|
87
|
+
*/
|
|
88
|
+
keyForValue: ((projectId: string, value: string) => string) | null;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const ENTITY_MV_ROUTES: ReadonlyArray<EntityMVRoute> = [
|
|
92
|
+
{
|
|
93
|
+
attributeKey: "resource.host.name",
|
|
94
|
+
tableName: AnalyticsTableName.MetricItemAggMV1mByHostV2,
|
|
95
|
+
keyColumn: "hostEntityKey",
|
|
96
|
+
keyForValue: keyForHost,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
attributeKey: "resource.k8s.cluster.name",
|
|
100
|
+
tableName: AnalyticsTableName.MetricItemAggMV1mByK8sCluster,
|
|
101
|
+
keyColumn: "k8sClusterEntityKey",
|
|
102
|
+
keyForValue: keyForKubernetesCluster,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
attributeKey: "resource.container.id",
|
|
106
|
+
tableName: AnalyticsTableName.MetricItemAggMV1mByContainer,
|
|
107
|
+
keyColumn: "containerEntityKey",
|
|
108
|
+
keyForValue: keyForContainer,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
attributeKey: "resource.service.name",
|
|
112
|
+
tableName: AnalyticsTableName.MetricItemAggMV1mByService,
|
|
113
|
+
keyColumn: "serviceEntityKey",
|
|
114
|
+
keyForValue: null,
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
|
|
49
118
|
export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
50
119
|
/*
|
|
51
120
|
* (projectId|metricName) -> metricPointType, resolved lazily on the
|
|
@@ -72,6 +141,33 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
72
141
|
private inFlightPointTypeLookups: Map<string, Promise<string | null>> =
|
|
73
142
|
new Map();
|
|
74
143
|
|
|
144
|
+
/*
|
|
145
|
+
* Registry-resolved serviceEntityKey set per aggregateBy call, for
|
|
146
|
+
* queries filtering by `resource.service.name`. Same hand-over pattern
|
|
147
|
+
* as the point-type hint: toAggregateStatement is synchronous, so the
|
|
148
|
+
* async Postgres lookup happens in aggregateBy and is keyed on the
|
|
149
|
+
* (shared) aggregateBy object. An empty array means "registry has no
|
|
150
|
+
* rows for this name" — the builder then refuses to route (raw path).
|
|
151
|
+
*/
|
|
152
|
+
private serviceEntityKeysHintByAggregate: WeakMap<
|
|
153
|
+
AggregateBy<Metric>,
|
|
154
|
+
Array<string>
|
|
155
|
+
> = new WeakMap();
|
|
156
|
+
|
|
157
|
+
/*
|
|
158
|
+
* (projectId|canonical service name) -> serviceEntityKey set. Bounded +
|
|
159
|
+
* short-TTL'd (see SERVICE_ENTITY_KEYS_CACHE_TTL_MS): a dashboard
|
|
160
|
+
* refresh fires one aggregate per metric name, all for the same
|
|
161
|
+
* service, and each would otherwise hit Postgres.
|
|
162
|
+
*/
|
|
163
|
+
private serviceEntityKeysCache: Map<
|
|
164
|
+
string,
|
|
165
|
+
{ keys: Array<string>; expiresAt: number }
|
|
166
|
+
> = new Map();
|
|
167
|
+
|
|
168
|
+
private inFlightServiceEntityKeyLookups: Map<string, Promise<Array<string>>> =
|
|
169
|
+
new Map();
|
|
170
|
+
|
|
75
171
|
public constructor(clickhouseDatabase?: ClickhouseDatabase | undefined) {
|
|
76
172
|
super({ modelType: Metric, database: clickhouseDatabase });
|
|
77
173
|
}
|
|
@@ -96,11 +192,244 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
96
192
|
const pointType: string | null =
|
|
97
193
|
await this.resolveMetricPointType(aggregateBy);
|
|
98
194
|
this.pointTypeHintByAggregate.set(aggregateBy, pointType);
|
|
195
|
+
|
|
196
|
+
/*
|
|
197
|
+
* Service-scoped queries need the registry-resolved serviceEntityKey
|
|
198
|
+
* set before the synchronous statement builder runs. Skipped when
|
|
199
|
+
* the point type already rules out MV routing (distribution
|
|
200
|
+
* metrics never touch the rollups).
|
|
201
|
+
*/
|
|
202
|
+
if (!pointType || !DISTRIBUTION_POINT_TYPES.includes(pointType)) {
|
|
203
|
+
const serviceKeys: Array<string> | null =
|
|
204
|
+
await this.resolveServiceEntityKeysHint(aggregateBy);
|
|
205
|
+
if (serviceKeys) {
|
|
206
|
+
this.serviceEntityKeysHintByAggregate.set(aggregateBy, serviceKeys);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
99
209
|
}
|
|
100
210
|
|
|
101
211
|
return super.aggregateBy(aggregateBy);
|
|
102
212
|
}
|
|
103
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Resolves the serviceEntityKey set for a query whose only entity
|
|
216
|
+
* filter is a `resource.service.name` equality — the shape (and the
|
|
217
|
+
* only shape) the entity-MV builder routes to the per-service rollup.
|
|
218
|
+
* Returns null for every other query shape so no lookup is wasted.
|
|
219
|
+
*/
|
|
220
|
+
private async resolveServiceEntityKeysHint(
|
|
221
|
+
aggregateBy: AggregateBy<Metric>,
|
|
222
|
+
): Promise<Array<string> | null> {
|
|
223
|
+
const queryRecord: Record<string, unknown> =
|
|
224
|
+
(aggregateBy.query as unknown as Record<string, unknown>) || {};
|
|
225
|
+
|
|
226
|
+
// service + entityScope is never routed (see the entity-MV builder).
|
|
227
|
+
if (
|
|
228
|
+
queryRecord["entityScope"] !== undefined &&
|
|
229
|
+
queryRecord["entityScope"] !== null
|
|
230
|
+
) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const attrs: unknown = queryRecord["attributes"];
|
|
235
|
+
if (!attrs || typeof attrs !== "object") {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
const attrEntries: Array<[string, unknown]> = Object.entries(
|
|
239
|
+
attrs as Record<string, unknown>,
|
|
240
|
+
);
|
|
241
|
+
if (attrEntries.length !== 1) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
const [attrKey, attrValue] = attrEntries[0]!;
|
|
245
|
+
if (
|
|
246
|
+
attrKey !== "resource.service.name" ||
|
|
247
|
+
typeof attrValue !== "string" ||
|
|
248
|
+
!attrValue
|
|
249
|
+
) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const projectIdValue: unknown = queryRecord["projectId"];
|
|
254
|
+
let projectId: string = "";
|
|
255
|
+
if (projectIdValue instanceof ObjectID) {
|
|
256
|
+
projectId = projectIdValue.toString();
|
|
257
|
+
} else if (typeof projectIdValue === "string") {
|
|
258
|
+
projectId = projectIdValue;
|
|
259
|
+
}
|
|
260
|
+
if (!projectId) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const cacheKey: string = `${projectId}|${canonicalizeEntityValue(
|
|
265
|
+
attrValue,
|
|
266
|
+
)}`;
|
|
267
|
+
const cached: { keys: Array<string>; expiresAt: number } | undefined =
|
|
268
|
+
this.serviceEntityKeysCache.get(cacheKey);
|
|
269
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
270
|
+
return cached.keys;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const inFlight: Promise<Array<string>> | undefined =
|
|
274
|
+
this.inFlightServiceEntityKeyLookups.get(cacheKey);
|
|
275
|
+
if (inFlight) {
|
|
276
|
+
return inFlight;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const lookup: Promise<Array<string>> = this.lookupServiceEntityKeys(
|
|
280
|
+
projectId,
|
|
281
|
+
attrValue,
|
|
282
|
+
)
|
|
283
|
+
.then((keys: Array<string>) => {
|
|
284
|
+
this.evictExpiredServiceEntityKeyCacheEntries();
|
|
285
|
+
this.serviceEntityKeysCache.set(cacheKey, {
|
|
286
|
+
keys,
|
|
287
|
+
expiresAt: Date.now() + SERVICE_ENTITY_KEYS_CACHE_TTL_MS,
|
|
288
|
+
});
|
|
289
|
+
return keys;
|
|
290
|
+
})
|
|
291
|
+
.finally(() => {
|
|
292
|
+
this.inFlightServiceEntityKeyLookups.delete(cacheKey);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
this.inFlightServiceEntityKeyLookups.set(cacheKey, lookup);
|
|
296
|
+
return lookup;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* All serviceEntityKeys the ingest pipeline can have stamped for rows
|
|
301
|
+
* reporting this `service.name`, from the Postgres TelemetryEntity
|
|
302
|
+
* registry. Service identity folds `service.namespace` into the key
|
|
303
|
+
* when the resource carries one (see the service resolver in
|
|
304
|
+
* Common/Server/Utils/Telemetry/TelemetryEntity), so one name maps to
|
|
305
|
+
* one key per namespace variant — only the registry knows them all.
|
|
306
|
+
*
|
|
307
|
+
* `displayName` equals the canonicalized service.name for every
|
|
308
|
+
* Service row the registry mints (deriveDisplayName prefers the
|
|
309
|
+
* `service.name` identifying attribute), so the query is indexed and
|
|
310
|
+
* tiny; the identifyingAttributes re-check below guards against a
|
|
311
|
+
* displayName that came from some other identity shape. Returns []
|
|
312
|
+
* on a registry miss, on any lookup failure, or when the project's
|
|
313
|
+
* Service registry is at/over its entity budget (new variants stop
|
|
314
|
+
* minting rows there, so the key set can be permanently partial) —
|
|
315
|
+
* the caller then refuses to route (an MV query must never return
|
|
316
|
+
* less data than the raw predicate it replaces, and rows of a
|
|
317
|
+
* namespaced service the registry does not know about would be
|
|
318
|
+
* silently dropped).
|
|
319
|
+
*/
|
|
320
|
+
private async lookupServiceEntityKeys(
|
|
321
|
+
projectId: string,
|
|
322
|
+
serviceName: string,
|
|
323
|
+
): Promise<Array<string>> {
|
|
324
|
+
try {
|
|
325
|
+
const canonicalName: string = canonicalizeEntityValue(serviceName);
|
|
326
|
+
|
|
327
|
+
const rows: Array<TelemetryEntity> = await TelemetryEntityService.findBy({
|
|
328
|
+
query: {
|
|
329
|
+
projectId: new ObjectID(projectId),
|
|
330
|
+
entityType: EntityType.Service,
|
|
331
|
+
displayName: canonicalName,
|
|
332
|
+
},
|
|
333
|
+
select: {
|
|
334
|
+
entityKey: true,
|
|
335
|
+
identifyingAttributes: true,
|
|
336
|
+
},
|
|
337
|
+
limit: LIMIT_PER_PROJECT,
|
|
338
|
+
skip: 0,
|
|
339
|
+
props: { isRoot: true },
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const keys: Set<string> = new Set<string>();
|
|
343
|
+
for (const row of rows) {
|
|
344
|
+
const identifying: Record<string, unknown> =
|
|
345
|
+
(row.identifyingAttributes as Record<string, unknown>) || {};
|
|
346
|
+
if (identifying["service.name"] !== canonicalName) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (typeof row.entityKey === "string" && row.entityKey) {
|
|
350
|
+
keys.add(row.entityKey);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (keys.size === 0) {
|
|
355
|
+
return [];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/*
|
|
359
|
+
* Budget-capped projects must not route. At/over the per-type
|
|
360
|
+
* entity budget, ingest stops minting NEW Service registry rows
|
|
361
|
+
* forever (TelemetryEntityService.beforeCreate) while namespaced
|
|
362
|
+
* serviceEntityKeys keep flowing on signal rows — so a non-empty
|
|
363
|
+
* registry key set may be PERMANENTLY missing identity variants,
|
|
364
|
+
* and a routed `serviceEntityKey IN (...)` would silently return
|
|
365
|
+
* less data than the raw attributes predicate it replaces (the
|
|
366
|
+
* invariant documented above). Same signal as the ingest-side
|
|
367
|
+
* gate; the [] is cached for 60s alongside key hits, so the extra
|
|
368
|
+
* countBy amortizes to one per (project, service name) per minute.
|
|
369
|
+
*/
|
|
370
|
+
const serviceEntityCount: PositiveNumber =
|
|
371
|
+
await TelemetryEntityService.countBy({
|
|
372
|
+
query: {
|
|
373
|
+
projectId: new ObjectID(projectId),
|
|
374
|
+
entityType: EntityType.Service,
|
|
375
|
+
},
|
|
376
|
+
props: { isRoot: true },
|
|
377
|
+
});
|
|
378
|
+
if (
|
|
379
|
+
serviceEntityCount.toNumber() >= getEntityBudget(EntityType.Service)
|
|
380
|
+
) {
|
|
381
|
+
logger.debug(
|
|
382
|
+
"Service entity registry is budget-capped; refusing MV routing",
|
|
383
|
+
);
|
|
384
|
+
return [];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/*
|
|
388
|
+
* Union in the deterministic namespace-less key: a service that
|
|
389
|
+
* ALSO reports without a namespace may not have minted its bare
|
|
390
|
+
* registry row yet (reconcile is throttled and budget-gated), and
|
|
391
|
+
* adding a key can only widen the MV result, never narrow it.
|
|
392
|
+
*/
|
|
393
|
+
keys.add(keyForService(projectId, serviceName));
|
|
394
|
+
|
|
395
|
+
return Array.from(keys).sort();
|
|
396
|
+
} catch (err) {
|
|
397
|
+
/*
|
|
398
|
+
* Lookup failures must not fail the aggregation — without the hint
|
|
399
|
+
* the builder falls back to the raw table, which is always correct.
|
|
400
|
+
*/
|
|
401
|
+
logger.debug("Service entity key lookup failed");
|
|
402
|
+
logger.debug(err);
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private evictExpiredServiceEntityKeyCacheEntries(): void {
|
|
408
|
+
if (
|
|
409
|
+
this.serviceEntityKeysCache.size < SERVICE_ENTITY_KEYS_CACHE_MAX_ENTRIES
|
|
410
|
+
) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const now: number = Date.now();
|
|
414
|
+
for (const [key, entry] of this.serviceEntityKeysCache) {
|
|
415
|
+
if (entry.expiresAt <= now) {
|
|
416
|
+
this.serviceEntityKeysCache.delete(key);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// Mirrors evictExpiredPointTypeCacheEntries: never clear() wholesale.
|
|
420
|
+
while (
|
|
421
|
+
this.serviceEntityKeysCache.size >= SERVICE_ENTITY_KEYS_CACHE_MAX_ENTRIES
|
|
422
|
+
) {
|
|
423
|
+
const oldestKey: string | undefined = this.serviceEntityKeysCache
|
|
424
|
+
.keys()
|
|
425
|
+
.next().value;
|
|
426
|
+
if (oldestKey === undefined) {
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
this.serviceEntityKeysCache.delete(oldestKey);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
104
433
|
private async resolveMetricPointType(
|
|
105
434
|
aggregateBy: AggregateBy<Metric>,
|
|
106
435
|
): Promise<string | null> {
|
|
@@ -336,6 +665,175 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
336
665
|
statement.append(`) AS attributes`);
|
|
337
666
|
}
|
|
338
667
|
|
|
668
|
+
/**
|
|
669
|
+
* Validated Top-K spec, or null when the caller did not request one.
|
|
670
|
+
* `count` is parameter-bound where it reaches SQL and `rankBy` only
|
|
671
|
+
* ever selects between two hardcoded aggregate names, so this is
|
|
672
|
+
* shape validation (like getSanitizedGroupByAttributeKeys), not
|
|
673
|
+
* injection defense.
|
|
674
|
+
*/
|
|
675
|
+
private getSanitizedTopK(
|
|
676
|
+
aggregateBy: AggregateBy<Metric>,
|
|
677
|
+
): { count: number; rankBy: "max" | "avg" } | null {
|
|
678
|
+
const topK: AggregateBy<Metric>["topK"] = aggregateBy.topK;
|
|
679
|
+
if (!topK) {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
const count: number = Math.floor(Number(topK.count));
|
|
683
|
+
if (!Number.isFinite(count) || count < 1 || count > LIMIT_PER_PROJECT) {
|
|
684
|
+
throw new BadDataException(
|
|
685
|
+
`topK.count must be a positive integer <= ${LIMIT_PER_PROJECT}.`,
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
if (topK.rankBy !== "max" && topK.rankBy !== "avg") {
|
|
689
|
+
throw new BadDataException(`topK.rankBy must be 'max' or 'avg'.`);
|
|
690
|
+
}
|
|
691
|
+
return { count, rankBy: topK.rankBy };
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Appends the raw-table expressions that identify a group — model
|
|
696
|
+
* group-by columns as identifiers, attribute keys as parameter-bound
|
|
697
|
+
* `attributes[...]` lookups — in a FIXED order, so the Top-K
|
|
698
|
+
* IN-restriction tuple, the ranking subquery's SELECT/GROUP BY, and
|
|
699
|
+
* the totalGroups counter always line up element-for-element.
|
|
700
|
+
*/
|
|
701
|
+
private appendRawGroupExpressionList(
|
|
702
|
+
statement: Statement,
|
|
703
|
+
groupByKeys: Array<string>,
|
|
704
|
+
attributeGroupKeys: Array<string>,
|
|
705
|
+
): void {
|
|
706
|
+
let isFirst: boolean = true;
|
|
707
|
+
for (const key of groupByKeys) {
|
|
708
|
+
if (!isFirst) {
|
|
709
|
+
statement.append(`, `);
|
|
710
|
+
}
|
|
711
|
+
statement.append(key);
|
|
712
|
+
isFirst = false;
|
|
713
|
+
}
|
|
714
|
+
for (const key of attributeGroupKeys) {
|
|
715
|
+
if (!isFirst) {
|
|
716
|
+
statement.append(`, `);
|
|
717
|
+
}
|
|
718
|
+
statement.append(
|
|
719
|
+
SQL`attributes[${{ value: key, type: TableColumnType.Text }}]`,
|
|
720
|
+
);
|
|
721
|
+
isFirst = false;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Appends the Top-K group restriction predicate:
|
|
727
|
+
*
|
|
728
|
+
* ` AND (<group exprs>) GLOBAL IN (SELECT <group exprs> FROM <table>
|
|
729
|
+
* WHERE ... GROUP BY <group exprs>
|
|
730
|
+
* ORDER BY max|avg(<column>) DESC LIMIT <k>)`
|
|
731
|
+
*
|
|
732
|
+
* The ranking subquery scores each group over the WHOLE query window
|
|
733
|
+
* (no time bucketing) so a series that spiked early ranks the same as
|
|
734
|
+
* one spiking late. Must be appended in raw-table WHERE scope — the
|
|
735
|
+
* innermost WHERE of the surrounding builder — where the group
|
|
736
|
+
* expressions are valid. Ranking uses plain max/avg of the raw
|
|
737
|
+
* column (not the distribution-aware expressions): for distribution
|
|
738
|
+
* metrics that scores by per-export-interval sums, which preserves
|
|
739
|
+
* relative group ordering well enough for series selection.
|
|
740
|
+
*
|
|
741
|
+
* GLOBAL IN is load-bearing: this predicate sits in a query on the
|
|
742
|
+
* Distributed Metric table whose subquery reads the SAME Distributed
|
|
743
|
+
* table, which multi-shard ClickHouse rejects outright as a
|
|
744
|
+
* double-distributed subquery (Code 288, distributed_product_mode =
|
|
745
|
+
* 'deny' by default) — every grouped Top-K chart would 500 on a
|
|
746
|
+
* 2+-shard cluster. A shard-local rewrite would be wrong anyway: the
|
|
747
|
+
* sharding key is cityHash64(projectId, name, primaryEntityId), so
|
|
748
|
+
* one project+metric's groups span shards and the ranking must be
|
|
749
|
+
* computed once globally, not per shard. On a single shard GLOBAL is
|
|
750
|
+
* a semantic no-op. (The mutable-metric twin below deliberately stays
|
|
751
|
+
* plain IN: its predicate lives in an initiator-evaluated outer query
|
|
752
|
+
* over a subquery-FROM, which is not subject to the denial.)
|
|
753
|
+
*/
|
|
754
|
+
private appendTopKGroupRestriction(data: {
|
|
755
|
+
statement: Statement;
|
|
756
|
+
databaseName: string;
|
|
757
|
+
aggregateBy: AggregateBy<Metric>;
|
|
758
|
+
topK: { count: number; rankBy: "max" | "avg" };
|
|
759
|
+
groupByKeys: Array<string>;
|
|
760
|
+
attributeGroupKeys: Array<string>;
|
|
761
|
+
}): void {
|
|
762
|
+
const statement: Statement = data.statement;
|
|
763
|
+
const aggregationColumn: string =
|
|
764
|
+
data.aggregateBy.aggregateColumnName.toString();
|
|
765
|
+
|
|
766
|
+
statement.append(` AND (`);
|
|
767
|
+
this.appendRawGroupExpressionList(
|
|
768
|
+
statement,
|
|
769
|
+
data.groupByKeys,
|
|
770
|
+
data.attributeGroupKeys,
|
|
771
|
+
);
|
|
772
|
+
statement.append(`) GLOBAL IN (SELECT `);
|
|
773
|
+
this.appendRawGroupExpressionList(
|
|
774
|
+
statement,
|
|
775
|
+
data.groupByKeys,
|
|
776
|
+
data.attributeGroupKeys,
|
|
777
|
+
);
|
|
778
|
+
statement.append(
|
|
779
|
+
` FROM ${data.databaseName}.${this.model.tableName} WHERE TRUE `,
|
|
780
|
+
);
|
|
781
|
+
statement.append(
|
|
782
|
+
this.statementGenerator.toWhereStatement(data.aggregateBy.query),
|
|
783
|
+
);
|
|
784
|
+
statement.append(this.getRetentionReadFilter());
|
|
785
|
+
statement.append(` GROUP BY `);
|
|
786
|
+
this.appendRawGroupExpressionList(
|
|
787
|
+
statement,
|
|
788
|
+
data.groupByKeys,
|
|
789
|
+
data.attributeGroupKeys,
|
|
790
|
+
);
|
|
791
|
+
statement.append(
|
|
792
|
+
` ORDER BY ${data.topK.rankBy}(${aggregationColumn}) DESC`,
|
|
793
|
+
);
|
|
794
|
+
statement.append(
|
|
795
|
+
SQL` LIMIT ${{
|
|
796
|
+
value: data.topK.count,
|
|
797
|
+
type: TableColumnType.Number,
|
|
798
|
+
}}`,
|
|
799
|
+
);
|
|
800
|
+
statement.append(`)`);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Appends `, (SELECT uniqExact(<group exprs>) FROM <table>
|
|
805
|
+
* WHERE ...) AS __total_groups` — a scalar subquery counting every
|
|
806
|
+
* distinct group in the window BEFORE Top-K trims them. The value is
|
|
807
|
+
* constant across result rows; AnalyticsDatabaseService._aggregateBy
|
|
808
|
+
* reads it off the first row into AggregatedResult.totalGroups and it
|
|
809
|
+
* never reaches the per-row payload. Must be appended in SELECT-list
|
|
810
|
+
* position of the outer query.
|
|
811
|
+
*/
|
|
812
|
+
private appendTotalGroupsColumn(data: {
|
|
813
|
+
statement: Statement;
|
|
814
|
+
databaseName: string;
|
|
815
|
+
aggregateBy: AggregateBy<Metric>;
|
|
816
|
+
groupByKeys: Array<string>;
|
|
817
|
+
attributeGroupKeys: Array<string>;
|
|
818
|
+
}): void {
|
|
819
|
+
const statement: Statement = data.statement;
|
|
820
|
+
|
|
821
|
+
statement.append(`, (SELECT uniqExact(`);
|
|
822
|
+
this.appendRawGroupExpressionList(
|
|
823
|
+
statement,
|
|
824
|
+
data.groupByKeys,
|
|
825
|
+
data.attributeGroupKeys,
|
|
826
|
+
);
|
|
827
|
+
statement.append(
|
|
828
|
+
`) FROM ${data.databaseName}.${this.model.tableName} WHERE TRUE `,
|
|
829
|
+
);
|
|
830
|
+
statement.append(
|
|
831
|
+
this.statementGenerator.toWhereStatement(data.aggregateBy.query),
|
|
832
|
+
);
|
|
833
|
+
statement.append(this.getRetentionReadFilter());
|
|
834
|
+
statement.append(`) AS __total_groups`);
|
|
835
|
+
}
|
|
836
|
+
|
|
339
837
|
/*
|
|
340
838
|
* Cascade deletes from `MetricItemV3` into the aggregating
|
|
341
839
|
* materialized-view target tables.
|
|
@@ -352,10 +850,11 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
352
850
|
* The cascade only runs when the caller scoped the delete by
|
|
353
851
|
* `primaryEntityId`. Global time-based purges (TTL cleanup) are handled
|
|
354
852
|
* by each MV table's own `retentionDate TTL DELETE`, so cascading those
|
|
355
|
-
* would pointlessly scan the whole MV. The per-
|
|
356
|
-
* (`MetricItemAggMV1mByHostV2`
|
|
357
|
-
* `
|
|
358
|
-
*
|
|
853
|
+
* would pointlessly scan the whole MV. The per-entity MVs
|
|
854
|
+
* (`MetricItemAggMV1mByHostV2` / `...ByService` / `...ByK8sCluster` /
|
|
855
|
+
* `...ByContainer`) are keyed by their scalar entity-key column rather
|
|
856
|
+
* than `primaryEntityId`, so an entity-scoped delete has nothing to
|
|
857
|
+
* remove there — skip them.
|
|
359
858
|
*/
|
|
360
859
|
public override async deleteBy(deleteBy: DeleteBy<Metric>): Promise<void> {
|
|
361
860
|
await super.deleteBy(deleteBy);
|
|
@@ -498,11 +997,12 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
498
997
|
|
|
499
998
|
if (!isPercentileAggregation(aggregateBy.aggregationType)) {
|
|
500
999
|
/*
|
|
501
|
-
* Try the per-
|
|
502
|
-
*
|
|
503
|
-
* the only
|
|
504
|
-
* (no
|
|
505
|
-
* to the project/primaryEntityId MV, then to the base
|
|
1000
|
+
* Try the per-entity MVs first — entity detail pages (host, k8s
|
|
1001
|
+
* cluster, container, service) are the dominant filtered path and
|
|
1002
|
+
* the entity-keyed rollups are the only ones that can serve them.
|
|
1003
|
+
* If none applies (no entity filter, or extra attrs/groupBy), fall
|
|
1004
|
+
* through to the project/primaryEntityId MV, then to the base
|
|
1005
|
+
* table.
|
|
506
1006
|
*
|
|
507
1007
|
* Distribution metrics (histograms/summaries) must skip the
|
|
508
1008
|
* MVs entirely: their states collapse `value`, which for
|
|
@@ -514,12 +1014,12 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
514
1014
|
attributeGroupKeys.length === 0 &&
|
|
515
1015
|
!this.isDistributionMetricAggregate(aggregateBy)
|
|
516
1016
|
) {
|
|
517
|
-
const
|
|
1017
|
+
const entityMvStatement: {
|
|
518
1018
|
statement: Statement;
|
|
519
1019
|
columns: Array<string>;
|
|
520
|
-
} | null = this.
|
|
521
|
-
if (
|
|
522
|
-
return
|
|
1020
|
+
} | null = this.tryBuildEntityAggregateMVStatement(aggregateBy);
|
|
1021
|
+
if (entityMvStatement) {
|
|
1022
|
+
return entityMvStatement;
|
|
523
1023
|
}
|
|
524
1024
|
const mvStatement: {
|
|
525
1025
|
statement: Statement;
|
|
@@ -693,6 +1193,30 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
693
1193
|
*/
|
|
694
1194
|
this.appendAttributeGroupMapColumn(statement, attributeGroupKeys);
|
|
695
1195
|
|
|
1196
|
+
/*
|
|
1197
|
+
* Server-side Top-K: rank groups over the whole window, restrict
|
|
1198
|
+
* the bucketed quantile to the winners, and surface the pre-trim
|
|
1199
|
+
* group count. Only meaningful when the aggregation is grouped —
|
|
1200
|
+
* p95-by-host must get the same series selection as the scalar
|
|
1201
|
+
* paths.
|
|
1202
|
+
*/
|
|
1203
|
+
const percentileTopK: { count: number; rankBy: "max" | "avg" } | null =
|
|
1204
|
+
this.getSanitizedTopK(aggregateBy);
|
|
1205
|
+
const percentileApplyTopK: boolean = Boolean(
|
|
1206
|
+
percentileTopK &&
|
|
1207
|
+
(groupByKeys.length > 0 || attributeGroupKeys.length > 0),
|
|
1208
|
+
);
|
|
1209
|
+
|
|
1210
|
+
if (percentileApplyTopK) {
|
|
1211
|
+
this.appendTotalGroupsColumn({
|
|
1212
|
+
statement,
|
|
1213
|
+
databaseName,
|
|
1214
|
+
aggregateBy,
|
|
1215
|
+
groupByKeys,
|
|
1216
|
+
attributeGroupKeys,
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
|
|
696
1220
|
statement.append(SQL` FROM (`);
|
|
697
1221
|
statement.append(`SELECT ${innerSelectClause}`);
|
|
698
1222
|
this.appendAttributeGroupExtractionColumns(statement, attributeGroupKeys);
|
|
@@ -701,6 +1225,16 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
701
1225
|
);
|
|
702
1226
|
statement.append(whereStatement);
|
|
703
1227
|
statement.append(this.getRetentionReadFilter());
|
|
1228
|
+
if (percentileApplyTopK) {
|
|
1229
|
+
this.appendTopKGroupRestriction({
|
|
1230
|
+
statement,
|
|
1231
|
+
databaseName,
|
|
1232
|
+
aggregateBy,
|
|
1233
|
+
topK: percentileTopK!,
|
|
1234
|
+
groupByKeys,
|
|
1235
|
+
attributeGroupKeys,
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
704
1238
|
statement.append(SQL`) `);
|
|
705
1239
|
|
|
706
1240
|
/*
|
|
@@ -748,16 +1282,28 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
748
1282
|
|
|
749
1283
|
/*
|
|
750
1284
|
* Match the read-path settings the base aggregator now appends (see
|
|
751
|
-
* AnalyticsDatabaseService.toAggregateStatement)
|
|
752
|
-
*
|
|
753
|
-
*
|
|
1285
|
+
* AnalyticsDatabaseService.toAggregateStatement), including the 45s
|
|
1286
|
+
* execution cap and the caller-selected overflow mode. The
|
|
1287
|
+
* percentile path bypasses the base method, so we mirror them here
|
|
1288
|
+
* to keep cluster behavior consistent across aggregation kinds.
|
|
1289
|
+
*
|
|
1290
|
+
* transform_null_in=1 rides along only when the Top-K restriction
|
|
1291
|
+
* is present: grouping by a Nullable model column (metricPointType,
|
|
1292
|
+
* unit, ...) puts NULL group keys in play, and under the ClickHouse
|
|
1293
|
+
* default (transform_null_in=0) NULL never matches an IN — a
|
|
1294
|
+
* NULL-keyed group could win a ranking slot yet return zero rows.
|
|
1295
|
+
* Query-wide it is safe here: user-filter IN lists are
|
|
1296
|
+
* parameter-bound scalar arrays that never carry NULL.
|
|
754
1297
|
*/
|
|
755
1298
|
statement.append(
|
|
756
1299
|
getQuerySettings({
|
|
1300
|
+
maxExecutionTimeInSeconds: 45,
|
|
1301
|
+
timeoutOverflowMode: this.getTimeoutOverflowMode(aggregateBy),
|
|
757
1302
|
additionalSettings: {
|
|
758
1303
|
optimize_aggregation_in_order: 1,
|
|
759
1304
|
optimize_move_to_prewhere: 1,
|
|
760
1305
|
max_threads: 4,
|
|
1306
|
+
...(percentileApplyTopK ? { transform_null_in: 1 } : {}),
|
|
761
1307
|
},
|
|
762
1308
|
}),
|
|
763
1309
|
);
|
|
@@ -898,6 +1444,17 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
898
1444
|
|
|
899
1445
|
const statement: Statement = SQL``;
|
|
900
1446
|
|
|
1447
|
+
/*
|
|
1448
|
+
* Server-side Top-K: rank groups over the whole window, restrict
|
|
1449
|
+
* the bucketed aggregation to the winners, and surface the pre-trim
|
|
1450
|
+
* group count. Only meaningful when the aggregation is grouped.
|
|
1451
|
+
*/
|
|
1452
|
+
const scalarTopK: { count: number; rankBy: "max" | "avg" } | null =
|
|
1453
|
+
this.getSanitizedTopK(aggregateBy);
|
|
1454
|
+
const scalarApplyTopK: boolean = Boolean(
|
|
1455
|
+
scalarTopK && (groupByKeys.length > 0 || attributeGroupKeys.length > 0),
|
|
1456
|
+
);
|
|
1457
|
+
|
|
901
1458
|
statement.append(
|
|
902
1459
|
`SELECT ${aggregationExpression} as ${aggregationColumn}, ${AggregateUtil.buildBucketTimestampSelect(resolvedInterval, aggregationTimestampColumn)}`,
|
|
903
1460
|
);
|
|
@@ -905,6 +1462,15 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
905
1462
|
statement.append(`, ${key}`);
|
|
906
1463
|
}
|
|
907
1464
|
this.appendAttributeGroupMapColumn(statement, attributeGroupKeys);
|
|
1465
|
+
if (scalarApplyTopK) {
|
|
1466
|
+
this.appendTotalGroupsColumn({
|
|
1467
|
+
statement,
|
|
1468
|
+
databaseName,
|
|
1469
|
+
aggregateBy,
|
|
1470
|
+
groupByKeys,
|
|
1471
|
+
attributeGroupKeys,
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
908
1474
|
|
|
909
1475
|
if (attributeGroupKeys.length > 0) {
|
|
910
1476
|
/*
|
|
@@ -935,6 +1501,16 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
935
1501
|
);
|
|
936
1502
|
statement.append(whereStatement);
|
|
937
1503
|
statement.append(this.getRetentionReadFilter());
|
|
1504
|
+
if (scalarApplyTopK) {
|
|
1505
|
+
this.appendTopKGroupRestriction({
|
|
1506
|
+
statement,
|
|
1507
|
+
databaseName,
|
|
1508
|
+
aggregateBy,
|
|
1509
|
+
topK: scalarTopK!,
|
|
1510
|
+
groupByKeys,
|
|
1511
|
+
attributeGroupKeys,
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
938
1514
|
statement.append(SQL`) `);
|
|
939
1515
|
} else {
|
|
940
1516
|
statement.append(
|
|
@@ -942,6 +1518,16 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
942
1518
|
);
|
|
943
1519
|
statement.append(whereStatement);
|
|
944
1520
|
statement.append(this.getRetentionReadFilter());
|
|
1521
|
+
if (scalarApplyTopK) {
|
|
1522
|
+
this.appendTopKGroupRestriction({
|
|
1523
|
+
statement,
|
|
1524
|
+
databaseName,
|
|
1525
|
+
aggregateBy,
|
|
1526
|
+
topK: scalarTopK!,
|
|
1527
|
+
groupByKeys,
|
|
1528
|
+
attributeGroupKeys,
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
945
1531
|
}
|
|
946
1532
|
|
|
947
1533
|
/*
|
|
@@ -986,12 +1572,19 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
986
1572
|
}} `,
|
|
987
1573
|
);
|
|
988
1574
|
|
|
1575
|
+
/*
|
|
1576
|
+
* transform_null_in=1 only when the Top-K IN restriction is present
|
|
1577
|
+
* — see the percentile path's settings comment for the rationale.
|
|
1578
|
+
*/
|
|
989
1579
|
statement.append(
|
|
990
1580
|
getQuerySettings({
|
|
1581
|
+
maxExecutionTimeInSeconds: 45,
|
|
1582
|
+
timeoutOverflowMode: this.getTimeoutOverflowMode(aggregateBy),
|
|
991
1583
|
additionalSettings: {
|
|
992
1584
|
optimize_aggregation_in_order: 1,
|
|
993
1585
|
optimize_move_to_prewhere: 1,
|
|
994
1586
|
max_threads: 4,
|
|
1587
|
+
...(scalarApplyTopK ? { transform_null_in: 1 } : {}),
|
|
995
1588
|
},
|
|
996
1589
|
}),
|
|
997
1590
|
);
|
|
@@ -1049,11 +1642,37 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
1049
1642
|
aggregateBy.sort!,
|
|
1050
1643
|
);
|
|
1051
1644
|
|
|
1645
|
+
/*
|
|
1646
|
+
* Server-side Top-K for the (model-column) grouped mutable path.
|
|
1647
|
+
* The ranking subquery and totalGroups counter each re-run the
|
|
1648
|
+
* dedup subquery (buildMutableMetricDedupSource) — acceptable
|
|
1649
|
+
* because the mutable table is small by design (bounded-cardinality
|
|
1650
|
+
* business metrics), unlike the raw Metric table.
|
|
1651
|
+
*/
|
|
1652
|
+
const mutableTopK: { count: number; rankBy: "max" | "avg" } | null =
|
|
1653
|
+
this.getSanitizedTopK(aggregateBy);
|
|
1654
|
+
const mutableGroupByKeys: Array<string> = aggregateBy.groupBy
|
|
1655
|
+
? Object.keys(aggregateBy.groupBy)
|
|
1656
|
+
: [];
|
|
1657
|
+
const mutableApplyTopK: boolean = Boolean(
|
|
1658
|
+
mutableTopK && mutableGroupByKeys.length > 0,
|
|
1659
|
+
);
|
|
1660
|
+
|
|
1052
1661
|
const statement: Statement = SQL``;
|
|
1053
1662
|
|
|
1663
|
+
statement.append(SQL`SELECT `).append(select.statement);
|
|
1664
|
+
|
|
1665
|
+
if (mutableApplyTopK) {
|
|
1666
|
+
statement.append(`, (SELECT uniqExact(`);
|
|
1667
|
+
this.appendRawGroupExpressionList(statement, mutableGroupByKeys, []);
|
|
1668
|
+
statement.append(`)`);
|
|
1669
|
+
statement.append(
|
|
1670
|
+
this.buildMutableMetricDedupSource(databaseName, aggregateBy),
|
|
1671
|
+
);
|
|
1672
|
+
statement.append(`) AS __total_groups`);
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1054
1675
|
statement
|
|
1055
|
-
.append(SQL`SELECT `)
|
|
1056
|
-
.append(select.statement)
|
|
1057
1676
|
.append(
|
|
1058
1677
|
SQL`
|
|
1059
1678
|
FROM (
|
|
@@ -1091,6 +1710,35 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
1091
1710
|
.append(outerWhereStatement)
|
|
1092
1711
|
.append(SQL` AND retentionDate >= now()`);
|
|
1093
1712
|
|
|
1713
|
+
if (mutableApplyTopK) {
|
|
1714
|
+
/*
|
|
1715
|
+
* Plain (non-GLOBAL) IN is deliberate here, unlike
|
|
1716
|
+
* appendTopKGroupRestriction: this predicate sits in the OUTER
|
|
1717
|
+
* query whose FROM is the argMax-dedup subquery — the initiator
|
|
1718
|
+
* evaluates it, so it is not a double-distributed subquery and
|
|
1719
|
+
* multi-shard ClickHouse accepts it as-is.
|
|
1720
|
+
*/
|
|
1721
|
+
statement.append(` AND (`);
|
|
1722
|
+
this.appendRawGroupExpressionList(statement, mutableGroupByKeys, []);
|
|
1723
|
+
statement.append(`) IN (SELECT `);
|
|
1724
|
+
this.appendRawGroupExpressionList(statement, mutableGroupByKeys, []);
|
|
1725
|
+
statement.append(
|
|
1726
|
+
this.buildMutableMetricDedupSource(databaseName, aggregateBy),
|
|
1727
|
+
);
|
|
1728
|
+
statement.append(` GROUP BY `);
|
|
1729
|
+
this.appendRawGroupExpressionList(statement, mutableGroupByKeys, []);
|
|
1730
|
+
statement.append(
|
|
1731
|
+
` ORDER BY ${mutableTopK!.rankBy}(${aggregateBy.aggregateColumnName.toString()}) DESC`,
|
|
1732
|
+
);
|
|
1733
|
+
statement.append(
|
|
1734
|
+
SQL` LIMIT ${{
|
|
1735
|
+
value: mutableTopK!.count,
|
|
1736
|
+
type: TableColumnType.Number,
|
|
1737
|
+
}}`,
|
|
1738
|
+
);
|
|
1739
|
+
statement.append(`)`);
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1094
1742
|
/*
|
|
1095
1743
|
* Mirror the base builder's Total handling: the SELECT above (shared
|
|
1096
1744
|
* toAggregateSelectStatement) already emits `min(time)` for a `Total`
|
|
@@ -1155,11 +1803,20 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
1155
1803
|
}}`,
|
|
1156
1804
|
);
|
|
1157
1805
|
|
|
1806
|
+
/*
|
|
1807
|
+
* transform_null_in=1 only when the Top-K IN restriction is present
|
|
1808
|
+
* — see the percentile path's settings comment for the rationale
|
|
1809
|
+
* (mutable group-by keys are Nullable model columns too, e.g.
|
|
1810
|
+
* primaryEntityType).
|
|
1811
|
+
*/
|
|
1158
1812
|
statement.append(
|
|
1159
1813
|
getQuerySettings({
|
|
1814
|
+
maxExecutionTimeInSeconds: 45,
|
|
1815
|
+
timeoutOverflowMode: this.getTimeoutOverflowMode(aggregateBy),
|
|
1160
1816
|
additionalSettings: {
|
|
1161
1817
|
optimize_move_to_prewhere: 1,
|
|
1162
1818
|
max_threads: 4,
|
|
1819
|
+
...(mutableApplyTopK ? { transform_null_in: 1 } : {}),
|
|
1163
1820
|
},
|
|
1164
1821
|
}),
|
|
1165
1822
|
);
|
|
@@ -1177,6 +1834,63 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
1177
1834
|
};
|
|
1178
1835
|
}
|
|
1179
1836
|
|
|
1837
|
+
/**
|
|
1838
|
+
* A fresh `FROM (...) WHERE ...` fragment over the argMax-deduped
|
|
1839
|
+
* mutable-metric rows — the same source the main mutable aggregate
|
|
1840
|
+
* reads from — for the Top-K ranking subquery and totalGroups
|
|
1841
|
+
* counter, which need their own scan of the deduped rows (the
|
|
1842
|
+
* versioned raw rows must never be ranked directly, or a metric
|
|
1843
|
+
* updated N times would rank by its stalest value).
|
|
1844
|
+
*/
|
|
1845
|
+
private buildMutableMetricDedupSource(
|
|
1846
|
+
databaseName: string,
|
|
1847
|
+
aggregateBy: AggregateBy<Metric>,
|
|
1848
|
+
): Statement {
|
|
1849
|
+
const source: Statement = SQL``;
|
|
1850
|
+
source
|
|
1851
|
+
.append(
|
|
1852
|
+
SQL`
|
|
1853
|
+
FROM (
|
|
1854
|
+
SELECT
|
|
1855
|
+
projectId,
|
|
1856
|
+
name,
|
|
1857
|
+
primaryEntityId,
|
|
1858
|
+
primaryEntityType,
|
|
1859
|
+
metricPointId,
|
|
1860
|
+
argMax(metricPointType, version) AS metricPointType,
|
|
1861
|
+
argMax(time, version) AS time,
|
|
1862
|
+
argMax(timeUnixNano, version) AS timeUnixNano,
|
|
1863
|
+
argMax(attributes, version) AS attributes,
|
|
1864
|
+
argMax(attributeKeys, version) AS attributeKeys,
|
|
1865
|
+
argMax(value, version) AS value,
|
|
1866
|
+
argMax(retentionDate, version) AS retentionDate,
|
|
1867
|
+
argMax(isDeleted, version) AS isDeleted
|
|
1868
|
+
FROM ${databaseName}.${AnalyticsTableName.MutableMetric}
|
|
1869
|
+
WHERE TRUE
|
|
1870
|
+
`,
|
|
1871
|
+
)
|
|
1872
|
+
.append(
|
|
1873
|
+
this.statementGenerator.toWhereStatement(
|
|
1874
|
+
this.getMutableMetricInnerQuery(aggregateBy.query),
|
|
1875
|
+
),
|
|
1876
|
+
)
|
|
1877
|
+
.append(
|
|
1878
|
+
SQL`
|
|
1879
|
+
GROUP BY
|
|
1880
|
+
projectId,
|
|
1881
|
+
name,
|
|
1882
|
+
primaryEntityId,
|
|
1883
|
+
primaryEntityType,
|
|
1884
|
+
metricPointId
|
|
1885
|
+
)
|
|
1886
|
+
WHERE isDeleted = false
|
|
1887
|
+
`,
|
|
1888
|
+
)
|
|
1889
|
+
.append(this.statementGenerator.toWhereStatement(aggregateBy.query))
|
|
1890
|
+
.append(SQL` AND retentionDate >= now()`);
|
|
1891
|
+
return source;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1180
1894
|
private getMutableMetricInnerQuery(query: Query<Metric>): Query<Metric> {
|
|
1181
1895
|
const queryRecord: Record<string, unknown> = query as unknown as Record<
|
|
1182
1896
|
string,
|
|
@@ -1323,10 +2037,11 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
1323
2037
|
});
|
|
1324
2038
|
/*
|
|
1325
2039
|
* The MV is bucketed at 1 minute, so every time-bucketed interval
|
|
1326
|
-
* (Minute /
|
|
1327
|
-
* acceptable — the honored interval flows into the
|
|
1328
|
-
* `Total` (whole-window, no bucketing) is the one
|
|
1329
|
-
* to the raw-table builder, which knows how to
|
|
2040
|
+
* (Minute / FiveMinutes / ... / Year) is >= MV resolution and
|
|
2041
|
+
* acceptable — the honored interval flows into the shared bucket
|
|
2042
|
+
* expression below. `Total` (whole-window, no bucketing) is the one
|
|
2043
|
+
* exception: fall back to the raw-table builder, which knows how to
|
|
2044
|
+
* collapse the window.
|
|
1330
2045
|
*/
|
|
1331
2046
|
if (AggregateUtil.isTotalAggregation(interval)) {
|
|
1332
2047
|
return null;
|
|
@@ -1382,8 +2097,6 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
1382
2097
|
}
|
|
1383
2098
|
const databaseName: string = this.database.getDatasourceOptions().database!;
|
|
1384
2099
|
|
|
1385
|
-
const intervalLower: string = interval.toLowerCase();
|
|
1386
|
-
|
|
1387
2100
|
let mergedExpr: string;
|
|
1388
2101
|
if (aggType === AggregationType.Sum) {
|
|
1389
2102
|
mergedExpr = `sumMerge(valueSumState)`;
|
|
@@ -1414,7 +2127,7 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
1414
2127
|
const statement: Statement = SQL``;
|
|
1415
2128
|
|
|
1416
2129
|
statement.append(
|
|
1417
|
-
`SELECT ${mergedExpr} as value,
|
|
2130
|
+
`SELECT ${mergedExpr} as value, ${AggregateUtil.buildBucketTimestampExpression(interval, "bucketTime")} as time`,
|
|
1418
2131
|
);
|
|
1419
2132
|
statement.append(SQL` FROM ${databaseName}.MetricItemAggMV1m`);
|
|
1420
2133
|
statement.append(
|
|
@@ -1438,6 +2151,8 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
1438
2151
|
);
|
|
1439
2152
|
statement.append(
|
|
1440
2153
|
getQuerySettings({
|
|
2154
|
+
maxExecutionTimeInSeconds: 45,
|
|
2155
|
+
timeoutOverflowMode: this.getTimeoutOverflowMode(aggregateBy),
|
|
1441
2156
|
additionalSettings: {
|
|
1442
2157
|
optimize_aggregation_in_order: 1,
|
|
1443
2158
|
optimize_move_to_prewhere: 1,
|
|
@@ -1463,30 +2178,55 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
1463
2178
|
}
|
|
1464
2179
|
|
|
1465
2180
|
/*
|
|
1466
|
-
*
|
|
2181
|
+
* Entity-scoped materialized-view fast path (per-entity 1-minute
|
|
2182
|
+
* rollups, keyed by the ingest-stamped scalar entity-key columns).
|
|
2183
|
+
* Entity keys canonicalize their value (trim + lowercase), so spelling
|
|
2184
|
+
* drift in the reported identity still lands on one rollup stream.
|
|
1467
2185
|
*
|
|
1468
|
-
*
|
|
1469
|
-
*
|
|
1470
|
-
*
|
|
1471
|
-
*
|
|
1472
|
-
*
|
|
1473
|
-
*
|
|
2186
|
+
* Routing decision table — ALL of these outer gates must hold:
|
|
2187
|
+
* Sum/Avg/Min/Max/Count over `value` bucketed by `time`; no group-by
|
|
2188
|
+
* of any kind (an entity-key-keyed MV cannot label legend series);
|
|
2189
|
+
* not a distribution metric and not a percentile (enforced by the
|
|
2190
|
+
* caller); a bucketed (non-Total) interval; a projectId (the entity
|
|
2191
|
+
* key is tenant-scoped by construction); and no query columns beyond
|
|
2192
|
+
* projectId/name/time/attributes/entityScope.
|
|
2193
|
+
* Then exactly ONE of these entity-filter shapes routes:
|
|
1474
2194
|
*
|
|
1475
|
-
*
|
|
1476
|
-
*
|
|
1477
|
-
*
|
|
1478
|
-
*
|
|
1479
|
-
*
|
|
1480
|
-
*
|
|
1481
|
-
*
|
|
1482
|
-
*
|
|
2195
|
+
* attributes == {resource.host.name: v}
|
|
2196
|
+
* -> MetricItemAggMV1mByHostV2, hostEntityKey = keyForHost(projectId, v)
|
|
2197
|
+
* attributes == {resource.k8s.cluster.name: v}
|
|
2198
|
+
* -> MetricItemAggMV1mByK8sCluster, k8sClusterEntityKey = keyForKubernetesCluster(projectId, v)
|
|
2199
|
+
* attributes == {resource.container.id: v}
|
|
2200
|
+
* -> MetricItemAggMV1mByContainer, containerEntityKey = keyForContainer(projectId, v)
|
|
2201
|
+
* attributes == {resource.service.name: v}
|
|
2202
|
+
* -> MetricItemAggMV1mByService, serviceEntityKey IN (<registry key set>)
|
|
2203
|
+
* The key set is resolved asynchronously in aggregateBy() from
|
|
2204
|
+
* the Postgres TelemetryEntity registry — service identity folds
|
|
2205
|
+
* service.namespace into the key at ingest, so one name can map
|
|
2206
|
+
* to several keys. No registry rows -> NO routing (raw path).
|
|
2207
|
+
* entityScope only, attributeKey in {host/k8s-cluster/container} and
|
|
2208
|
+
* entityKeys verified byte-equal to [keyFor<type>(projectId, attributeValue)]
|
|
2209
|
+
* -> same MV as the matching attribute shape. The scope's attribute
|
|
2210
|
+
* OR-fallback only adds rows ingested before the entity-key
|
|
2211
|
+
* columns existed — the same retention-bounded gap the original
|
|
2212
|
+
* per-host path accepted.
|
|
2213
|
+
* attributes(single recognized key) + entityScope agreeing on
|
|
2214
|
+
* attributeKey/attributeValue (the host/k8s Metrics-tab sparkline
|
|
2215
|
+
* shape; the scope is then redundant — A AND (B OR A) === A)
|
|
2216
|
+
* -> same MV as the attribute shape (service excluded).
|
|
2217
|
+
*
|
|
2218
|
+
* Everything else falls through (minute MV, then raw): service +
|
|
2219
|
+
* entityScope (a bare-key translation would drop namespaced variants),
|
|
2220
|
+
* unverifiable or multi-key scopes, extra attribute filters, non-string
|
|
2221
|
+
* values, k8s pod/node identities (composite cluster(+namespace)+uid —
|
|
2222
|
+
* not derivable from a single attribute), and any other query column.
|
|
2223
|
+
* When in doubt this method returns null; the raw path is always
|
|
2224
|
+
* correct.
|
|
1483
2225
|
*
|
|
1484
|
-
*
|
|
1485
|
-
*
|
|
1486
|
-
* shape (columns: aggregateColumn, timestampColumn) matches
|
|
1487
|
-
* the base statement so downstream code needs no changes.
|
|
2226
|
+
* The result row shape (columns: aggregateColumn, timestampColumn)
|
|
2227
|
+
* matches the base statement so downstream code needs no changes.
|
|
1488
2228
|
*/
|
|
1489
|
-
private
|
|
2229
|
+
private tryBuildEntityAggregateMVStatement(
|
|
1490
2230
|
aggregateBy: AggregateBy<Metric>,
|
|
1491
2231
|
): { statement: Statement; columns: Array<string> } | null {
|
|
1492
2232
|
const aggType: AggregationType = aggregateBy.aggregationType;
|
|
@@ -1519,80 +2259,51 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
1519
2259
|
return null;
|
|
1520
2260
|
}
|
|
1521
2261
|
|
|
1522
|
-
/*
|
|
1523
|
-
* Inspect the attribute filter. This MV is only safe when
|
|
1524
|
-
* the user is filtering by exactly one attribute,
|
|
1525
|
-
* `resource.host.name`, with a bare-string value (the
|
|
1526
|
-
* canonical Overview/Metrics-page pattern). Anything else —
|
|
1527
|
-
* extra attribute filters, NotEqual, Search, etc. — has to
|
|
1528
|
-
* fall back so the result stays correct.
|
|
1529
|
-
*/
|
|
1530
2262
|
const queryRecord: Record<string, unknown> =
|
|
1531
2263
|
(aggregateBy.query as unknown as Record<string, unknown>) || {};
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
2264
|
+
|
|
2265
|
+
/*
|
|
2266
|
+
* The entity keys fold the tenant in (sha256(projectId|type|...)), so
|
|
2267
|
+
* MV rows can only be located when the query is project-scoped.
|
|
2268
|
+
* Dashboard reads always are; anything else falls back safely.
|
|
2269
|
+
*/
|
|
2270
|
+
const projectIdValue: unknown = queryRecord["projectId"];
|
|
2271
|
+
let projectId: string = "";
|
|
2272
|
+
if (projectIdValue instanceof ObjectID) {
|
|
2273
|
+
projectId = projectIdValue.toString();
|
|
2274
|
+
} else if (typeof projectIdValue === "string") {
|
|
2275
|
+
projectId = projectIdValue;
|
|
1541
2276
|
}
|
|
1542
|
-
|
|
1543
|
-
if (attrKey !== "resource.host.name") {
|
|
2277
|
+
if (!projectId) {
|
|
1544
2278
|
return null;
|
|
1545
2279
|
}
|
|
1546
|
-
|
|
2280
|
+
|
|
2281
|
+
const resolved: { route: EntityMVRoute; keys: Array<string> } | null =
|
|
2282
|
+
this.resolveEntityMVRouteAndKeys(aggregateBy, queryRecord, projectId);
|
|
2283
|
+
if (!resolved) {
|
|
1547
2284
|
return null;
|
|
1548
2285
|
}
|
|
1549
2286
|
|
|
1550
2287
|
/*
|
|
1551
|
-
*
|
|
1552
|
-
* other query key (primaryEntityId,
|
|
1553
|
-
*
|
|
1554
|
-
*
|
|
1555
|
-
*
|
|
2288
|
+
* Each MV only carries projectId/name/<entity key>/bucketTime. Any
|
|
2289
|
+
* other query key (primaryEntityId, entityKeys, ...) would compile to
|
|
2290
|
+
* a WHERE over a column the MV does not have, so fall back to the raw
|
|
2291
|
+
* table for those. Mirrors tryBuildMinuteAggregateMVStatement.
|
|
2292
|
+
* `attributes` and `entityScope` are validated and rewritten into the
|
|
2293
|
+
* entity-key predicate by resolveEntityMVRouteAndKeys above.
|
|
1556
2294
|
*/
|
|
1557
2295
|
const mvQueryableColumns: ReadonlyArray<string> = [
|
|
1558
2296
|
"projectId",
|
|
1559
2297
|
"name",
|
|
1560
2298
|
"time", // stripped below; bucketTime range is added explicitly
|
|
1561
|
-
"attributes",
|
|
2299
|
+
"attributes",
|
|
2300
|
+
"entityScope",
|
|
1562
2301
|
];
|
|
1563
2302
|
for (const queryKey of Object.keys(queryRecord)) {
|
|
1564
2303
|
if (!mvQueryableColumns.includes(queryKey)) {
|
|
1565
2304
|
return null;
|
|
1566
2305
|
}
|
|
1567
2306
|
}
|
|
1568
|
-
const hostIdentifier: string =
|
|
1569
|
-
typeof attrValue === "string" ? attrValue : "";
|
|
1570
|
-
if (!hostIdentifier) {
|
|
1571
|
-
return null;
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
/*
|
|
1575
|
-
* The entity key folds the tenant in (sha256(projectId|host|...)), so
|
|
1576
|
-
* the MV row can only be located when the query is project-scoped.
|
|
1577
|
-
* Dashboard reads always are; anything else falls back safely.
|
|
1578
|
-
*/
|
|
1579
|
-
const projectIdValue: unknown = queryRecord["projectId"];
|
|
1580
|
-
let projectId: string = "";
|
|
1581
|
-
if (projectIdValue instanceof ObjectID) {
|
|
1582
|
-
projectId = projectIdValue.toString();
|
|
1583
|
-
} else if (typeof projectIdValue === "string") {
|
|
1584
|
-
projectId = projectIdValue;
|
|
1585
|
-
}
|
|
1586
|
-
if (!projectId) {
|
|
1587
|
-
return null;
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
/*
|
|
1591
|
-
* Same canonicalized key the ingest pipeline stamps into
|
|
1592
|
-
* MetricItemV3.hostEntityKey (and the V2 MV groups by) — byte-equality
|
|
1593
|
-
* is what makes this lookup correct, see Common/Utils/Telemetry/EntityKey.
|
|
1594
|
-
*/
|
|
1595
|
-
const hostEntityKey: string = keyForHost(projectId, hostIdentifier);
|
|
1596
2307
|
|
|
1597
2308
|
const interval: AggregationInterval = AggregateUtil.getAggregationInterval({
|
|
1598
2309
|
startDate: aggregateBy.startTimestamp!,
|
|
@@ -1600,10 +2311,10 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
1600
2311
|
aggregationInterval: aggregateBy.aggregationInterval,
|
|
1601
2312
|
});
|
|
1602
2313
|
/*
|
|
1603
|
-
* `Total` (whole-window, no bucketing) is not served by
|
|
1604
|
-
* time-bucketed per-
|
|
1605
|
-
* Every other interval is >= the
|
|
1606
|
-
* into the
|
|
2314
|
+
* `Total` (whole-window, no bucketing) is not served by these
|
|
2315
|
+
* time-bucketed per-entity MVs — fall back to the raw-table builder.
|
|
2316
|
+
* Every other interval is >= the MVs' 1-minute resolution and flows
|
|
2317
|
+
* into the shared bucket expression below.
|
|
1607
2318
|
*/
|
|
1608
2319
|
if (AggregateUtil.isTotalAggregation(interval)) {
|
|
1609
2320
|
return null;
|
|
@@ -1614,8 +2325,6 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
1614
2325
|
}
|
|
1615
2326
|
const databaseName: string = this.database.getDatasourceOptions().database!;
|
|
1616
2327
|
|
|
1617
|
-
const intervalLower: string = interval.toLowerCase();
|
|
1618
|
-
|
|
1619
2328
|
let mergedExpr: string;
|
|
1620
2329
|
if (aggType === AggregationType.Sum) {
|
|
1621
2330
|
mergedExpr = `sumMerge(valueSumState)`;
|
|
@@ -1630,13 +2339,13 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
1630
2339
|
}
|
|
1631
2340
|
|
|
1632
2341
|
/*
|
|
1633
|
-
* Strip
|
|
1634
|
-
*
|
|
1635
|
-
*
|
|
1636
|
-
*
|
|
2342
|
+
* Strip `time` (column doesn't exist on the MV; we inject an
|
|
2343
|
+
* explicit bucketTime range below) plus `attributes`/`entityScope`
|
|
2344
|
+
* (the entity filter is now an explicit predicate against the MV's
|
|
2345
|
+
* entity-key column).
|
|
1637
2346
|
*/
|
|
1638
2347
|
const filteredQuery: typeof aggregateBy.query =
|
|
1639
|
-
this.
|
|
2348
|
+
this.stripEntityFilterAndTimeFromQuery(
|
|
1640
2349
|
aggregateBy.query,
|
|
1641
2350
|
) as typeof aggregateBy.query;
|
|
1642
2351
|
const nonTimeWhere: Statement =
|
|
@@ -1648,18 +2357,29 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
1648
2357
|
const statement: Statement = SQL``;
|
|
1649
2358
|
|
|
1650
2359
|
statement.append(
|
|
1651
|
-
`SELECT ${mergedExpr} as value,
|
|
2360
|
+
`SELECT ${mergedExpr} as value, ${AggregateUtil.buildBucketTimestampExpression(interval, "bucketTime")} as time`,
|
|
1652
2361
|
);
|
|
1653
|
-
statement.append(SQL` FROM ${databaseName}
|
|
2362
|
+
statement.append(SQL` FROM ${databaseName}.`);
|
|
2363
|
+
statement.append(resolved.route.tableName);
|
|
1654
2364
|
statement.append(
|
|
1655
2365
|
` WHERE bucketTime >= toDateTime('${this.formatDateTime(aggregateBy.startTimestamp!)}') AND bucketTime <= toDateTime('${this.formatDateTime(aggregateBy.endTimestamp!)}')${this.getRetentionReadFilter()}`,
|
|
1656
2366
|
);
|
|
1657
|
-
statement.append(
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
2367
|
+
statement.append(` AND ${resolved.route.keyColumn}`);
|
|
2368
|
+
if (resolved.keys.length === 1) {
|
|
2369
|
+
statement.append(
|
|
2370
|
+
SQL` = ${{
|
|
2371
|
+
value: resolved.keys[0]!,
|
|
2372
|
+
type: TableColumnType.Text,
|
|
2373
|
+
}}`,
|
|
2374
|
+
);
|
|
2375
|
+
} else {
|
|
2376
|
+
statement.append(
|
|
2377
|
+
SQL` IN ${{
|
|
2378
|
+
value: resolved.keys,
|
|
2379
|
+
type: TableColumnType.ArrayText,
|
|
2380
|
+
}}`,
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
1663
2383
|
statement.append(SQL` `).append(nonTimeWhere);
|
|
1664
2384
|
|
|
1665
2385
|
statement.append(SQL` GROUP BY `).append(`time`);
|
|
@@ -1678,6 +2398,8 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
1678
2398
|
);
|
|
1679
2399
|
statement.append(
|
|
1680
2400
|
getQuerySettings({
|
|
2401
|
+
maxExecutionTimeInSeconds: 45,
|
|
2402
|
+
timeoutOverflowMode: this.getTimeoutOverflowMode(aggregateBy),
|
|
1681
2403
|
additionalSettings: {
|
|
1682
2404
|
optimize_aggregation_in_order: 1,
|
|
1683
2405
|
optimize_move_to_prewhere: 1,
|
|
@@ -1686,7 +2408,7 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
1686
2408
|
}),
|
|
1687
2409
|
);
|
|
1688
2410
|
|
|
1689
|
-
logger.debug(`${this.model.tableName}
|
|
2411
|
+
logger.debug(`${this.model.tableName} Entity MV Aggregate Statement`, {
|
|
1690
2412
|
tableName: this.model.tableName,
|
|
1691
2413
|
} as LogAttributes);
|
|
1692
2414
|
logger.debug(statement, {
|
|
@@ -1702,13 +2424,139 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
1702
2424
|
};
|
|
1703
2425
|
}
|
|
1704
2426
|
|
|
1705
|
-
|
|
2427
|
+
/**
|
|
2428
|
+
* The single entity filter of a query, resolved to the MV route that
|
|
2429
|
+
* serves it and the exact entity-key set to filter by — or null when
|
|
2430
|
+
* no branch of the routing decision table (see
|
|
2431
|
+
* tryBuildEntityAggregateMVStatement) matches. Never guesses: every
|
|
2432
|
+
* shape it accepts is one whose MV predicate provably covers the raw
|
|
2433
|
+
* predicate it replaces (modulo the accepted pre-entity-key-column
|
|
2434
|
+
* rows).
|
|
2435
|
+
*/
|
|
2436
|
+
private resolveEntityMVRouteAndKeys(
|
|
2437
|
+
aggregateBy: AggregateBy<Metric>,
|
|
2438
|
+
queryRecord: Record<string, unknown>,
|
|
2439
|
+
projectId: string,
|
|
2440
|
+
): { route: EntityMVRoute; keys: Array<string> } | null {
|
|
2441
|
+
const attrs: unknown = queryRecord["attributes"];
|
|
2442
|
+
if (attrs !== undefined && attrs !== null && typeof attrs !== "object") {
|
|
2443
|
+
return null;
|
|
2444
|
+
}
|
|
2445
|
+
const attrEntries: Array<[string, unknown]> =
|
|
2446
|
+
attrs && typeof attrs === "object"
|
|
2447
|
+
? Object.entries(attrs as Record<string, unknown>)
|
|
2448
|
+
: [];
|
|
2449
|
+
if (attrEntries.length > 1) {
|
|
2450
|
+
return null;
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
const scopeValue: unknown = queryRecord["entityScope"];
|
|
2454
|
+
const scope: EntityScopeQueryValue | null =
|
|
2455
|
+
scopeValue !== undefined &&
|
|
2456
|
+
scopeValue !== null &&
|
|
2457
|
+
typeof scopeValue === "object"
|
|
2458
|
+
? (scopeValue as EntityScopeQueryValue)
|
|
2459
|
+
: null;
|
|
2460
|
+
// A present-but-malformed entityScope is a filter we cannot honor.
|
|
2461
|
+
if (scopeValue !== undefined && scopeValue !== null && !scope) {
|
|
2462
|
+
return null;
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
if (attrEntries.length === 1) {
|
|
2466
|
+
const [attrKey, attrValue] = attrEntries[0]!;
|
|
2467
|
+
const route: EntityMVRoute | undefined = ENTITY_MV_ROUTES.find(
|
|
2468
|
+
(candidate: EntityMVRoute) => {
|
|
2469
|
+
return candidate.attributeKey === attrKey;
|
|
2470
|
+
},
|
|
2471
|
+
);
|
|
2472
|
+
if (!route || typeof attrValue !== "string" || !attrValue) {
|
|
2473
|
+
return null;
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
if (scope) {
|
|
2477
|
+
/*
|
|
2478
|
+
* The host/k8s Metrics tabs send the attribute filter AND a
|
|
2479
|
+
* matching entityScope in the same query. When the scope is
|
|
2480
|
+
* provably the SAME entity filter (same attributeKey/value), the
|
|
2481
|
+
* conjunction reduces to the attribute equality — the scope's
|
|
2482
|
+
* OR-fallback is subsumed — so route on the attribute alone. Any
|
|
2483
|
+
* disagreement means a genuine second filter: fall back. Service
|
|
2484
|
+
* is excluded outright: its scope keys cannot be verified against
|
|
2485
|
+
* a computed key (namespace variants).
|
|
2486
|
+
*/
|
|
2487
|
+
if (route.keyForValue === null) {
|
|
2488
|
+
return null;
|
|
2489
|
+
}
|
|
2490
|
+
if (
|
|
2491
|
+
scope.attributeKey !== attrKey ||
|
|
2492
|
+
String(scope.attributeValue ?? "") !== attrValue
|
|
2493
|
+
) {
|
|
2494
|
+
return null;
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
if (route.keyForValue === null) {
|
|
2499
|
+
/*
|
|
2500
|
+
* Service: the registry-resolved key set was handed over by
|
|
2501
|
+
* aggregateBy(). Absent or empty (sync caller, registry miss,
|
|
2502
|
+
* lookup failure) -> raw path.
|
|
2503
|
+
*/
|
|
2504
|
+
const hintKeys: Array<string> | undefined =
|
|
2505
|
+
this.serviceEntityKeysHintByAggregate.get(aggregateBy);
|
|
2506
|
+
if (!hintKeys || hintKeys.length === 0) {
|
|
2507
|
+
return null;
|
|
2508
|
+
}
|
|
2509
|
+
return { route, keys: hintKeys };
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
return { route, keys: [route.keyForValue(projectId, attrValue)] };
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
// No attribute filter: entityScope-only shape.
|
|
2516
|
+
if (!scope) {
|
|
2517
|
+
return null;
|
|
2518
|
+
}
|
|
2519
|
+
const route: EntityMVRoute | undefined = ENTITY_MV_ROUTES.find(
|
|
2520
|
+
(candidate: EntityMVRoute) => {
|
|
2521
|
+
return candidate.attributeKey === scope.attributeKey;
|
|
2522
|
+
},
|
|
2523
|
+
);
|
|
2524
|
+
if (!route || route.keyForValue === null) {
|
|
2525
|
+
return null;
|
|
2526
|
+
}
|
|
2527
|
+
const scopeAttributeValue: unknown = scope.attributeValue;
|
|
2528
|
+
if (typeof scopeAttributeValue !== "string" || !scopeAttributeValue) {
|
|
2529
|
+
return null;
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
/*
|
|
2533
|
+
* `hasAny(entityKeys, [...])` is only translatable to the scalar
|
|
2534
|
+
* entity-key column when every listed key IS the key this scope's
|
|
2535
|
+
* attribute value derives to — recompute it server-side and require
|
|
2536
|
+
* byte equality (a foreign or extra key would make the raw predicate
|
|
2537
|
+
* match rows the MV predicate cannot).
|
|
2538
|
+
*/
|
|
2539
|
+
const expectedKey: string = route.keyForValue(
|
|
2540
|
+
projectId,
|
|
2541
|
+
scopeAttributeValue,
|
|
2542
|
+
);
|
|
2543
|
+
const scopeKeys: Array<string> = Array.isArray(scope.entityKeys)
|
|
2544
|
+
? scope.entityKeys
|
|
2545
|
+
: [];
|
|
2546
|
+
if (scopeKeys.length !== 1 || scopeKeys[0] !== expectedKey) {
|
|
2547
|
+
return null;
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
return { route, keys: [expectedKey] };
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
private stripEntityFilterAndTimeFromQuery(query: unknown): typeof query {
|
|
1706
2554
|
if (!query || typeof query !== "object") {
|
|
1707
2555
|
return query;
|
|
1708
2556
|
}
|
|
1709
2557
|
const out: Record<string, unknown> = {};
|
|
1710
2558
|
for (const [k, v] of Object.entries(query as Record<string, unknown>)) {
|
|
1711
|
-
if (k === "time" || k === "attributes") {
|
|
2559
|
+
if (k === "time" || k === "attributes" || k === "entityScope") {
|
|
1712
2560
|
continue;
|
|
1713
2561
|
}
|
|
1714
2562
|
out[k] = v;
|