This is an automated email from the ASF dual-hosted git repository.
ankitsultana pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new cf71cc02148 [timeseries] Added query stats panel to timeseries UI
(#17423)
cf71cc02148 is described below
commit cf71cc0214866694b7d593d8e1120ac2a4f39180
Author: Shaurya Chaturvedi <[email protected]>
AuthorDate: Sun Dec 28 20:51:59 2025 -0800
[timeseries] Added query stats panel to timeseries UI (#17423)
* [timeseries] Added query stats panel to timeseries UI
* Minimizing unnecessary changes
---------
Co-authored-by: shauryachats <[email protected]>
---
.../app/components/Query/TimeseriesQueryPage.tsx | 106 +++++++------
.../src/main/resources/app/pages/Query.tsx | 30 +---
.../main/resources/app/utils/PinotMethodUtils.ts | 169 ++++++++++++---------
3 files changed, 153 insertions(+), 152 deletions(-)
diff --git
a/pinot-controller/src/main/resources/app/components/Query/TimeseriesQueryPage.tsx
b/pinot-controller/src/main/resources/app/components/Query/TimeseriesQueryPage.tsx
index c1a2b8d06bb..7e945070f3a 100644
---
a/pinot-controller/src/main/resources/app/components/Query/TimeseriesQueryPage.tsx
+++
b/pinot-controller/src/main/resources/app/components/Query/TimeseriesQueryPage.tsx
@@ -38,7 +38,7 @@ import { UnControlled as CodeMirror } from
'react-codemirror2';
import 'codemirror/lib/codemirror.css';
import 'codemirror/theme/material.css';
import 'codemirror/mode/javascript/javascript';
-import { getTimeSeriesQueryResult, getTimeSeriesLanguages } from
'../../requests';
+import { getTimeSeriesLanguages } from '../../requests';
import { useHistory, useLocation } from 'react-router';
import TableToolbar from '../TableToolbar';
import { Resizable } from 're-resizable';
@@ -46,21 +46,10 @@ import SimpleAccordion from '../SimpleAccordion';
import TimeseriesChart from './TimeseriesChart';
import MetricStatsTable from './MetricStatsTable';
import { parseTimeseriesResponse, isBrokerFormat } from
'../../utils/TimeseriesUtils';
-import { ChartSeries } from 'Models';
+import { ChartSeries, TableData } from 'Models';
import { DEFAULT_SERIES_LIMIT } from '../../utils/ChartConstants';
-
-// Define proper types
-interface TimeseriesQueryResponse {
- resultTable?: {
- dataSchema: {
- columnNames: string[];
- columnDataTypes: string[];
- };
- rows: any[][];
- };
- exceptions?: any[];
- error?: string;
-}
+import CustomizedTables from '../Table';
+import PinotMethodUtils from '../../utils/PinotMethodUtils';
interface CodeMirrorEditor {
getValue: () => string;
@@ -276,6 +265,10 @@ const TimeseriesQueryPage = () => {
const [viewType, setViewType] = useState<'json' | 'chart'>('chart');
const [selectedMetric, setSelectedMetric] = useState<string | null>(null);
const [seriesLimitInput, setSeriesLimitInput] =
useState<string>(DEFAULT_SERIES_LIMIT.toString());
+ const [queryStats, setQueryStats] = useState<TableData>({
+ columns: [],
+ records: [],
+ });
// Fetch supported languages from controller configuration
@@ -364,38 +357,6 @@ const TimeseriesQueryPage = () => {
handleQueryInterfaceKeyDownRef.current = handleQueryInterfaceKeyDown;
}, [handleQueryInterfaceKeyDown]);
- // Extract data processing logic
- const processQueryResponse = useCallback((parsedData:
TimeseriesQueryResponse) => {
- setRawOutput(JSON.stringify(parsedData, null, 2));
-
- // Check for errors
- const errorMsg = parsedData.error ||
- (parsedData.exceptions?.length > 0 ?
- parsedData.exceptions.map((e: any) => e.message ||
e.toString()).join('; ') : '');
- if (errorMsg) {
- setError(errorMsg);
- setChartSeries([]);
- setTotalSeriesCount(0);
- return;
- }
-
- // Parse broker response directly
- if (isBrokerFormat(parsedData)) {
- const series = parseTimeseriesResponse(parsedData);
- setTotalSeriesCount(series.length);
-
- // Create truncated series for visualization (limit to seriesLimitInput
or default to DEFAULT_SERIES_LIMIT)
- const limit = parseInt(seriesLimitInput, 10);
- const effectiveLimit = !isNaN(limit) && limit > 0 ? limit :
DEFAULT_SERIES_LIMIT;
-
- const truncatedSeries = series.slice(0, effectiveLimit);
- setChartSeries(truncatedSeries);
- } else {
- setChartSeries([]);
- setTotalSeriesCount(0);
- }
- }, [seriesLimitInput]);
-
const handleExecuteQuery = useCallback(async () => {
if (!config.query.trim()) {
setError('Please enter a query');
@@ -409,6 +370,7 @@ const TimeseriesQueryPage = () => {
setSelectedMetric(null);
setChartSeries([]);
setTotalSeriesCount(0);
+ setQueryStats({ columns: [], records: [] });
try {
const requestPayload = {
@@ -421,14 +383,39 @@ const TimeseriesQueryPage = () => {
queryOptions: `timeoutMs=${config.timeout}`
};
- const response = await getTimeSeriesQueryResult(requestPayload);
+ // Call the API and process with the shared broker response processor
+ const results = await
PinotMethodUtils.getTimeseriesQueryResults(requestPayload);
+
+ // Set raw output
+ setRawOutput(JSON.stringify(results.data, null, 2));
+
+ // Set query stats (already extracted by the utility)
+ setQueryStats(results.queryStats || { columns: [], records: [] });
+
+ // Handle exceptions/errors
+ if (results.exceptions && results.exceptions.length > 0) {
+ const errorMsg = results.exceptions.map((e: any) => e.message ||
e.toString()).join('; ');
+ setError(errorMsg);
+ setChartSeries([]);
+ setTotalSeriesCount(0);
+ return;
+ }
+
+ // Parse timeseries data for visualization
+ if (isBrokerFormat(results.data)) {
+ const series = parseTimeseriesResponse(results.data);
+ setTotalSeriesCount(series.length);
- // Handle response data - it might be stringified JSON or already an
object
- const parsedData = typeof response.data === 'string'
- ? JSON.parse(response.data)
- : response.data;
+ // Create truncated series for visualization
+ const limit = parseInt(seriesLimitInput, 10);
+ const effectiveLimit = !isNaN(limit) && limit > 0 ? limit :
DEFAULT_SERIES_LIMIT;
- processQueryResponse(parsedData);
+ const truncatedSeries = series.slice(0, effectiveLimit);
+ setChartSeries(truncatedSeries);
+ } else {
+ setChartSeries([]);
+ setTotalSeriesCount(0);
+ }
} catch (error) {
console.error('Error executing timeseries query:', error);
const errorMessage = error.response?.data?.message || error.message ||
'Unknown error occurred';
@@ -436,7 +423,7 @@ const TimeseriesQueryPage = () => {
} finally {
setIsLoading(false);
}
- }, [config, updateURL, processQueryResponse]);
+ }, [config, updateURL, seriesLimitInput]);
const copyToClipboard = () => {
const aux = document.createElement('input');
@@ -562,6 +549,17 @@ const TimeseriesQueryPage = () => {
</Grid>
</Grid>
+ {queryStats.columns.length > 0 && (
+ <Grid item xs style={{ backgroundColor: 'white' }}>
+ <CustomizedTables
+ title="Query Response Stats"
+ data={queryStats}
+ showSearchBox={true}
+ inAccordionFormat={true}
+ />
+ </Grid>
+ )}
+
{rawOutput && (
<Grid item xs style={{ backgroundColor: 'white' }}>
<ViewToggle
diff --git a/pinot-controller/src/main/resources/app/pages/Query.tsx
b/pinot-controller/src/main/resources/app/pages/Query.tsx
index 639807dd84a..692881e541f 100644
--- a/pinot-controller/src/main/resources/app/pages/Query.tsx
+++ b/pinot-controller/src/main/resources/app/pages/Query.tsx
@@ -42,7 +42,7 @@ import CustomizedTables from '../components/Table';
import QuerySideBar from '../components/Query/QuerySideBar';
import TableToolbar from '../components/TableToolbar';
import SimpleAccordion from '../components/SimpleAccordion';
-import PinotMethodUtils from '../utils/PinotMethodUtils';
+import PinotMethodUtils, { QUERY_STATS_COLUMNS } from
'../utils/PinotMethodUtils';
import '../styles/styles.css';
import {Resizable} from "re-resizable";
import {useHistory, useLocation} from 'react-router';
@@ -146,32 +146,6 @@ const sqlFuntionsList = [
'SUMMV', 'AVGMV', 'MINMAXRANGEMV', 'DISTINCTCOUNTMV',
'DISTINCTCOUNTBITMAPMV', 'DISTINCTCOUNTHLLMV',
'DISTINCTCOUNTRAWHLLMV', 'DISTINCT', 'ST_UNION'];
-const responseStatCols = [
- 'timeUsedMs',
- 'numDocsScanned',
- 'totalDocs',
- 'numServersQueried',
- 'numServersResponded',
- 'numSegmentsQueried',
- 'numSegmentsProcessed',
- 'numSegmentsMatched',
- 'numConsumingSegmentsQueried',
- 'numEntriesScannedInFilter',
- 'numEntriesScannedPostFilter',
- 'numGroupsLimitReached',
- 'numGroupsWarningLimitReached',
- 'partialResult',
- 'minConsumingFreshnessTimeMs',
- 'offlineThreadCpuTimeNs',
- 'realtimeThreadCpuTimeNs',
- 'offlineSystemActivitiesCpuTimeNs',
- 'realtimeSystemActivitiesCpuTimeNs',
- 'offlineResponseSerializationCpuTimeNs',
- 'realtimeResponseSerializationCpuTimeNs',
- 'offlineTotalCpuTimeNs',
- 'realtimeTotalCpuTimeNs'
-];
-
// A custom hook that builds on useLocation to parse the query string
function useQuery() {
const { search } = useLocation();
@@ -389,7 +363,7 @@ const QueryPage = () => {
const results = await PinotMethodUtils.getQueryResults(params);
setResultError(results.exceptions || []);
setResultData(results.result || { columns: [], records: [] });
- setQueryStats(results.queryStats || { columns: responseStatCols, records:
[] });
+ setQueryStats(results.queryStats || { columns: QUERY_STATS_COLUMNS,
records: [] });
setOutputResult(JSON.stringify(results.data, null, 2) || '');
setStageStats(results?.data?.stageStats || {});
setWarnings(extractWarnings(results));
diff --git a/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
b/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
index 4db98c7e728..584e92cb6ef 100644
--- a/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
+++ b/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
@@ -62,6 +62,7 @@ import {
getQueryTables,
getTableSchema,
getQueryResult,
+ getTimeSeriesQueryResult,
getTenantTable,
getTableSize,
getIdealState,
@@ -313,82 +314,106 @@ const getAsObject = (str: SQLResult) => {
return str;
};
+// Query stats column names (used for both SQL and Timeseries queries)
+const QUERY_STATS_COLUMNS = ['timeUsedMs',
+ 'numDocsScanned',
+ 'totalDocs',
+ 'numServersQueried',
+ 'numServersResponded',
+ 'numSegmentsQueried',
+ 'numSegmentsProcessed',
+ 'numSegmentsMatched',
+ 'numConsumingSegmentsQueried',
+ 'numEntriesScannedInFilter',
+ 'numEntriesScannedPostFilter',
+ 'numGroupsLimitReached',
+ 'numGroupsWarningLimitReached',
+ 'partialResult',
+ 'minConsumingFreshnessTimeMs',
+ 'offlineThreadCpuTimeNs',
+ 'realtimeThreadCpuTimeNs',
+ 'offlineSystemActivitiesCpuTimeNs',
+ 'realtimeSystemActivitiesCpuTimeNs',
+ 'offlineResponseSerializationCpuTimeNs',
+ 'realtimeResponseSerializationCpuTimeNs',
+ 'offlineTotalCpuTimeNs',
+ 'realtimeTotalCpuTimeNs'
+];
+
+// Extract query stats from broker response
+// This utility can be used for both SQL and Timeseries queries
+const extractQueryStatsFromResponse = (queryResponse) => {
+ const partialResult = queryResponse.partialResult ??
queryResponse.partialResponse;
+
+ return {
+ columns: QUERY_STATS_COLUMNS,
+ records: [[queryResponse.timeUsedMs, queryResponse.numDocsScanned,
queryResponse.totalDocs, queryResponse.numServersQueried,
queryResponse.numServersResponded,
+ queryResponse.numSegmentsQueried, queryResponse.numSegmentsProcessed,
queryResponse.numSegmentsMatched, queryResponse.numConsumingSegmentsQueried,
+ queryResponse.numEntriesScannedInFilter,
queryResponse.numEntriesScannedPostFilter, queryResponse.numGroupsLimitReached,
queryResponse.numGroupsWarningLimitReached,
+ partialResult ?? '-', queryResponse.minConsumingFreshnessTimeMs,
+ queryResponse.offlineThreadCpuTimeNs,
queryResponse.realtimeThreadCpuTimeNs,
+ queryResponse.offlineSystemActivitiesCpuTimeNs,
queryResponse.realtimeSystemActivitiesCpuTimeNs,
+ queryResponse.offlineResponseSerializationCpuTimeNs,
queryResponse.realtimeResponseSerializationCpuTimeNs,
+ queryResponse.offlineTotalCpuTimeNs,
queryResponse.realtimeTotalCpuTimeNs]]
+ };
+};
+
+// Process broker response (used for both SQL and Timeseries queries)
+// Both APIs return BrokerResponseNativeV2 structure
+const processBrokerResponse = (queryResponse) => {
+ let exceptions: SqlException[] = [];
+ let dataArray = [];
+ let columnList = [];
+
+ // if sql api throws error, handle here
+ if(typeof queryResponse === 'string'){
+ exceptions.push({errorCode: null, message: queryResponse});
+ }
+ // if sql api returns a structured error with a `code`, handle here
+ if (queryResponse && queryResponse.code) {
+ if (queryResponse.error) {
+ exceptions.push({errorCode: null, message: "Query failed with error
code: " + queryResponse.code + " and error: " + queryResponse.error});
+ } else {
+ exceptions.push({errorCode: null, message: "Query failed with error
code: " + queryResponse.code + " but no logs. Please see controller logs for
error."});
+ }
+ }
+ if (queryResponse && queryResponse.exceptions &&
queryResponse.exceptions.length) {
+ exceptions = queryResponse.exceptions as SqlException[];
+ }
+ if (queryResponse.resultTable?.dataSchema?.columnNames?.length) {
+ columnList = queryResponse.resultTable.dataSchema.columnNames;
+ dataArray = queryResponse.resultTable.rows;
+ }
+
+ return {
+ exceptions: exceptions,
+ result: {
+ columns: columnList,
+ records: dataArray,
+ },
+ queryStats: extractQueryStatsFromResponse(queryResponse),
+ data: queryResponse,
+ };
+};
+
// This method is used to display query output in tabular format as well as
JSON format on query page
// API: /:urlName (Eg: sql or pql)
// Expected Output: {columns: [], records: []}
const getQueryResults = (params) => {
return getQueryResult(params).then(({ data }) => {
let queryResponse = getAsObject(data);
+ return processBrokerResponse(queryResponse);
+ });
+};
- let exceptions: SqlException[] = [];
- let dataArray = [];
- let columnList = [];
- // if sql api throws error, handle here
- if(typeof queryResponse === 'string'){
- exceptions.push({errorCode: null, message: queryResponse});
- }
- // if sql api returns a structured error with a `code`, handle here
- if (queryResponse && queryResponse.code) {
- if (queryResponse.error) {
- exceptions.push({errorCode: null, message: "Query failed with error
code: " + queryResponse.code + " and error: " + queryResponse.error});
- } else {
- exceptions.push({errorCode: null, message: "Query failed with error
code: " + queryResponse.code + " but no logs. Please see controller logs for
error."});
- }
- }
- if (queryResponse && queryResponse.exceptions &&
queryResponse.exceptions.length) {
- exceptions = queryResponse.exceptions as SqlException[];
- }
- if (queryResponse.resultTable?.dataSchema?.columnNames?.length) {
- columnList = queryResponse.resultTable.dataSchema.columnNames;
- dataArray = queryResponse.resultTable.rows;
- }
-
- const columnStats = ['timeUsedMs',
- 'numDocsScanned',
- 'totalDocs',
- 'numServersQueried',
- 'numServersResponded',
- 'numSegmentsQueried',
- 'numSegmentsProcessed',
- 'numSegmentsMatched',
- 'numConsumingSegmentsQueried',
- 'numEntriesScannedInFilter',
- 'numEntriesScannedPostFilter',
- 'numGroupsLimitReached',
- 'numGroupsWarningLimitReached',
- 'partialResult',
- 'minConsumingFreshnessTimeMs',
- 'offlineThreadCpuTimeNs',
- 'realtimeThreadCpuTimeNs',
- 'offlineSystemActivitiesCpuTimeNs',
- 'realtimeSystemActivitiesCpuTimeNs',
- 'offlineResponseSerializationCpuTimeNs',
- 'realtimeResponseSerializationCpuTimeNs',
- 'offlineTotalCpuTimeNs',
- 'realtimeTotalCpuTimeNs'
- ];
-
- const partialResult = queryResponse.partialResult ??
queryResponse.partialResponse;
-
- return {
- exceptions: exceptions,
- result: {
- columns: columnList,
- records: dataArray,
- },
- queryStats: {
- columns: columnStats,
- records: [[queryResponse.timeUsedMs, queryResponse.numDocsScanned,
queryResponse.totalDocs, queryResponse.numServersQueried,
queryResponse.numServersResponded,
- queryResponse.numSegmentsQueried,
queryResponse.numSegmentsProcessed, queryResponse.numSegmentsMatched,
queryResponse.numConsumingSegmentsQueried,
- queryResponse.numEntriesScannedInFilter,
queryResponse.numEntriesScannedPostFilter, queryResponse.numGroupsLimitReached,
queryResponse.numGroupsWarningLimitReached,
- partialResult ?? '-', queryResponse.minConsumingFreshnessTimeMs,
- queryResponse.offlineThreadCpuTimeNs,
queryResponse.realtimeThreadCpuTimeNs,
- queryResponse.offlineSystemActivitiesCpuTimeNs,
queryResponse.realtimeSystemActivitiesCpuTimeNs,
- queryResponse.offlineResponseSerializationCpuTimeNs,
queryResponse.realtimeResponseSerializationCpuTimeNs,
- queryResponse.offlineTotalCpuTimeNs,
queryResponse.realtimeTotalCpuTimeNs]]
- },
- data: queryResponse,
- };
+// This method processes timeseries query results
+// Uses the same processBrokerResponse as SQL queries since both return
BrokerResponseNativeV2
+// API: /query/timeseries
+// Expected Output: {exceptions: [], result: {columns: [], records: []},
queryStats: {columns: [], records: []}, data: {}}
+const getTimeseriesQueryResults = (params) => {
+ return getTimeSeriesQueryResult(params).then(({ data }) => {
+ let queryResponse = getAsObject(data);
+ return processBrokerResponse(queryResponse);
});
};
@@ -1381,7 +1406,7 @@ const getPackageVersionsData = () => {
packageName,
String(version)
]);
-
+
return {
columns: ['Package', 'Version'],
records: records
@@ -1399,6 +1424,7 @@ export default {
getQueryLogicalTablesList,
getTableSchemaData,
getQueryResults,
+ getTimeseriesQueryResults,
getTenantTableData,
allTableDetailsColumnHeader,
getAllTableDetails,
@@ -1490,3 +1516,6 @@ export default {
getConsumingSegmentsInfoData,
getPackageVersionsData
};
+
+// Named exports for shared constants and utilities
+export { QUERY_STATS_COLUMNS, processBrokerResponse };
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]