import { PieArcDatum } from '@visx/shape/lib/shapes/Pie';
import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBuilderV2/utils';
import {
initialQueryBuilderFormValuesMap,
OPERATORS,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import cloneDeep from 'lodash-es/cloneDeep';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
Query,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
export function getBaseMeta(
query: Query,
filterKey: string,
): BaseAutocompleteData | null {
const steps = query.builder.queryData;
for (let i = 0; i < steps.length; i++) {
const { groupBy = [] } = steps[i];
for (let j = 0; j < groupBy.length; j++) {
if (groupBy[j].key === filterKey) {
return groupBy[j];
}
}
}
return null;
}
export const getRoute = (key: string): string => {
switch (key) {
case 'view_logs':
return ROUTES.LOGS_EXPLORER;
case 'view_metrics':
return ROUTES.METRICS_EXPLORER;
case 'view_traces':
return ROUTES.TRACES_EXPLORER;
default:
return '';
}
};
export const isNumberDataType = (dataType: DataTypes | undefined): boolean => {
if (!dataType) return false;
return dataType === DataTypes.Int64 || dataType === DataTypes.Float64;
};
export interface FilterData {
filterKey: string;
filterValue: string | number;
operator: string;
}
// Helper function to avoid code duplication
function addFiltersToQuerySteps(
query: Query,
filters: FilterData[],
queryName?: string,
): Query {
// 1) clone so we don't mutate the original
const q = cloneDeep(query);
// 2) map over builder.queryData to return a new modified version
q.builder.queryData = q.builder.queryData.map((step) => {
// Only modify the step that matches the queryName (if provided)
if (queryName && step.queryName !== queryName) {
return step;
}
// 3) build the new filters array
const newFilters = {
...step.filters,
op: step?.filters?.op || 'AND',
items: [...(step?.filters?.items || [])],
};
// Add each filter to the items array
filters.forEach(({ filterKey, filterValue, operator }) => {
// skip if this step doesn't group by our key
const baseMeta = step.groupBy.find((g) => g.key === filterKey);
if (!baseMeta) return;
newFilters.items.push({
id: uuid(),
key: baseMeta,
op: operator,
value: filterValue,
});
});
const resolvedFilters = convertFiltersToExpressionWithExistingQuery(
newFilters,
step.filter?.expression,
);
// 4) return a new step object with updated filters
return {
...step,
...resolvedFilters,
};
});
return q;
}
export function addFilterToQuery(query: Query, filters: FilterData[]): Query {
return addFiltersToQuerySteps(query, filters);
}
export const addFilterToSelectedQuery = (
query: Query,
filters: FilterData[],
queryName: string,
): Query => addFiltersToQuerySteps(query, filters, queryName);
export const getAggregateColumnHeader = (
query: Query,
queryName: string,
): { dataSource: string; aggregations: string } => {
// Find the query step with the matching queryName
const queryStep = query?.builder?.queryData.find(
(step) => step.queryName === queryName,
);
if (!queryStep) {
return { dataSource: '', aggregations: '' };
}
const { dataSource, aggregations } = queryStep; // TODO: check if this is correct
// Extract aggregation expressions based on data source type
let aggregationExpressions: string[] = [];
if (aggregations && aggregations.length > 0) {
if (dataSource === 'metrics') {
// For metrics, construct expression from spaceAggregation(metricName)
aggregationExpressions = aggregations.map((agg: any) => {
const { spaceAggregation, metricName } = agg;
return `${spaceAggregation}(${metricName})`;
});
} else {
// For traces and logs, use the expression field directly
aggregationExpressions = aggregations.map((agg: any) => agg.expression);
}
}
return {
dataSource,
aggregations: aggregationExpressions.join(', '),
};
};
const getFiltersFromMetric = (metric: any): FilterData[] =>
Object.keys(metric).map((key) => ({
filterKey: key,
filterValue: metric[key],
operator: OPERATORS['='],
}));
export const getUplotClickData = ({
metric,
queryData,
absoluteMouseX,
absoluteMouseY,
focusedSeries,
}: {
metric?: { [key: string]: string };
queryData?: { queryName: string; inFocusOrNot: boolean };
absoluteMouseX: number;
absoluteMouseY: number;
focusedSeries?: {
seriesIndex: number;
seriesName: string;
value: number;
color: string;
show: boolean;
isFocused: boolean;
} | null;
}): {
coord: { x: number; y: number };
record: { queryName: string; filters: FilterData[] };
label: string | React.ReactNode;
} | null => {
if (!queryData?.queryName || !metric) {
return null;
}
const record = {
queryName: queryData.queryName,
filters: getFiltersFromMetric(metric),
};
// Generate label from focusedSeries data
let label: string | React.ReactNode = '';
if (focusedSeries && focusedSeries.seriesName) {
label = (
{focusedSeries.seriesName}
);
}
return {
coord: {
x: absoluteMouseX,
y: absoluteMouseY,
},
record,
label,
};
};
export const getPieChartClickData = (
arc: PieArcDatum<{
label: string;
value: string;
color: string;
record: any;
}>,
): {
queryName: string;
filters: FilterData[];
label: string | React.ReactNode;
} | null => {
const { metric, queryName } = arc.data.record;
if (!queryName || !metric) return null;
const label = {arc.data.label};
return {
queryName,
filters: getFiltersFromMetric(metric), // TODO: add where clause query as well.
label,
};
};
/**
* Gets the query data that matches the aggregate data's queryName
*/
export const getQueryData = (
query: Query,
queryName: string,
): IBuilderQuery => {
const queryData = query?.builder?.queryData?.filter(
(item: IBuilderQuery) => item.queryName === queryName,
);
return queryData[0];
};
/**
* Checks if a query name is valid for drilldown operations
* Returns false if queryName is empty or starts with 'F'
* Note: Checking if queryName starts with 'F' is a hack to know if it's a Formulae based query
*/
export const isValidQueryName = (queryName: string): boolean => {
if (!queryName || queryName.trim() === '') {
return false;
}
return !queryName.startsWith('F');
};
const VIEW_QUERY_MAP: Record = {
view_logs: initialQueryBuilderFormValuesMap.logs,
view_metrics: initialQueryBuilderFormValuesMap.metrics,
view_traces: initialQueryBuilderFormValuesMap.traces,
};
/**
* TEMP LOGIC - TO BE REMOVED LATER
* Transforms metric query filters to logs/traces format
* Applies the following transformations:
* - Rule 2: operation → name
* - Rule 3: span.kind → kind
* - Rule 4: status.code → status_code_string with value mapping
* - Rule 5: http.status_code type conversion
*/
const transformMetricsToLogsTraces = (
filterExpression: string | undefined,
): string | undefined => {
if (!filterExpression) return filterExpression;
// ===========================================
// MAPPING OBJECTS - ALL TRANSFORMATIONS DEFINED HERE
// ===========================================
const METRIC_TO_LOGS_TRACES_MAPPINGS = {
// Rule 2: operation → name
attributeRenames: {
operation: 'name',
},
// Rule 3: span.kind → kind with value mapping
spanKindMapping: {
attribute: 'span.kind',
newAttribute: 'kind',
valueMappings: {
SPAN_KIND_INTERNAL: '1',
SPAN_KIND_SERVER: '2',
SPAN_KIND_CLIENT: '3',
SPAN_KIND_PRODUCER: '4',
SPAN_KIND_CONSUMER: '5',
},
},
// Rule 4: status.code → status_code_string with value mapping
statusCodeMapping: {
attribute: 'status.code',
newAttribute: 'status_code_string',
valueMappings: {
// From metrics format → To logs/traces format
STATUS_CODE_UNSET: 'Unset',
STATUS_CODE_OK: 'Ok',
STATUS_CODE_ERROR: 'Error',
},
},
// Rule 5: http.status_code type conversion
typeConversions: {
'http.status_code': 'number',
},
};
// ===========================================
let transformedExpression = filterExpression;
// Apply attribute renames
Object.entries(METRIC_TO_LOGS_TRACES_MAPPINGS.attributeRenames).forEach(
([oldAttr, newAttr]) => {
const regex = new RegExp(`\\b${oldAttr}\\b`, 'g');
transformedExpression = transformedExpression.replace(regex, newAttr);
},
);
// Apply span.kind → kind transformation
const { spanKindMapping } = METRIC_TO_LOGS_TRACES_MAPPINGS;
if (spanKindMapping) {
// Replace attribute name - use word boundaries to avoid partial matches
const attrRegex = new RegExp(
`\\b${spanKindMapping.attribute.replace(/\./g, '\\.')}\\b`,
'g',
);
transformedExpression = transformedExpression.replace(
attrRegex,
spanKindMapping.newAttribute,
);
// Replace values
Object.entries(spanKindMapping.valueMappings).forEach(
([oldValue, newValue]) => {
const valueRegex = new RegExp(`\\b${oldValue}\\b`, 'g');
transformedExpression = transformedExpression.replace(valueRegex, newValue);
},
);
}
// Apply status.code → status_code_string transformation
const { statusCodeMapping } = METRIC_TO_LOGS_TRACES_MAPPINGS;
if (statusCodeMapping) {
// Replace attribute name - use word boundaries to avoid partial matches
// This prevents http.status_code from being transformed
const attrRegex = new RegExp(
`\\b${statusCodeMapping.attribute.replace(/\./g, '\\.')}\\b`,
'g',
);
transformedExpression = transformedExpression.replace(
attrRegex,
statusCodeMapping.newAttribute,
);
// Replace values
Object.entries(statusCodeMapping.valueMappings).forEach(
([oldValue, newValue]) => {
const valueRegex = new RegExp(`\\b${oldValue}\\b`, 'g');
transformedExpression = transformedExpression.replace(
valueRegex,
`${newValue}`,
);
},
);
}
// Note: Type conversions (Rule 5) would need more complex parsing
// of the filter expression to implement properly
return transformedExpression;
};
export const getViewQuery = (
query: Query,
filtersToAdd: FilterData[],
key: string,
queryName: string,
): Query | null => {
const newQuery = cloneDeep(query);
const queryBuilderData = VIEW_QUERY_MAP[key];
if (!queryBuilderData) return null;
let existingFilters: TagFilterItem[] = [];
let existingFilterExpression: string | undefined;
if (queryName) {
const queryData = getQueryData(query, queryName);
existingFilters = queryData?.filters?.items || [];
existingFilterExpression = queryData?.filter?.expression;
}
newQuery.builder.queryData = [queryBuilderData];
const filters = filtersToAdd.reduce((acc: any[], filter) => {
// use existing query to get baseMeta
const baseMeta = getBaseMeta(query, filter.filterKey);
if (!baseMeta) return acc;
acc.push({
id: uuid(),
key: baseMeta,
op: filter.operator,
value: filter.filterValue,
});
return acc;
}, []);
const allFilters = [...existingFilters, ...filters];
const {
// filters: newFilters,
filter: newFilterExpression,
} = convertFiltersToExpressionWithExistingQuery(
{
items: allFilters,
op: 'AND',
},
existingFilterExpression,
);
// newQuery.builder.queryData[0].filters = newFilters;
newQuery.builder.queryData[0].filter = newFilterExpression;
try {
// ===========================================
// TEMP LOGIC - TO BE REMOVED LATER
// ===========================================
// Apply metric-to-logs/traces transformations
if (key === 'view_logs' || key === 'view_traces') {
const transformedExpression = transformMetricsToLogsTraces(
newFilterExpression?.expression,
);
newQuery.builder.queryData[0].filter = {
expression: transformedExpression || '',
};
}
// ===========================================
} catch (error) {
console.error('Error transforming metrics to logs/traces:', error);
}
return newQuery;
};
export function isDrilldownEnabled(): boolean {
return true;
// temp code
// if (typeof window === 'undefined') return false;
// const drilldownValue = window.localStorage.getItem('drilldown');
// return drilldownValue === 'true';
}