@oneuptime/common 11.3.28 → 11.4.1
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/DatabaseModels/AIRun.ts +50 -0
- package/Models/DatabaseModels/Project.ts +60 -0
- package/Server/API/AIAgentTaskAPI.ts +10 -24
- package/Server/API/AIInvestigationAPI.ts +148 -0
- package/Server/API/TelemetryAPI.ts +4 -3
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783598145111-AddEnableAutomaticIncidentInvestigationToProject.ts +19 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783615884106-AddInvestigationSubjectToAIRun.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783632447975-AddEnableAutomaticAlertInvestigationToProject.ts +19 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Services/AIAgentTaskService.ts +62 -0
- package/Server/Services/AlertService.ts +25 -0
- package/Server/Services/AnalyticsDatabaseService.ts +27 -0
- package/Server/Services/IncidentService.ts +25 -0
- package/Server/Services/IncidentStateTimelineService.ts +19 -0
- package/Server/Services/LogAggregationService.ts +29 -11
- package/Server/Services/MetricService.ts +603 -13
- package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +119 -11
- package/Server/Utils/AI/Sentinel/AlertInvestigationRunner.ts +141 -0
- package/Server/Utils/AI/Sentinel/IncidentInvestigationRunner.ts +178 -0
- package/Server/Utils/AI/Sentinel/IncidentPostmortemRunner.ts +105 -0
- package/Server/Utils/AI/Sentinel/README.md +10 -0
- package/Server/Utils/AI/Sentinel/SentinelInvestigationEngine.ts +405 -0
- package/Server/Utils/AI/Sentinel/SentinelMemory.ts +167 -0
- package/Server/Utils/AI/Toolbox/Index.ts +11 -0
- package/Server/Utils/AI/Toolbox/SentinelActionTools.ts +577 -0
- package/Server/Utils/Monitor/Criteria/APIRequestCriteria.ts +44 -0
- package/Server/Utils/Monitor/MonitorMetricUtil.ts +108 -2
- package/Tests/Server/API/BaseAPI.test.ts +1 -0
- package/Tests/Server/Services/AIAgentTaskServiceClaim.test.ts +146 -0
- package/Tests/Server/Services/LogAggregationService.test.ts +22 -0
- package/Tests/Server/Services/MetricServiceAggregate.test.ts +194 -0
- package/Tests/Types/AI/AIAgentTaskStatus.test.ts +86 -0
- package/Tests/Types/BaseDatabase/AggregationIntervalUtil.test.ts +127 -0
- package/Tests/Types/BaseDatabase/DatabaseCommonInteractionPropsUtil.test.ts +152 -0
- package/Tests/Types/OnCallDutyPolicy/ScheduleShiftUtil.test.ts +260 -0
- package/Tests/Types/Trace/TraceRecordingRuleDefinition.test.ts +308 -0
- package/Tests/UI/Components/Dropdown.test.tsx +17 -0
- package/Tests/UI/Components/EntityDropdown.test.tsx +130 -0
- package/Tests/UI/Components/Modal.test.tsx +144 -2
- package/Tests/UI/Components/TelemetrySearchBar.test.tsx +90 -0
- package/Tests/UI/Utils/Theme.test.ts +161 -0
- package/Types/BaseDatabase/AggregateBy.ts +12 -0
- package/Types/Dashboard/DashboardComponentType.ts +1 -0
- package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
- package/Types/Dashboard/DashboardComponents/DashboardLogChartComponent.ts +19 -0
- package/Types/Dashboard/DashboardTemplates.ts +9 -1
- package/Types/Date.ts +46 -0
- package/Types/Monitor/CriteriaFilter.ts +4 -0
- package/Types/Monitor/HttpPhaseTimings.ts +16 -0
- package/Types/Monitor/MonitorMetricType.ts +17 -0
- package/Types/Monitor/PingMonitor/PingMonitorResponse.ts +20 -0
- package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +257 -0
- package/Types/Probe/ProbeMonitorResponse.ts +14 -0
- package/UI/Components/Calendar/Calendar.tsx +8 -2
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +5 -5
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +8 -5
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +4 -1
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +6 -3
- package/UI/Components/CodeBlock/CodeBlock.tsx +2 -1
- package/UI/Components/CodeEditor/CodeEditor.tsx +3 -0
- package/UI/Components/Divider/Divider.tsx +1 -1
- package/UI/Components/Dropdown/Dropdown.tsx +13 -4
- package/UI/Components/EntityDropdown/EntityDropdown.tsx +110 -2
- package/UI/Components/ErrorBoundary.tsx +2 -1
- package/UI/Components/EventItem/EventItem.tsx +2 -2
- package/UI/Components/GanttChart/Bar/Index.tsx +1 -1
- package/UI/Components/Graphs/UptimeBarTooltip.tsx +30 -23
- package/UI/Components/Header/Header.tsx +7 -0
- package/UI/Components/Header/HeaderIconDropdownButton.tsx +2 -0
- package/UI/Components/Label/Label.tsx +2 -2
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +22 -10
- package/UI/Components/LogsViewer/components/LogsHistogram.tsx +9 -3
- package/UI/Components/Markdown.tsx/MarkdownEditor.tsx +8 -8
- package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +44 -20
- package/UI/Components/Modal/ConfirmModal.tsx +1 -1
- package/UI/Components/Modal/Modal.tsx +213 -121
- package/UI/Components/Modal/ModalFooter.tsx +33 -22
- package/UI/Components/ModelTable/TableView.tsx +1 -1
- package/UI/Components/MonitorGraphs/UptimeBarDayModal.tsx +20 -12
- package/UI/Components/Pill/Pill.tsx +21 -13
- package/UI/Components/StatusBubble/StatusBubble.tsx +3 -3
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +7 -1
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +9 -3
- package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +62 -4
- package/UI/Components/Workflow/Component.tsx +53 -33
- package/UI/Components/Workflow/ComponentPortViewer.tsx +6 -6
- package/UI/Components/Workflow/ComponentReturnValueViewer.tsx +8 -8
- package/UI/Components/Workflow/ComponentsModal.tsx +16 -9
- package/UI/Components/Workflow/Workflow.tsx +13 -11
- package/UI/Utils/Theme.ts +140 -0
- package/Utils/Dashboard/Components/DashboardLogChartComponent.ts +104 -0
- package/Utils/Dashboard/Components/Index.ts +7 -0
- package/Utils/Monitor/MonitorMetricType.ts +70 -2
- package/build/dist/Models/DatabaseModels/AIRun.js +52 -0
- package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +62 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Server/API/AIAgentTaskAPI.js +8 -23
- package/build/dist/Server/API/AIAgentTaskAPI.js.map +1 -1
- package/build/dist/Server/API/AIInvestigationAPI.js +107 -0
- package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -0
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783598145111-AddEnableAutomaticIncidentInvestigationToProject.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783598145111-AddEnableAutomaticIncidentInvestigationToProject.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783615884106-AddInvestigationSubjectToAIRun.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783615884106-AddInvestigationSubjectToAIRun.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783632447975-AddEnableAutomaticAlertInvestigationToProject.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783632447975-AddEnableAutomaticAlertInvestigationToProject.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AIAgentTaskService.js +59 -0
- package/build/dist/Server/Services/AIAgentTaskService.js.map +1 -1
- package/build/dist/Server/Services/AlertService.js +24 -0
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +19 -0
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +24 -0
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +21 -7
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/MetricService.js +449 -7
- package/build/dist/Server/Services/MetricService.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +59 -11
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
- package/build/dist/Server/Utils/AI/Sentinel/AlertInvestigationRunner.js +119 -0
- package/build/dist/Server/Utils/AI/Sentinel/AlertInvestigationRunner.js.map +1 -0
- package/build/dist/Server/Utils/AI/Sentinel/IncidentInvestigationRunner.js +147 -0
- package/build/dist/Server/Utils/AI/Sentinel/IncidentInvestigationRunner.js.map +1 -0
- package/build/dist/Server/Utils/AI/Sentinel/IncidentPostmortemRunner.js +93 -0
- package/build/dist/Server/Utils/AI/Sentinel/IncidentPostmortemRunner.js.map +1 -0
- package/build/dist/Server/Utils/AI/Sentinel/SentinelInvestigationEngine.js +311 -0
- package/build/dist/Server/Utils/AI/Sentinel/SentinelInvestigationEngine.js.map +1 -0
- package/build/dist/Server/Utils/AI/Sentinel/SentinelMemory.js +143 -0
- package/build/dist/Server/Utils/AI/Sentinel/SentinelMemory.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js +6 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/SentinelActionTools.js +451 -0
- package/build/dist/Server/Utils/AI/Toolbox/SentinelActionTools.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js +29 -0
- package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js +86 -2
- package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardLogChartComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardLogChartComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardTemplates.js +9 -1
- package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
- package/build/dist/Types/Date.js +32 -0
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Monitor/CriteriaFilter.js +4 -0
- package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
- package/build/dist/Types/Monitor/HttpPhaseTimings.js +2 -0
- package/build/dist/Types/Monitor/HttpPhaseTimings.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorMetricType.js +15 -0
- package/build/dist/Types/Monitor/MonitorMetricType.js.map +1 -1
- package/build/dist/Types/Monitor/PingMonitor/PingMonitorResponse.js +2 -0
- package/build/dist/Types/Monitor/PingMonitor/PingMonitorResponse.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +159 -0
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -0
- package/build/dist/UI/Components/Calendar/Calendar.js +2 -2
- package/build/dist/UI/Components/Calendar/Calendar.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +2 -2
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +7 -4
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +4 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +6 -3
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/CodeBlock/CodeBlock.js +1 -1
- package/build/dist/UI/Components/CodeBlock/CodeBlock.js.map +1 -1
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js +3 -1
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
- package/build/dist/UI/Components/Divider/Divider.js +1 -1
- package/build/dist/UI/Components/Divider/Divider.js.map +1 -1
- package/build/dist/UI/Components/Dropdown/Dropdown.js +10 -4
- package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +76 -3
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
- package/build/dist/UI/Components/ErrorBoundary.js +2 -1
- package/build/dist/UI/Components/ErrorBoundary.js.map +1 -1
- package/build/dist/UI/Components/EventItem/EventItem.js +2 -2
- package/build/dist/UI/Components/EventItem/EventItem.js.map +1 -1
- package/build/dist/UI/Components/GanttChart/Bar/Index.js +1 -1
- package/build/dist/UI/Components/GanttChart/Bar/Index.js.map +1 -1
- package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js +29 -23
- package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js.map +1 -1
- package/build/dist/UI/Components/Header/Header.js +1 -0
- package/build/dist/UI/Components/Header/Header.js.map +1 -1
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +1 -1
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
- package/build/dist/UI/Components/Label/Label.js +2 -2
- package/build/dist/UI/Components/Label/Label.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +20 -8
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +8 -2
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js +8 -8
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +40 -20
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/Modal/ConfirmModal.js +1 -1
- package/build/dist/UI/Components/Modal/ConfirmModal.js.map +1 -1
- package/build/dist/UI/Components/Modal/Modal.js +109 -51
- package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
- package/build/dist/UI/Components/Modal/ModalFooter.js +13 -10
- package/build/dist/UI/Components/Modal/ModalFooter.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/TableView.js +1 -1
- package/build/dist/UI/Components/ModelTable/TableView.js.map +1 -1
- package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js +17 -12
- package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js.map +1 -1
- package/build/dist/UI/Components/Pill/Pill.js +12 -13
- package/build/dist/UI/Components/Pill/Pill.js.map +1 -1
- package/build/dist/UI/Components/StatusBubble/StatusBubble.js +3 -3
- package/build/dist/UI/Components/StatusBubble/StatusBubble.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +8 -2
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +36 -6
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -1
- package/build/dist/UI/Components/Workflow/Component.js +43 -33
- package/build/dist/UI/Components/Workflow/Component.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentPortViewer.js +6 -6
- package/build/dist/UI/Components/Workflow/ComponentPortViewer.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentReturnValueViewer.js +8 -8
- package/build/dist/UI/Components/Workflow/ComponentReturnValueViewer.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentsModal.js +15 -9
- package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
- package/build/dist/UI/Components/Workflow/Workflow.js +13 -11
- package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
- package/build/dist/UI/Utils/Theme.js +106 -0
- package/build/dist/UI/Utils/Theme.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardLogChartComponent.js +92 -0
- package/build/dist/Utils/Dashboard/Components/DashboardLogChartComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
- package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
- package/build/dist/Utils/Monitor/MonitorMetricType.js +70 -3
- package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ClickhouseDatabase from "../Infrastructure/ClickhouseDatabase";
|
|
2
|
+
import { ResponseJSON, ResultSet } from "@clickhouse/client";
|
|
2
3
|
import AnalyticsDatabaseService from "./AnalyticsDatabaseService";
|
|
3
4
|
import { MutableMetricService as MutableMetricServiceClass } from "./MutableMetricService";
|
|
4
5
|
import Metric from "../../Models/AnalyticsModels/Metric";
|
|
@@ -14,17 +15,327 @@ import AggregationType, {
|
|
|
14
15
|
isPercentileAggregation,
|
|
15
16
|
} from "../../Types/BaseDatabase/AggregationType";
|
|
16
17
|
import AggregationInterval from "../../Types/BaseDatabase/AggregationInterval";
|
|
18
|
+
import AggregatedResult from "../../Types/BaseDatabase/AggregatedResult";
|
|
17
19
|
import AnalyticsTableName from "../../Types/AnalyticsDatabase/AnalyticsTableName";
|
|
18
20
|
import TableColumnType from "../../Types/AnalyticsDatabase/TableColumnType";
|
|
21
|
+
import BadDataException from "../../Types/Exception/BadDataException";
|
|
22
|
+
import { JSONObject } from "../../Types/JSON";
|
|
19
23
|
import { keyForHost } from "../../Utils/Telemetry/EntityKey";
|
|
20
24
|
import ObjectID from "../../Types/ObjectID";
|
|
21
25
|
import logger, { LogAttributes } from "../Utils/Logger";
|
|
22
26
|
|
|
27
|
+
/*
|
|
28
|
+
* Metric point types whose rows carry a pre-aggregated distribution
|
|
29
|
+
* (count/sum/min/max + buckets) instead of a single observation. For
|
|
30
|
+
* these rows the `value` column holds the datapoint's SUM of all
|
|
31
|
+
* observations in the export interval (see OtelMetricsIngestService's
|
|
32
|
+
* buildMetricRow: value = asInt ?? asDouble ?? sum), so aggregating
|
|
33
|
+
* `value` directly produces sum-scale numbers — e.g. "Avg of
|
|
34
|
+
* http.client.request.duration" would return ~110s for a service whose
|
|
35
|
+
* true mean latency is 0.586s at ~188 requests per export interval.
|
|
36
|
+
*/
|
|
37
|
+
const DISTRIBUTION_POINT_TYPES: ReadonlyArray<string> = [
|
|
38
|
+
"Histogram",
|
|
39
|
+
"ExponentialHistogram",
|
|
40
|
+
"Summary",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const MAX_GROUP_BY_ATTRIBUTE_KEYS: number = 10;
|
|
44
|
+
const MAX_GROUP_BY_ATTRIBUTE_KEY_LENGTH: number = 256;
|
|
45
|
+
|
|
46
|
+
const POINT_TYPE_CACHE_TTL_MS: number = 10 * 60 * 1000;
|
|
47
|
+
const POINT_TYPE_CACHE_MAX_ENTRIES: number = 5000;
|
|
48
|
+
|
|
23
49
|
export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
50
|
+
/*
|
|
51
|
+
* (projectId|metricName) -> metricPointType, resolved lazily on the
|
|
52
|
+
* aggregate path so scalar aggregations of distribution metrics can
|
|
53
|
+
* skip the MVs (whose states collapse `value` — the histogram sum)
|
|
54
|
+
* and use the count-weighted expressions instead. Bounded + TTL'd so
|
|
55
|
+
* a tenant churning metric names can't grow it without limit.
|
|
56
|
+
*/
|
|
57
|
+
private metricPointTypeCache: Map<
|
|
58
|
+
string,
|
|
59
|
+
{ pointType: string | null; expiresAt: number }
|
|
60
|
+
> = new Map();
|
|
61
|
+
|
|
62
|
+
/*
|
|
63
|
+
* Point-type hint per aggregateBy call. toAggregateStatement is a
|
|
64
|
+
* synchronous override, so the async lookup happens in aggregateBy
|
|
65
|
+
* and is handed over keyed on the (shared) aggregateBy object.
|
|
66
|
+
*/
|
|
67
|
+
private pointTypeHintByAggregate: WeakMap<
|
|
68
|
+
AggregateBy<Metric>,
|
|
69
|
+
string | null
|
|
70
|
+
> = new WeakMap();
|
|
71
|
+
|
|
72
|
+
private inFlightPointTypeLookups: Map<string, Promise<string | null>> =
|
|
73
|
+
new Map();
|
|
74
|
+
|
|
24
75
|
public constructor(clickhouseDatabase?: ClickhouseDatabase | undefined) {
|
|
25
76
|
super({ modelType: Metric, database: clickhouseDatabase });
|
|
26
77
|
}
|
|
27
78
|
|
|
79
|
+
public override async aggregateBy(
|
|
80
|
+
aggregateBy: AggregateBy<Metric>,
|
|
81
|
+
): Promise<AggregatedResult> {
|
|
82
|
+
/*
|
|
83
|
+
* Only scalar aggregations over `value` need the point type — the
|
|
84
|
+
* percentile path is already distribution-aware via the bucket
|
|
85
|
+
* fanout, and MV routing is the only decision the hint drives.
|
|
86
|
+
* Grouped queries (model-column or attribute-key) never route to
|
|
87
|
+
* the MVs, so the lookup would be dead weight there.
|
|
88
|
+
*/
|
|
89
|
+
if (
|
|
90
|
+
!isPercentileAggregation(aggregateBy.aggregationType) &&
|
|
91
|
+
aggregateBy.aggregateColumnName.toString() === "value" &&
|
|
92
|
+
(!aggregateBy.groupBy || Object.keys(aggregateBy.groupBy).length === 0) &&
|
|
93
|
+
(!aggregateBy.groupByAttributeKeys ||
|
|
94
|
+
aggregateBy.groupByAttributeKeys.length === 0)
|
|
95
|
+
) {
|
|
96
|
+
const pointType: string | null =
|
|
97
|
+
await this.resolveMetricPointType(aggregateBy);
|
|
98
|
+
this.pointTypeHintByAggregate.set(aggregateBy, pointType);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return super.aggregateBy(aggregateBy);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private async resolveMetricPointType(
|
|
105
|
+
aggregateBy: AggregateBy<Metric>,
|
|
106
|
+
): Promise<string | null> {
|
|
107
|
+
const metricName: string | null = this.getExactMetricNameFromQuery(
|
|
108
|
+
aggregateBy.query,
|
|
109
|
+
);
|
|
110
|
+
if (!metricName) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const queryRecord: Record<string, unknown> =
|
|
115
|
+
(aggregateBy.query as unknown as Record<string, unknown>) || {};
|
|
116
|
+
const projectIdValue: unknown = queryRecord["projectId"];
|
|
117
|
+
let projectId: string = "";
|
|
118
|
+
if (projectIdValue instanceof ObjectID) {
|
|
119
|
+
projectId = projectIdValue.toString();
|
|
120
|
+
} else if (typeof projectIdValue === "string") {
|
|
121
|
+
projectId = projectIdValue;
|
|
122
|
+
}
|
|
123
|
+
if (!projectId) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const cacheKey: string = `${projectId}|${metricName}`;
|
|
128
|
+
const cached: { pointType: string | null; expiresAt: number } | undefined =
|
|
129
|
+
this.metricPointTypeCache.get(cacheKey);
|
|
130
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
131
|
+
return cached.pointType;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/*
|
|
135
|
+
* Coalesce concurrent misses: a dashboard refresh fires many
|
|
136
|
+
* aggregates for the same metric at once; without this every one
|
|
137
|
+
* of them would issue the same lookup before any could populate
|
|
138
|
+
* the cache.
|
|
139
|
+
*/
|
|
140
|
+
const inFlight: Promise<string | null> | undefined =
|
|
141
|
+
this.inFlightPointTypeLookups.get(cacheKey);
|
|
142
|
+
if (inFlight) {
|
|
143
|
+
return inFlight;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const lookup: Promise<string | null> = this.lookupMetricPointType(
|
|
147
|
+
aggregateBy,
|
|
148
|
+
projectId,
|
|
149
|
+
metricName,
|
|
150
|
+
)
|
|
151
|
+
.then((pointType: string | null) => {
|
|
152
|
+
this.evictExpiredPointTypeCacheEntries();
|
|
153
|
+
this.metricPointTypeCache.set(cacheKey, {
|
|
154
|
+
pointType,
|
|
155
|
+
expiresAt: Date.now() + POINT_TYPE_CACHE_TTL_MS,
|
|
156
|
+
});
|
|
157
|
+
return pointType;
|
|
158
|
+
})
|
|
159
|
+
.finally(() => {
|
|
160
|
+
this.inFlightPointTypeLookups.delete(cacheKey);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
this.inFlightPointTypeLookups.set(cacheKey, lookup);
|
|
164
|
+
return lookup;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private async lookupMetricPointType(
|
|
168
|
+
aggregateBy: AggregateBy<Metric>,
|
|
169
|
+
projectId: string,
|
|
170
|
+
metricName: string,
|
|
171
|
+
): Promise<string | null> {
|
|
172
|
+
try {
|
|
173
|
+
if (!this.database) {
|
|
174
|
+
this.useDefaultDatabase();
|
|
175
|
+
}
|
|
176
|
+
const databaseName: string =
|
|
177
|
+
this.database.getDatasourceOptions().database!;
|
|
178
|
+
|
|
179
|
+
const statement: Statement = SQL`SELECT metricPointType FROM `;
|
|
180
|
+
statement.append(`${databaseName}.${this.model.tableName}`);
|
|
181
|
+
statement.append(
|
|
182
|
+
SQL` WHERE projectId = ${{
|
|
183
|
+
value: projectId,
|
|
184
|
+
type: TableColumnType.ObjectID,
|
|
185
|
+
}} AND name = ${{
|
|
186
|
+
value: metricName,
|
|
187
|
+
type: TableColumnType.Text,
|
|
188
|
+
}} AND isNotNull(metricPointType)`,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
/*
|
|
192
|
+
* Bound the lookup to the aggregation's own window so ClickHouse
|
|
193
|
+
* can prune daily partitions (the table partitions on
|
|
194
|
+
* toYYYYMMDD(time)); without this the LIMIT 1 seek could touch
|
|
195
|
+
* every partition in retention for a metric with all-null point
|
|
196
|
+
* types. The point type is stable per metric name, so any window
|
|
197
|
+
* that has data answers correctly.
|
|
198
|
+
*/
|
|
199
|
+
if (aggregateBy.startTimestamp && aggregateBy.endTimestamp) {
|
|
200
|
+
statement.append(
|
|
201
|
+
` AND time >= toDateTime('${this.formatDateTime(
|
|
202
|
+
aggregateBy.startTimestamp,
|
|
203
|
+
)}') AND time <= toDateTime('${this.formatDateTime(
|
|
204
|
+
aggregateBy.endTimestamp,
|
|
205
|
+
)}')`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
statement.append(SQL` LIMIT 1`);
|
|
210
|
+
|
|
211
|
+
const dbResult: ResultSet<"JSON"> = await this.executeQuery(statement);
|
|
212
|
+
const responseJSON: ResponseJSON<JSONObject> =
|
|
213
|
+
await dbResult.json<JSONObject>();
|
|
214
|
+
const row: JSONObject | undefined = (responseJSON.data || [])[0];
|
|
215
|
+
const rawPointType: unknown = row ? row["metricPointType"] : undefined;
|
|
216
|
+
return typeof rawPointType === "string" ? rawPointType : null;
|
|
217
|
+
} catch (err) {
|
|
218
|
+
/*
|
|
219
|
+
* Lookup failures must not fail the aggregation — fall back to
|
|
220
|
+
* the (possibly MV-served) scalar path, which is today's
|
|
221
|
+
* behavior for unknown point types.
|
|
222
|
+
*/
|
|
223
|
+
logger.debug("Metric point type lookup failed");
|
|
224
|
+
logger.debug(err);
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private evictExpiredPointTypeCacheEntries(): void {
|
|
230
|
+
if (this.metricPointTypeCache.size < POINT_TYPE_CACHE_MAX_ENTRIES) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const now: number = Date.now();
|
|
234
|
+
for (const [key, entry] of this.metricPointTypeCache) {
|
|
235
|
+
if (entry.expiresAt <= now) {
|
|
236
|
+
this.metricPointTypeCache.delete(key);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/*
|
|
240
|
+
* Still full after dropping expired entries: evict oldest-inserted
|
|
241
|
+
* until under the cap (Map preserves insertion order). Never
|
|
242
|
+
* clear() — that would stampede every active dashboard into
|
|
243
|
+
* re-issuing its lookup at once.
|
|
244
|
+
*/
|
|
245
|
+
while (this.metricPointTypeCache.size >= POINT_TYPE_CACHE_MAX_ENTRIES) {
|
|
246
|
+
const oldestKey: string | undefined = this.metricPointTypeCache
|
|
247
|
+
.keys()
|
|
248
|
+
.next().value;
|
|
249
|
+
if (oldestKey === undefined) {
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
this.metricPointTypeCache.delete(oldestKey);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private isDistributionMetricAggregate(
|
|
257
|
+
aggregateBy: AggregateBy<Metric>,
|
|
258
|
+
): boolean {
|
|
259
|
+
const pointType: string | null | undefined =
|
|
260
|
+
this.pointTypeHintByAggregate.get(aggregateBy);
|
|
261
|
+
return Boolean(pointType && DISTRIBUTION_POINT_TYPES.includes(pointType));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Validated, deduplicated attribute keys to group the aggregation by.
|
|
266
|
+
* Keys are bound as query parameters wherever they reach SQL, so this
|
|
267
|
+
* is shape validation (not injection defense): drop non-strings and
|
|
268
|
+
* empties, cap key length and count.
|
|
269
|
+
*/
|
|
270
|
+
private getSanitizedGroupByAttributeKeys(
|
|
271
|
+
aggregateBy: AggregateBy<Metric>,
|
|
272
|
+
): Array<string> {
|
|
273
|
+
const keys: Array<string> = [];
|
|
274
|
+
for (const rawKey of aggregateBy.groupByAttributeKeys || []) {
|
|
275
|
+
if (typeof rawKey !== "string") {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const key: string = rawKey.trim();
|
|
279
|
+
if (
|
|
280
|
+
!key ||
|
|
281
|
+
key.length > MAX_GROUP_BY_ATTRIBUTE_KEY_LENGTH ||
|
|
282
|
+
keys.includes(key)
|
|
283
|
+
) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
keys.push(key);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (keys.length > MAX_GROUP_BY_ATTRIBUTE_KEYS) {
|
|
290
|
+
throw new BadDataException(
|
|
291
|
+
`A maximum of ${MAX_GROUP_BY_ATTRIBUTE_KEYS} group-by attribute keys is supported.`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return keys;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Appends `, attributes[{key}] AS __attr_grp_<i>` for every group-by
|
|
300
|
+
* attribute key. Used in the (inner) SELECT so the outer/main query
|
|
301
|
+
* can group on and re-project the extracted values.
|
|
302
|
+
*/
|
|
303
|
+
private appendAttributeGroupExtractionColumns(
|
|
304
|
+
statement: Statement,
|
|
305
|
+
attributeGroupKeys: Array<string>,
|
|
306
|
+
): void {
|
|
307
|
+
attributeGroupKeys.forEach((key: string, index: number) => {
|
|
308
|
+
statement.append(`, `);
|
|
309
|
+
statement.append(
|
|
310
|
+
SQL`attributes[${{ value: key, type: TableColumnType.Text }}]`,
|
|
311
|
+
);
|
|
312
|
+
statement.append(` AS __attr_grp_${index}`);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Appends `, map({key0}, __attr_grp_0, ...) AS attributes` so grouped
|
|
318
|
+
* rows return exactly the selected keys as an attributes map — the
|
|
319
|
+
* same shape series splitters already read (`row.attributes[key]`).
|
|
320
|
+
*/
|
|
321
|
+
private appendAttributeGroupMapColumn(
|
|
322
|
+
statement: Statement,
|
|
323
|
+
attributeGroupKeys: Array<string>,
|
|
324
|
+
): void {
|
|
325
|
+
if (attributeGroupKeys.length === 0) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
statement.append(`, map(`);
|
|
329
|
+
attributeGroupKeys.forEach((key: string, index: number) => {
|
|
330
|
+
if (index > 0) {
|
|
331
|
+
statement.append(`, `);
|
|
332
|
+
}
|
|
333
|
+
statement.append(SQL`${{ value: key, type: TableColumnType.Text }}`);
|
|
334
|
+
statement.append(`, __attr_grp_${index}`);
|
|
335
|
+
});
|
|
336
|
+
statement.append(`) AS attributes`);
|
|
337
|
+
}
|
|
338
|
+
|
|
28
339
|
/*
|
|
29
340
|
* Cascade deletes from `MetricItemV3` into the aggregating
|
|
30
341
|
* materialized-view target tables.
|
|
@@ -182,6 +493,9 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
182
493
|
return mutableMetricStatement;
|
|
183
494
|
}
|
|
184
495
|
|
|
496
|
+
const attributeGroupKeys: Array<string> =
|
|
497
|
+
this.getSanitizedGroupByAttributeKeys(aggregateBy);
|
|
498
|
+
|
|
185
499
|
if (!isPercentileAggregation(aggregateBy.aggregationType)) {
|
|
186
500
|
/*
|
|
187
501
|
* Try the per-host MV first — host detail pages are the
|
|
@@ -189,21 +503,43 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
189
503
|
* the only one that can serve them. If it doesn't apply
|
|
190
504
|
* (no host filter, or extra attrs/groupBy), fall through
|
|
191
505
|
* to the project/primaryEntityId MV, then to the base table.
|
|
506
|
+
*
|
|
507
|
+
* Distribution metrics (histograms/summaries) must skip the
|
|
508
|
+
* MVs entirely: their states collapse `value`, which for
|
|
509
|
+
* distribution rows is the datapoint SUM — merging those can
|
|
510
|
+
* only ever produce sum-scale results. The base-table builder
|
|
511
|
+
* below weights by the datapoint `count` instead.
|
|
192
512
|
*/
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
513
|
+
if (
|
|
514
|
+
attributeGroupKeys.length === 0 &&
|
|
515
|
+
!this.isDistributionMetricAggregate(aggregateBy)
|
|
516
|
+
) {
|
|
517
|
+
const hostMvStatement: {
|
|
518
|
+
statement: Statement;
|
|
519
|
+
columns: Array<string>;
|
|
520
|
+
} | null = this.tryBuildHostAggregateMVStatement(aggregateBy);
|
|
521
|
+
if (hostMvStatement) {
|
|
522
|
+
return hostMvStatement;
|
|
523
|
+
}
|
|
524
|
+
const mvStatement: {
|
|
525
|
+
statement: Statement;
|
|
526
|
+
columns: Array<string>;
|
|
527
|
+
} | null = this.tryBuildMinuteAggregateMVStatement(aggregateBy);
|
|
528
|
+
if (mvStatement) {
|
|
529
|
+
return mvStatement;
|
|
530
|
+
}
|
|
199
531
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
return
|
|
532
|
+
|
|
533
|
+
if (
|
|
534
|
+
aggregateBy.aggregateColumnName.toString() === "value" &&
|
|
535
|
+
aggregateBy.aggregationTimestampColumnName.toString() === "time"
|
|
536
|
+
) {
|
|
537
|
+
return this.buildScalarAggregateStatement(
|
|
538
|
+
aggregateBy,
|
|
539
|
+
attributeGroupKeys,
|
|
540
|
+
);
|
|
206
541
|
}
|
|
542
|
+
|
|
207
543
|
return super.toAggregateStatement(aggregateBy);
|
|
208
544
|
}
|
|
209
545
|
|
|
@@ -233,7 +569,10 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
233
569
|
* Group-by columns from the caller need to be carried through the
|
|
234
570
|
* inner subquery so the outer GROUP BY can reference them. Only
|
|
235
571
|
* columns that exist on the model are accepted (matches the base
|
|
236
|
-
* generator's safety net).
|
|
572
|
+
* generator's safety net). A whole-map `attributes` group-by is
|
|
573
|
+
* dropped when key-scoped grouping is requested — keeping both
|
|
574
|
+
* would fragment the series again and collide with the
|
|
575
|
+
* `map(...) AS attributes` projection.
|
|
237
576
|
*/
|
|
238
577
|
const groupByKeys: Array<string> = [];
|
|
239
578
|
if (aggregateBy.groupBy) {
|
|
@@ -241,6 +580,9 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
241
580
|
if (!this.model.getTableColumn(key)) {
|
|
242
581
|
continue;
|
|
243
582
|
}
|
|
583
|
+
if (key === "attributes" && attributeGroupKeys.length > 0) {
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
244
586
|
groupByKeys.push(key);
|
|
245
587
|
}
|
|
246
588
|
}
|
|
@@ -336,8 +678,20 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
336
678
|
statement.append(`, ${key}`);
|
|
337
679
|
}
|
|
338
680
|
|
|
681
|
+
/*
|
|
682
|
+
* Attribute-key grouping: the inner subquery extracts each selected
|
|
683
|
+
* key into `__attr_grp_<i>`; the outer query groups on those and
|
|
684
|
+
* folds them back into a per-row `attributes` map so the result
|
|
685
|
+
* shape matches what series splitters read. Grouping happens on the
|
|
686
|
+
* individual keys — NOT the whole attributes map, which would
|
|
687
|
+
* fragment the quantile into one near-constant series per unique
|
|
688
|
+
* attribute combination.
|
|
689
|
+
*/
|
|
690
|
+
this.appendAttributeGroupMapColumn(statement, attributeGroupKeys);
|
|
691
|
+
|
|
339
692
|
statement.append(SQL` FROM (`);
|
|
340
693
|
statement.append(`SELECT ${innerSelectClause}`);
|
|
694
|
+
this.appendAttributeGroupExtractionColumns(statement, attributeGroupKeys);
|
|
341
695
|
statement.append(
|
|
342
696
|
` FROM ${databaseName}.${this.model.tableName} WHERE TRUE `,
|
|
343
697
|
);
|
|
@@ -349,6 +703,9 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
349
703
|
for (const key of groupByKeys) {
|
|
350
704
|
statement.append(`, ${key}`);
|
|
351
705
|
}
|
|
706
|
+
attributeGroupKeys.forEach((_key: string, index: number) => {
|
|
707
|
+
statement.append(`, __attr_grp_${index}`);
|
|
708
|
+
});
|
|
352
709
|
|
|
353
710
|
statement.append(SQL` ORDER BY `).append(sortStatement);
|
|
354
711
|
|
|
@@ -385,6 +742,7 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
385
742
|
aggregationColumn,
|
|
386
743
|
aggregationTimestampColumn,
|
|
387
744
|
...groupByKeys,
|
|
745
|
+
...(attributeGroupKeys.length > 0 ? ["attributes"] : []),
|
|
388
746
|
];
|
|
389
747
|
|
|
390
748
|
logger.debug(`${this.model.tableName} Percentile Aggregate Statement`, {
|
|
@@ -397,6 +755,217 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
397
755
|
return { statement, columns };
|
|
398
756
|
}
|
|
399
757
|
|
|
758
|
+
/**
|
|
759
|
+
* Distribution-aware per-bucket expression for scalar aggregations
|
|
760
|
+
* over `value`. Distribution rows (histogram/summary — identified by
|
|
761
|
+
* a non-null `count`) carry the datapoint's observation total in
|
|
762
|
+
* `sum` (mirrored into `value` at ingest) and the observation count
|
|
763
|
+
* in `count`, so:
|
|
764
|
+
*
|
|
765
|
+
* Avg -> sum(sum) / sum(count) (count-weighted mean of
|
|
766
|
+
* observations, not the mean of per-export-interval sums)
|
|
767
|
+
* Sum -> sum(sum) (total of all observations)
|
|
768
|
+
* Count -> sum(count) (number of observations, not
|
|
769
|
+
* the number of export intervals)
|
|
770
|
+
* Min -> min(min), falling back to the datapoint mean when the
|
|
771
|
+
* optional OTLP min is absent
|
|
772
|
+
* Max -> max(max), same fallback
|
|
773
|
+
*
|
|
774
|
+
* Scalar rows (Sum/Gauge points — `count` is null) degrade to the
|
|
775
|
+
* plain value semantics these aggregations always had: each row is
|
|
776
|
+
* one observation of `value` with weight 1.
|
|
777
|
+
*/
|
|
778
|
+
private getDistributionAwareAggregationExpression(
|
|
779
|
+
aggregationType: AggregationType,
|
|
780
|
+
column: string,
|
|
781
|
+
): string {
|
|
782
|
+
const distRowCondition: string = `isNotNull(count) AND isNotNull(coalesce(sum, ${column}))`;
|
|
783
|
+
const observationTotalExpression: string = `sum(multiIf(${distRowCondition}, toFloat64(coalesce(sum, ${column})), isNotNull(${column}), toFloat64(${column}), 0))`;
|
|
784
|
+
const observationCountExpression: string = `sum(multiIf(${distRowCondition}, toFloat64(count), isNotNull(${column}), 1, 0))`;
|
|
785
|
+
const datapointMeanExpression: string = `toFloat64(coalesce(sum, ${column})) / toFloat64(count)`;
|
|
786
|
+
|
|
787
|
+
switch (aggregationType) {
|
|
788
|
+
case AggregationType.Sum:
|
|
789
|
+
return observationTotalExpression;
|
|
790
|
+
case AggregationType.Count:
|
|
791
|
+
return observationCountExpression;
|
|
792
|
+
case AggregationType.Avg:
|
|
793
|
+
return `if(${observationCountExpression} = 0, 0, ${observationTotalExpression} / ${observationCountExpression})`;
|
|
794
|
+
case AggregationType.Min:
|
|
795
|
+
return `min(multiIf(isNotNull(min), toFloat64(min), ${distRowCondition} AND count > 0, ${datapointMeanExpression}, isNotNull(${column}), toFloat64(${column}), NULL))`;
|
|
796
|
+
case AggregationType.Max:
|
|
797
|
+
return `max(multiIf(isNotNull(max), toFloat64(max), ${distRowCondition} AND count > 0, ${datapointMeanExpression}, isNotNull(${column}), toFloat64(${column}), NULL))`;
|
|
798
|
+
default:
|
|
799
|
+
return `${aggregationType.toLocaleLowerCase()}(${column})`;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Base-table statement for non-percentile aggregations over `value`,
|
|
805
|
+
* used whenever the MV fast paths don't apply (attribute filters,
|
|
806
|
+
* group-by, or a distribution metric). Compared to the generic
|
|
807
|
+
* statement in AnalyticsDatabaseService this adds:
|
|
808
|
+
*
|
|
809
|
+
* - distribution-aware aggregation expressions (see
|
|
810
|
+
* getDistributionAwareAggregationExpression), and
|
|
811
|
+
* - grouping by individual attribute keys via
|
|
812
|
+
* `attributes['<key>']` (aliased and re-projected as a compact
|
|
813
|
+
* `attributes` map), which GroupBy<Metric> cannot express since
|
|
814
|
+
* it only references whole model columns.
|
|
815
|
+
*
|
|
816
|
+
* Model-column group-bys (including the legacy whole-map
|
|
817
|
+
* `groupBy: { attributes: true }` used by the infra pages) are
|
|
818
|
+
* carried through unchanged, appended as raw identifiers exactly
|
|
819
|
+
* like the percentile path does.
|
|
820
|
+
*/
|
|
821
|
+
private buildScalarAggregateStatement(
|
|
822
|
+
aggregateBy: AggregateBy<Metric>,
|
|
823
|
+
attributeGroupKeys: Array<string>,
|
|
824
|
+
): { statement: Statement; columns: Array<string> } {
|
|
825
|
+
if (!this.database) {
|
|
826
|
+
this.useDefaultDatabase();
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const databaseName: string = this.database.getDatasourceOptions().database!;
|
|
830
|
+
|
|
831
|
+
const aggregationColumn: string =
|
|
832
|
+
aggregateBy.aggregateColumnName.toString();
|
|
833
|
+
const aggregationTimestampColumn: string =
|
|
834
|
+
aggregateBy.aggregationTimestampColumnName.toString();
|
|
835
|
+
const aggregationInterval: string = AggregateUtil.getAggregationInterval({
|
|
836
|
+
startDate: aggregateBy.startTimestamp!,
|
|
837
|
+
endDate: aggregateBy.endTimestamp!,
|
|
838
|
+
}).toLowerCase();
|
|
839
|
+
|
|
840
|
+
const groupByKeys: Array<string> = [];
|
|
841
|
+
if (aggregateBy.groupBy) {
|
|
842
|
+
for (const key of Object.keys(aggregateBy.groupBy)) {
|
|
843
|
+
if (!this.model.getTableColumn(key)) {
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
/*
|
|
847
|
+
* Key-scoped grouping supersedes a whole-map `attributes`
|
|
848
|
+
* group-by: keeping both would fragment the series again AND
|
|
849
|
+
* collide with the `map(...) AS attributes` projection below.
|
|
850
|
+
*/
|
|
851
|
+
if (key === "attributes" && attributeGroupKeys.length > 0) {
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
groupByKeys.push(key);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const whereStatement: Statement = this.statementGenerator.toWhereStatement(
|
|
859
|
+
aggregateBy.query,
|
|
860
|
+
);
|
|
861
|
+
const sortStatement: Statement = this.statementGenerator.toSortStatement(
|
|
862
|
+
aggregateBy.sort!,
|
|
863
|
+
);
|
|
864
|
+
|
|
865
|
+
const aggregationExpression: string =
|
|
866
|
+
this.getDistributionAwareAggregationExpression(
|
|
867
|
+
aggregateBy.aggregationType,
|
|
868
|
+
aggregationColumn,
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
const statement: Statement = SQL``;
|
|
872
|
+
|
|
873
|
+
statement.append(
|
|
874
|
+
`SELECT ${aggregationExpression} as ${aggregationColumn}, date_trunc('${aggregationInterval}', toStartOfInterval(${aggregationTimestampColumn}, INTERVAL 1 ${aggregationInterval})) as ${aggregationTimestampColumn}`,
|
|
875
|
+
);
|
|
876
|
+
for (const key of groupByKeys) {
|
|
877
|
+
statement.append(`, ${key}`);
|
|
878
|
+
}
|
|
879
|
+
this.appendAttributeGroupMapColumn(statement, attributeGroupKeys);
|
|
880
|
+
|
|
881
|
+
if (attributeGroupKeys.length > 0) {
|
|
882
|
+
/*
|
|
883
|
+
* Subquery form: extract the selected keys into `__attr_grp_<i>`
|
|
884
|
+
* one level down so the outer `map(...) AS attributes` alias can
|
|
885
|
+
* never shadow (or cycle with) the real `attributes` column the
|
|
886
|
+
* extraction and WHERE filters read. Mirrors the percentile path.
|
|
887
|
+
*/
|
|
888
|
+
const innerColumns: Array<string> = [
|
|
889
|
+
aggregationTimestampColumn,
|
|
890
|
+
"value",
|
|
891
|
+
"sum",
|
|
892
|
+
"count",
|
|
893
|
+
"min",
|
|
894
|
+
"max",
|
|
895
|
+
];
|
|
896
|
+
for (const key of groupByKeys) {
|
|
897
|
+
if (!innerColumns.includes(key)) {
|
|
898
|
+
innerColumns.push(key);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
statement.append(SQL` FROM (`);
|
|
903
|
+
statement.append(`SELECT ${innerColumns.join(", ")}`);
|
|
904
|
+
this.appendAttributeGroupExtractionColumns(statement, attributeGroupKeys);
|
|
905
|
+
statement.append(
|
|
906
|
+
` FROM ${databaseName}.${this.model.tableName} WHERE TRUE `,
|
|
907
|
+
);
|
|
908
|
+
statement.append(whereStatement);
|
|
909
|
+
statement.append(this.getRetentionReadFilter());
|
|
910
|
+
statement.append(SQL`) `);
|
|
911
|
+
} else {
|
|
912
|
+
statement.append(
|
|
913
|
+
` FROM ${databaseName}.${this.model.tableName} WHERE TRUE `,
|
|
914
|
+
);
|
|
915
|
+
statement.append(whereStatement);
|
|
916
|
+
statement.append(this.getRetentionReadFilter());
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
statement.append(SQL` GROUP BY `).append(`${aggregationTimestampColumn}`);
|
|
920
|
+
for (const key of groupByKeys) {
|
|
921
|
+
statement.append(`, ${key}`);
|
|
922
|
+
}
|
|
923
|
+
attributeGroupKeys.forEach((_key: string, index: number) => {
|
|
924
|
+
statement.append(`, __attr_grp_${index}`);
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
statement.append(SQL` ORDER BY `).append(sortStatement);
|
|
928
|
+
|
|
929
|
+
statement.append(
|
|
930
|
+
SQL` LIMIT ${{
|
|
931
|
+
value: Number(aggregateBy.limit),
|
|
932
|
+
type: TableColumnType.Number,
|
|
933
|
+
}}`,
|
|
934
|
+
);
|
|
935
|
+
statement.append(
|
|
936
|
+
SQL` OFFSET ${{
|
|
937
|
+
value: Number(aggregateBy.skip),
|
|
938
|
+
type: TableColumnType.Number,
|
|
939
|
+
}} `,
|
|
940
|
+
);
|
|
941
|
+
|
|
942
|
+
statement.append(
|
|
943
|
+
getQuerySettings({
|
|
944
|
+
additionalSettings: {
|
|
945
|
+
optimize_aggregation_in_order: 1,
|
|
946
|
+
optimize_move_to_prewhere: 1,
|
|
947
|
+
max_threads: 4,
|
|
948
|
+
},
|
|
949
|
+
}),
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
const columns: Array<string> = [
|
|
953
|
+
aggregationColumn,
|
|
954
|
+
aggregationTimestampColumn,
|
|
955
|
+
...groupByKeys,
|
|
956
|
+
...(attributeGroupKeys.length > 0 ? ["attributes"] : []),
|
|
957
|
+
];
|
|
958
|
+
|
|
959
|
+
logger.debug(`${this.model.tableName} Scalar Aggregate Statement`, {
|
|
960
|
+
tableName: this.model.tableName,
|
|
961
|
+
} as LogAttributes);
|
|
962
|
+
logger.debug(statement, {
|
|
963
|
+
tableName: this.model.tableName,
|
|
964
|
+
} as LogAttributes);
|
|
965
|
+
|
|
966
|
+
return { statement, columns };
|
|
967
|
+
}
|
|
968
|
+
|
|
400
969
|
private tryBuildMutableMetricAggregateStatement(
|
|
401
970
|
aggregateBy: AggregateBy<Metric>,
|
|
402
971
|
): { statement: Statement; columns: Array<string> } | null {
|
|
@@ -564,6 +1133,13 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
564
1133
|
private canRouteAggregateToMutableMetricTable(
|
|
565
1134
|
aggregateBy: AggregateBy<Metric>,
|
|
566
1135
|
): boolean {
|
|
1136
|
+
if (
|
|
1137
|
+
aggregateBy.groupByAttributeKeys &&
|
|
1138
|
+
aggregateBy.groupByAttributeKeys.length > 0
|
|
1139
|
+
) {
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
567
1143
|
const supportedColumns: Set<string> = new Set<string>([
|
|
568
1144
|
"projectId",
|
|
569
1145
|
"name",
|
|
@@ -687,6 +1263,13 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
687
1263
|
return null;
|
|
688
1264
|
}
|
|
689
1265
|
|
|
1266
|
+
if (
|
|
1267
|
+
aggregateBy.groupByAttributeKeys &&
|
|
1268
|
+
aggregateBy.groupByAttributeKeys.length > 0
|
|
1269
|
+
) {
|
|
1270
|
+
return null;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
690
1273
|
/*
|
|
691
1274
|
* The MV only carries projectId/name/primaryEntityId/bucketTime, so a
|
|
692
1275
|
* query filtering on any other column (e.g. entityKeys membership,
|
|
@@ -842,6 +1425,13 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
842
1425
|
return null;
|
|
843
1426
|
}
|
|
844
1427
|
|
|
1428
|
+
if (
|
|
1429
|
+
aggregateBy.groupByAttributeKeys &&
|
|
1430
|
+
aggregateBy.groupByAttributeKeys.length > 0
|
|
1431
|
+
) {
|
|
1432
|
+
return null;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
845
1435
|
/*
|
|
846
1436
|
* Inspect the attribute filter. This MV is only safe when
|
|
847
1437
|
* the user is filtering by exactly one attribute,
|