feat: add support for log query attributes and enhance active filter management in LogsViewer

This commit is contained in:
Nawaz Dhandala
2026-03-25 12:00:37 +00:00
parent 27e65caef2
commit 2561117445
6 changed files with 159 additions and 9 deletions

View File

@@ -323,6 +323,21 @@ const DashboardLogsViewer: FunctionComponent<ComponentProps> = (
});
}, [props.serviceIds]);
// Extract attribute filters from logQuery for histogram/facets API calls
const logQueryAttributes: Record<string, string> | undefined = useMemo(() => {
if (!props.logQuery) {
return undefined;
}
const attributes: Record<string, string> | undefined = (props.logQuery as any).attributes as Record<string, string> | undefined;
if (!attributes || Object.keys(attributes).length === 0) {
return undefined;
}
return attributes;
}, [props.logQuery]);
const savedViewOptions: Array<LogsSavedViewOption> = useMemo(() => {
return [...savedViews]
.sort((left: LogSavedView, right: LogSavedView) => {
@@ -536,6 +551,10 @@ const DashboardLogsViewer: FunctionComponent<ComponentProps> = (
(requestData as any)["spanIds"] = Array.from(spanFilterValues);
}
if (logQueryAttributes) {
(requestData as any)["attributes"] = logQueryAttributes;
}
const response: HTTPResponse<JSONObject> = await postApi(
"/telemetry/logs/histogram",
requestData,
@@ -551,7 +570,7 @@ const DashboardLogsViewer: FunctionComponent<ComponentProps> = (
} finally {
setHistogramLoading(false);
}
}, [serviceIdStrings, appliedFacetFilters, timeRange]);
}, [serviceIdStrings, appliedFacetFilters, timeRange, logQueryAttributes]);
// --- Fetch facets ---
@@ -574,6 +593,10 @@ const DashboardLogsViewer: FunctionComponent<ComponentProps> = (
(requestData as any)["serviceIds"] = serviceIdStrings;
}
if (logQueryAttributes) {
(requestData as any)["attributes"] = logQueryAttributes;
}
const response: HTTPResponse<JSONObject> = await postApi(
"/telemetry/logs/facets",
requestData,
@@ -589,7 +612,7 @@ const DashboardLogsViewer: FunctionComponent<ComponentProps> = (
} finally {
setFacetLoading(false);
}
}, [serviceIdStrings, timeRange]);
}, [serviceIdStrings, timeRange, logQueryAttributes]);
// --- Handlers (defined before effects that reference them) ---
@@ -1065,6 +1088,68 @@ const DashboardLogsViewer: FunctionComponent<ComponentProps> = (
[handleFacetInclude],
);
// Build read-only base filter chips from props (serviceIds, traceIds, spanIds, logQuery attributes)
const baseActiveFilters: Array<ActiveFilter> = useMemo(() => {
const filters: Array<ActiveFilter> = [];
if (props.serviceIds && props.serviceIds.length > 0) {
for (const serviceId of props.serviceIds) {
filters.push({
facetKey: "serviceId",
value: serviceId.toString(),
displayKey: "Service",
displayValue: serviceId.toString(),
readOnly: true,
});
}
}
if (props.traceIds && props.traceIds.length > 0) {
for (const traceId of props.traceIds) {
filters.push({
facetKey: "traceId",
value: traceId,
displayKey: "Trace",
displayValue: traceId,
readOnly: true,
});
}
}
if (props.spanIds && props.spanIds.length > 0) {
for (const spanId of props.spanIds) {
filters.push({
facetKey: "spanId",
value: spanId,
displayKey: "Span",
displayValue: spanId,
readOnly: true,
});
}
}
if (logQueryAttributes) {
const attributeDisplayNames: Record<string, string> = {
"resource.k8s.cluster.name": "Cluster",
"resource.k8s.pod.name": "Pod",
"resource.k8s.container.name": "Container",
"resource.k8s.namespace.name": "Namespace",
};
for (const [attrKey, attrValue] of Object.entries(logQueryAttributes)) {
filters.push({
facetKey: `attributes.${attrKey}`,
value: attrValue,
displayKey: attributeDisplayNames[attrKey] || attrKey,
displayValue: attrValue,
readOnly: true,
});
}
}
return filters;
}, [props.serviceIds, props.traceIds, props.spanIds, logQueryAttributes]);
// Build activeFilters array for UI display
const activeFilters: Array<ActiveFilter> = useMemo(() => {
const filters: Array<ActiveFilter> = [];
@@ -1267,6 +1352,7 @@ const DashboardLogsViewer: FunctionComponent<ComponentProps> = (
onFacetExclude={handleFacetExclude}
showFacetSidebar={true}
activeFilters={activeFilters}
baseActiveFilters={baseActiveFilters}
onRemoveFilter={handleRemoveFilter}
onClearAllFilters={handleClearAllFilters}
valueSuggestions={valueSuggestions}

View File

@@ -158,6 +158,10 @@ router.post(
? (body["spanIds"] as Array<string>)
: undefined;
const attributes: Record<string, string> | undefined = body["attributes"]
? (body["attributes"] as Record<string, string>)
: undefined;
const request: HistogramRequest = {
projectId: databaseProps.tenantId,
startTime,
@@ -168,6 +172,7 @@ router.post(
bodySearchText,
traceIds,
spanIds,
attributes,
};
const buckets: Array<HistogramBucket> =
@@ -242,6 +247,10 @@ router.post(
? (body["spanIds"] as Array<string>)
: undefined;
const attributes: Record<string, string> | undefined = body["attributes"]
? (body["attributes"] as Record<string, string>)
: undefined;
const facets: Record<string, Array<FacetValue>> = {};
for (const facetKey of facetKeys) {
@@ -256,6 +265,7 @@ router.post(
bodySearchText,
traceIds,
spanIds,
attributes,
};
facets[facetKey] = await LogAggregationService.getFacetValues(request);

View File

@@ -25,6 +25,7 @@ export interface HistogramRequest {
bodySearchText?: string | undefined;
traceIds?: Array<string> | undefined;
spanIds?: Array<string> | undefined;
attributes?: Record<string, string> | undefined;
}
export interface FacetValue {
@@ -43,6 +44,7 @@ export interface FacetRequest {
bodySearchText?: string | undefined;
traceIds?: Array<string> | undefined;
spanIds?: Array<string> | undefined;
attributes?: Record<string, string> | undefined;
}
export type AnalyticsChartType = "timeseries" | "toplist" | "table";
@@ -589,7 +591,7 @@ export class LogAggregationService {
statement: Statement,
request: Pick<
HistogramRequest,
"serviceIds" | "severityTexts" | "bodySearchText" | "traceIds" | "spanIds"
"serviceIds" | "severityTexts" | "bodySearchText" | "traceIds" | "spanIds" | "attributes"
>,
): void {
if (request.serviceIds && request.serviceIds.length > 0) {
@@ -640,6 +642,22 @@ export class LogAggregationService {
}}`,
);
}
if (request.attributes && Object.keys(request.attributes).length > 0) {
for (const [attrKey, attrValue] of Object.entries(request.attributes)) {
LogAggregationService.validateFacetKey(attrKey);
statement.append(
SQL` AND JSONExtractString(attributes, ${{
type: TableColumnType.Text,
value: attrKey,
}}) = ${{
type: TableColumnType.Text,
value: attrValue,
}}`,
);
}
}
}
@CaptureSpan()

View File

@@ -87,6 +87,7 @@ export interface ComponentProps {
onFacetExclude?: (facetKey: string, value: string) => void;
showFacetSidebar?: boolean;
activeFilters?: Array<ActiveFilter> | undefined;
baseActiveFilters?: Array<ActiveFilter> | undefined;
onRemoveFilter?: ((facetKey: string, value: string) => void) | undefined;
onClearAllFilters?: (() => void) | undefined;
valueSuggestions?: Record<string, Array<string>> | undefined;
@@ -829,11 +830,15 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
</div>
)}
{/* Active filter chips */}
{enrichedActiveFilters.length > 0 && props.onRemoveFilter && (
{/* Active filter chips (read-only base filters + user-applied filters) */}
{((props.baseActiveFilters && props.baseActiveFilters.length > 0) ||
(enrichedActiveFilters.length > 0 && props.onRemoveFilter)) && (
<ActiveFilterChips
filters={enrichedActiveFilters}
onRemove={props.onRemoveFilter}
filters={[
...(props.baseActiveFilters || []),
...enrichedActiveFilters,
]}
onRemove={props.onRemoveFilter || (() => {})}
onClearAll={props.onClearAllFilters || (() => {})}
/>
)}

View File

@@ -16,9 +16,39 @@ const ActiveFilterChips: FunctionComponent<ActiveFilterChipsProps> = (
return null;
}
const readOnlyFilters: Array<ActiveFilter> = props.filters.filter(
(f: ActiveFilter) => {
return f.readOnly;
},
);
const removableFilters: Array<ActiveFilter> = props.filters.filter(
(f: ActiveFilter) => {
return !f.readOnly;
},
);
return (
<div className="flex flex-wrap items-center gap-1.5 px-0.5">
{props.filters.map((filter: ActiveFilter) => {
{readOnlyFilters.map((filter: ActiveFilter) => {
const chipKey: string = `readonly:${filter.facetKey}:${filter.value}`;
return (
<span
key={chipKey}
className="inline-flex items-center gap-1 rounded-md border border-gray-300 bg-gray-100 py-0.5 pl-2 pr-2 text-xs text-gray-700"
title={`${filter.displayKey}: ${filter.displayValue} (applied filter)`}
>
<Icon
icon={IconProp.Lock}
className="h-2.5 w-2.5 text-gray-400"
/>
<span className="font-medium text-gray-500">
{filter.displayKey}:
</span>
<span>{filter.displayValue}</span>
</span>
);
})}
{removableFilters.map((filter: ActiveFilter) => {
const chipKey: string = `${filter.facetKey}:${filter.value}`;
return (
<span
@@ -42,7 +72,7 @@ const ActiveFilterChips: FunctionComponent<ActiveFilterChipsProps> = (
</span>
);
})}
{props.filters.length > 1 && (
{removableFilters.length > 1 && (
<button
type="button"
className="rounded px-1.5 py-0.5 text-[11px] font-medium text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"

View File

@@ -22,6 +22,7 @@ export interface ActiveFilter {
value: string;
displayKey: string;
displayValue: string;
readOnly?: boolean | undefined;
}
export type LogsViewMode = "list" | "analytics";