1111 */
1212import { DATABASE } from '../config.js' ;
1313import { state } from '../state.js' ;
14- import { query , getQueryErrorDetails , isAbortError } from '../api.js' ;
14+ import { getQueryErrorDetails , isAbortError } from '../api.js' ;
1515import {
1616 startRequestContext , getRequestContext , isRequestCurrent , mergeAbortSignals ,
1717} from '../request-context.js' ;
@@ -21,17 +21,16 @@ import {
2121import { allBreakdowns as defaultBreakdowns } from './definitions.js' ;
2222import { renderBreakdownTable , renderBreakdownError , getNextTopN } from './render.js' ;
2323import { compileFilters } from '../filter-sql.js' ;
24- import { getFiltersForColumn } from '../filters.js' ;
24+
2525import { loadSql } from '../sql-loader.js' ;
2626import { createLimiter } from '../concurrency-limiter.js' ;
2727import {
2828 fetchBreakdownData as fetchCoralogixBreakdown ,
2929} from '../coralogix/adapter.js' ;
3030
31- // Intentionally limits only breakdown queries: breakdowns fan out 20+ parallel
32- // queries (one per facet), the only code path with bulk parallelism. Chart, logs,
33- // and autocomplete each fire 1-2 queries and don't need limiting.
34- const queryLimiter = createLimiter ( 1 ) ;
31+ // Soft cap on Coralogix breakdown concurrency — guards against unbounded
32+ // parallelism if the breakdown list grows significantly.
33+ const coralogixQueryLimiter = createLimiter ( 30 ) ;
3534
3635export function getBreakdowns ( ) {
3736 return state . breakdowns ?. length ? state . breakdowns : defaultBreakdowns ;
@@ -132,64 +131,6 @@ function fillExpectedLabels(data, b) {
132131 } ) ;
133132}
134133
135- /**
136- * Fetch and append missing filtered values to data
137- */
138- async function appendMissingFilteredValues ( data , b , col , aggs , queryParams , requestStatus ) {
139- const { isCurrent, signal } = requestStatus || { } ;
140- const shouldApply = ( ) => ( typeof isCurrent === 'function' ? isCurrent ( ) : true ) ;
141- const { originalCol, isBytes, mult } = queryParams ;
142- const filtersForCol = getFiltersForColumn ( originalCol ) ;
143- if ( filtersForCol . length === 0 || b . getExpectedLabels ) return data ;
144-
145- const existingDims = new Set ( data . map ( ( row ) => row . dim ) ) ;
146- const missingFilterValues = filtersForCol
147- . map ( ( f ) => f . value )
148- . filter ( ( v ) => v !== '' && ! existingDims . has ( v ) ) ;
149-
150- if ( missingFilterValues . length === 0 ) return data ;
151-
152- const searchCol = b . filterCol || col ;
153- const valuesList = missingFilterValues
154- . map ( ( v ) => `'${ v . replace ( / ' / g, "''" ) } '` )
155- . join ( ', ' ) ;
156-
157- const missingValuesSql = await loadSql ( 'breakdown-missing' , {
158- col,
159- aggTotal : isBytes ? `sum(\`response.headers.content_length\`)${ mult } ` : `count()${ mult } ` ,
160- aggOk : aggs . aggOk ,
161- agg4xx : aggs . agg4xx ,
162- agg5xx : aggs . agg5xx ,
163- database : DATABASE ,
164- table : getTable ( ) ,
165- sampleClause : queryParams . sampleClause ,
166- timeFilter : queryParams . timeFilter ,
167- hostFilter : queryParams . hostFilter ,
168- extra : queryParams . extra ,
169- additionalWhereClause : state . additionalWhereClause ,
170- searchCol,
171- valuesList,
172- } ) ;
173-
174- try {
175- if ( ! shouldApply ( ) ) return data ;
176- const missingResult = await query ( missingValuesSql , { signal } ) ;
177- if ( ! shouldApply ( ) ) return data ;
178- if ( missingResult . data && missingResult . data . length > 0 ) {
179- const markedRows = missingResult . data . map ( ( row ) => ( {
180- ...row ,
181- isFilteredValue : true ,
182- } ) ) ;
183- return [ ...data , ...markedRows ] ;
184- }
185- } catch ( err ) {
186- if ( ! shouldApply ( ) ) return data ;
187- if ( isAbortError ( err ) ) return data ;
188- // Silently ignore errors fetching filtered values
189- }
190- return data ;
191- }
192-
193134/**
194135 * Build SQL query parameters for breakdown
195136 */
@@ -426,7 +367,7 @@ async function fetchBreakdownData(b, timeFilter, hostFilter, requestStatus) {
426367 // Get facet column name (handle function-based columns)
427368 const facetCol = typeof b . col === 'function' ? b . col ( state . topN ) : b . col ;
428369
429- // Build params object for compatibility with appendMissingFilteredValues
370+ // Build params object for renderBreakdownTable compatibility
430371 const params = {
431372 col : facetCol ,
432373 originalCol : facetCol ,
@@ -442,8 +383,8 @@ async function fetchBreakdownData(b, timeFilter, hostFilter, requestStatus) {
442383
443384 const startTime = performance . now ( ) ;
444385
445- // Call Coralogix adapter
446- const result = await queryLimiter ( ( ) => fetchCoralogixBreakdown ( {
386+ // Call Coralogix adapter (rate-limited to coralogixQueryLimiter slots)
387+ const result = await coralogixQueryLimiter ( ( ) => fetchCoralogixBreakdown ( {
447388 facet : facetCol ,
448389 topN : state . topN ,
449390 filters : state . filters ,
@@ -463,8 +404,6 @@ async function fetchBreakdownData(b, timeFilter, hostFilter, requestStatus) {
463404 const summaryRatio = getSummaryRatio ( b , totals ) ;
464405
465406 const data = fillExpectedLabels ( resultData , b ) ;
466- // Note: appendMissingFilteredValues uses ClickHouse queries - skip for now with Coralogix
467- // data = await appendMissingFilteredValues(data, b, params.col, aggs, params, requestStatus);
468407 if ( ! isCurrent ( ) ) return null ;
469408
470409 return {
@@ -601,189 +540,46 @@ export function increaseTopN(topNSelectEl, saveStateToURL, loadAllBreakdownsFn)
601540 }
602541}
603542
604- // --- Preview breakdowns during time range selection ---
605-
606- const HOUR_MS = 60 * 60 * 1000 ;
607-
608- function formatPreviewDateTime ( date ) {
609- return date . toISOString ( ) . replace ( 'T' , ' ' ) . slice ( 0 , 19 ) ;
610- }
611-
612- function getPreviewTimeFilter ( start , end ) {
613- const startIso = formatPreviewDateTime ( start ) ;
614- const endIso = formatPreviewDateTime ( end ) ;
615- return `toStartOfMinute(timestamp) BETWEEN toStartOfMinute(toDateTime('${ startIso } ')) AND toStartOfMinute(toDateTime('${ endIso } '))` ;
616- }
617-
618- function getPreviewSamplingConfig ( durationMs ) {
619- if ( durationMs <= HOUR_MS ) {
620- return { sampleClause : '' , multiplier : 1 } ;
621- }
622- const ratio = HOUR_MS / durationMs ;
623- const sampleRate = Math . max ( Math . round ( ratio * 10000 ) / 10000 , 0.0001 ) ;
624- const multiplier = Math . round ( 1 / sampleRate ) ;
625- return { sampleClause : `SAMPLE ${ sampleRate } ` , multiplier } ;
626- }
627-
628- function buildPreviewQueryParams ( b , col , timeFilter , hostFilter , sampling ) {
629- const originalCol = typeof b . col === 'function' ? b . col ( state . topN ) : b . col ;
630- const hasActiveFilter = b . filterOp === 'LIKE' && b . filterCol
631- && state . filters . some ( ( f ) => f . col === originalCol ) ;
632- const actualCol = hasActiveFilter ? b . filterCol : col ;
633-
634- const mode = b . modeToggle ? state [ b . modeToggle ] : 'count' ;
635- const isBytes = mode === 'bytes' ;
636- const { sampleClause, multiplier } = sampling ;
637- const mult = multiplier > 1 ? ` * ${ multiplier } ` : '' ;
638-
639- return {
640- col : actualCol ,
641- originalCol,
642- hasActiveFilter,
643- isBytes,
644- sampleClause,
645- mult,
646- extra : b . extraFilter || '' ,
647- facetFilters : getFacetFiltersExcluding ( originalCol ) ,
648- timeFilter,
649- hostFilter,
650- } ;
651- }
652-
653- async function buildPreviewBreakdownSql ( b , timeFilter , hostFilter , facetTimes , sampling ) {
654- const baseCol = typeof b . col === 'function' ? b . col ( state . topN ) : b . col ;
655-
656- if ( canUseFacetTable ( b ) ) {
657- const { startTime, endTime } = facetTimes ;
658- const dimFilter = b . extraFilter ? "AND dim != ''" : '' ;
659- const hasSummary = ! ! b . summaryDimCondition ;
660- const sql = await loadSql ( 'breakdown-facet' , {
661- database : DATABASE ,
662- facetName : b . facetName ,
663- startTime,
664- endTime,
665- dimFilter,
666- innerSummaryCol : hasSummary
667- ? `,\n if(${ b . summaryDimCondition } , cnt, 0) as summary_cnt`
668- : '' ,
669- summaryCol : hasSummary
670- ? ',\n sum(summary_cnt) as summary_cnt'
671- : '' ,
672- orderBy : b . orderBy || 'cnt DESC' ,
673- topN : String ( state . topN ) ,
674- } ) ;
675-
676- const params = {
677- col : baseCol ,
678- originalCol : baseCol ,
679- hasActiveFilter : false ,
680- isBytes : false ,
681- sampleClause : '' ,
682- mult : '' ,
683- extra : '' ,
684- facetFilters : '' ,
685- timeFilter,
686- hostFilter,
687- } ;
688- return { sql, params, aggs : buildAggregations ( false , '' ) } ;
689- }
690-
691- const params = buildPreviewQueryParams ( b , baseCol , timeFilter , hostFilter , sampling ) ;
692- const aggs = buildAggregations ( params . isBytes , params . mult ) ;
693-
694- if ( b . rawCol && typeof b . col === 'function' ) {
695- const bucketExpr = b . col ( state . topN , 'val' ) ;
696- const innerSummary = b . summaryCountIf
697- ? `,\n countIf(${ b . summaryCountIf } )${ params . mult } as summary_cnt`
698- : '' ;
699- const outerSummary = b . summaryCountIf
700- ? ',\n sum(summary_cnt) as summary_cnt'
701- : '' ;
702-
703- const sql = await loadSql ( 'breakdown-bucketed' , {
704- bucketExpr,
705- rawCol : b . rawCol ,
706- ...aggs ,
707- innerSummaryCol : innerSummary ,
708- outerSummaryCol : outerSummary ,
709- database : DATABASE ,
710- table : getTable ( ) ,
711- sampleClause : params . sampleClause ,
712- timeFilter,
713- hostFilter,
714- facetFilters : params . facetFilters ,
715- extra : params . extra ,
716- additionalWhereClause : state . additionalWhereClause ,
717- topN : String ( state . topN ) ,
718- } ) ;
719-
720- return { sql, params, aggs } ;
721- }
722-
723- const summaryColWithMult = b . summaryCountIf
724- ? `,\n countIf(${ b . summaryCountIf } )${ params . mult } as summary_cnt`
725- : '' ;
726-
727- const sql = await loadSql ( 'breakdown' , {
728- col : params . col ,
729- ...aggs ,
730- summaryCol : summaryColWithMult ,
731- database : DATABASE ,
732- table : getTable ( ) ,
733- sampleClause : params . sampleClause ,
734- timeFilter,
735- hostFilter,
736- facetFilters : params . facetFilters ,
737- extra : params . extra ,
738- additionalWhereClause : state . additionalWhereClause ,
739- orderBy : b . orderBy || 'cnt DESC' ,
740- topN : String ( state . topN ) ,
741- } ) ;
742-
743- return { sql, params, aggs } ;
744- }
745-
746543// Track whether preview is active for CSS indicator
747544let previewActive = false ;
748545
749546export function isPreviewActive ( ) {
750547 return previewActive ;
751548}
752549
753- async function loadPreviewBreakdown (
754- b ,
755- timeFilter ,
756- hostFilter ,
757- facetTimes ,
758- sampling ,
759- requestStatus ,
760- ) {
550+ async function loadPreviewBreakdown ( b , start , end , hostFilter , requestStatus ) {
761551 const { isCurrent, signal } = requestStatus ;
762552 const card = document . getElementById ( b . id ) ;
763553
764554 if ( state . hiddenFacets . includes ( b . id ) ) return ;
765555
766556 card . classList . add ( 'updating' ) ;
767557
558+ const facetCol = typeof b . col === 'function' ? b . col ( state . topN ) : b . col ;
559+
768560 try {
769- const built = await buildPreviewBreakdownSql ( b , timeFilter , hostFilter , facetTimes , sampling ) ;
770- const { sql, params, aggs } = built ;
771- const startTime = performance . now ( ) ;
772- const result = await queryLimiter ( ( ) => query ( sql , { signal } ) ) ;
561+ const perfStart = performance . now ( ) ;
562+ const result = await coralogixQueryLimiter ( ( ) => fetchCoralogixBreakdown ( {
563+ facet : facetCol ,
564+ topN : state . topN ,
565+ filters : state . filters ,
566+ hostFilter,
567+ startTime : start ,
568+ endTime : end ,
569+ extraFilter : b . extraFilter || '' ,
570+ signal,
571+ } ) ) ;
773572 if ( ! isCurrent ( ) ) return ;
774573
775- const elapsed = result . networkTime ?? ( performance . now ( ) - startTime ) ;
574+ const elapsed = result . networkTime ?? ( performance . now ( ) - perfStart ) ;
776575 const summaryRatio = getSummaryRatio ( b , result . totals ) ;
777-
778- let data = fillExpectedLabels ( result . data , b ) ;
779- data = await appendMissingFilteredValues ( data , b , params . col , aggs , params , requestStatus ) ;
780- if ( ! isCurrent ( ) ) return ;
576+ const data = fillExpectedLabels ( result . data , b ) ;
781577
782578 renderBreakdownTable (
783579 b . id ,
784580 data ,
785581 result . totals ,
786- params . col ,
582+ facetCol ,
787583 b . linkPrefix ,
788584 b . linkSuffix ,
789585 b . linkFn ,
@@ -795,9 +591,9 @@ async function loadPreviewBreakdown(
795591 b . summaryColor ,
796592 b . modeToggle ,
797593 ! ! b . getExpectedLabels ,
798- params . hasActiveFilter ? null : b . filterCol ,
799- params . hasActiveFilter ? null : b . filterValueFn ,
800- params . hasActiveFilter ? null : b . filterOp ,
594+ b . filterCol ,
595+ b . filterValueFn ,
596+ b . filterOp ,
801597 ) ;
802598
803599 card . classList . add ( 'preview' ) ;
@@ -821,24 +617,14 @@ export async function loadPreviewBreakdowns(selectionStart, selectionEnd) {
821617 signal : requestContext . signal ,
822618 } ;
823619
824- const durationMs = selectionEnd - selectionStart ;
825620 const start = new Date ( Math . floor ( selectionStart . getTime ( ) / 60000 ) * 60000 ) ;
826621 const end = new Date ( Math . ceil ( selectionEnd . getTime ( ) / 60000 ) * 60000 ) ;
827-
828- const timeFilter = getPreviewTimeFilter ( start , end ) ;
829622 const hostFilter = getHostFilter ( ) ;
830- const facetTimes = {
831- startTime : formatPreviewDateTime ( start ) ,
832- endTime : formatPreviewDateTime ( end ) ,
833- } ;
834- const sampling = getPreviewSamplingConfig ( durationMs ) ;
835623
836624 previewActive = true ;
837625 const breakdowns = getBreakdowns ( ) ;
838626 await Promise . all (
839- breakdowns . map (
840- ( b ) => loadPreviewBreakdown ( b , timeFilter , hostFilter , facetTimes , sampling , requestStatus ) ,
841- ) ,
627+ breakdowns . map ( ( b ) => loadPreviewBreakdown ( b , start , end , hostFilter , requestStatus ) ) ,
842628 ) ;
843629}
844630
0 commit comments