Skip to content

Commit 60b64b0

Browse files
authored
Merge pull request #143 from coralogix/cx-source
Cx source
2 parents a6525ea + 7d83be1 commit 60b64b0

File tree

7 files changed

+217
-398
lines changed

7 files changed

+217
-398
lines changed

css/chart.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
height: 250px;
2222
}
2323

24+
.chart-container.updating {
25+
filter: blur(2px);
26+
opacity: 0.7;
27+
}
28+
2429
#chart {
2530
width: 100%;
2631
height: 100%;

js/breakdowns/index.js

Lines changed: 29 additions & 243 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212
import { DATABASE } from '../config.js';
1313
import { state } from '../state.js';
14-
import { query, getQueryErrorDetails, isAbortError } from '../api.js';
14+
import { getQueryErrorDetails, isAbortError } from '../api.js';
1515
import {
1616
startRequestContext, getRequestContext, isRequestCurrent, mergeAbortSignals,
1717
} from '../request-context.js';
@@ -21,17 +21,16 @@ import {
2121
import { allBreakdowns as defaultBreakdowns } from './definitions.js';
2222
import { renderBreakdownTable, renderBreakdownError, getNextTopN } from './render.js';
2323
import { compileFilters } from '../filter-sql.js';
24-
import { getFiltersForColumn } from '../filters.js';
24+
2525
import { loadSql } from '../sql-loader.js';
2626
import { createLimiter } from '../concurrency-limiter.js';
2727
import {
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

3635
export 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
747544
let previewActive = false;
748545

749546
export 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

Comments
 (0)