diff --git a/App/FeatureSet/Dashboard/src/Components/Logs/LogsViewer.tsx b/App/FeatureSet/Dashboard/src/Components/Logs/LogsViewer.tsx index a5ae442ff4..43e118c64d 100644 --- a/App/FeatureSet/Dashboard/src/Components/Logs/LogsViewer.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Logs/LogsViewer.tsx @@ -323,6 +323,21 @@ const DashboardLogsViewer: FunctionComponent = ( }); }, [props.serviceIds]); + // Extract attribute filters from logQuery for histogram/facets API calls + const logQueryAttributes: Record | undefined = useMemo(() => { + if (!props.logQuery) { + return undefined; + } + + const attributes: Record | undefined = (props.logQuery as any).attributes as Record | undefined; + + if (!attributes || Object.keys(attributes).length === 0) { + return undefined; + } + + return attributes; + }, [props.logQuery]); + const savedViewOptions: Array = useMemo(() => { return [...savedViews] .sort((left: LogSavedView, right: LogSavedView) => { @@ -536,6 +551,10 @@ const DashboardLogsViewer: FunctionComponent = ( (requestData as any)["spanIds"] = Array.from(spanFilterValues); } + if (logQueryAttributes) { + (requestData as any)["attributes"] = logQueryAttributes; + } + const response: HTTPResponse = await postApi( "/telemetry/logs/histogram", requestData, @@ -551,7 +570,7 @@ const DashboardLogsViewer: FunctionComponent = ( } finally { setHistogramLoading(false); } - }, [serviceIdStrings, appliedFacetFilters, timeRange]); + }, [serviceIdStrings, appliedFacetFilters, timeRange, logQueryAttributes]); // --- Fetch facets --- @@ -574,6 +593,10 @@ const DashboardLogsViewer: FunctionComponent = ( (requestData as any)["serviceIds"] = serviceIdStrings; } + if (logQueryAttributes) { + (requestData as any)["attributes"] = logQueryAttributes; + } + const response: HTTPResponse = await postApi( "/telemetry/logs/facets", requestData, @@ -589,7 +612,7 @@ const DashboardLogsViewer: FunctionComponent = ( } finally { setFacetLoading(false); } - }, [serviceIdStrings, timeRange]); + }, [serviceIdStrings, timeRange, logQueryAttributes]); // --- Handlers (defined before effects that reference them) --- @@ -1065,6 +1088,68 @@ const DashboardLogsViewer: FunctionComponent = ( [handleFacetInclude], ); + // Build read-only base filter chips from props (serviceIds, traceIds, spanIds, logQuery attributes) + const baseActiveFilters: Array = useMemo(() => { + const filters: Array = []; + + 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 = { + "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 = useMemo(() => { const filters: Array = []; @@ -1267,6 +1352,7 @@ const DashboardLogsViewer: FunctionComponent = ( onFacetExclude={handleFacetExclude} showFacetSidebar={true} activeFilters={activeFilters} + baseActiveFilters={baseActiveFilters} onRemoveFilter={handleRemoveFilter} onClearAllFilters={handleClearAllFilters} valueSuggestions={valueSuggestions} diff --git a/Common/Server/API/TelemetryAPI.ts b/Common/Server/API/TelemetryAPI.ts index 19403f0f2c..e9805211fe 100644 --- a/Common/Server/API/TelemetryAPI.ts +++ b/Common/Server/API/TelemetryAPI.ts @@ -158,6 +158,10 @@ router.post( ? (body["spanIds"] as Array) : undefined; + const attributes: Record | undefined = body["attributes"] + ? (body["attributes"] as Record) + : undefined; + const request: HistogramRequest = { projectId: databaseProps.tenantId, startTime, @@ -168,6 +172,7 @@ router.post( bodySearchText, traceIds, spanIds, + attributes, }; const buckets: Array = @@ -242,6 +247,10 @@ router.post( ? (body["spanIds"] as Array) : undefined; + const attributes: Record | undefined = body["attributes"] + ? (body["attributes"] as Record) + : undefined; + const facets: Record> = {}; for (const facetKey of facetKeys) { @@ -256,6 +265,7 @@ router.post( bodySearchText, traceIds, spanIds, + attributes, }; facets[facetKey] = await LogAggregationService.getFacetValues(request); diff --git a/Common/Server/Services/LogAggregationService.ts b/Common/Server/Services/LogAggregationService.ts index ee8df594c2..6176a35ba0 100644 --- a/Common/Server/Services/LogAggregationService.ts +++ b/Common/Server/Services/LogAggregationService.ts @@ -25,6 +25,7 @@ export interface HistogramRequest { bodySearchText?: string | undefined; traceIds?: Array | undefined; spanIds?: Array | undefined; + attributes?: Record | undefined; } export interface FacetValue { @@ -43,6 +44,7 @@ export interface FacetRequest { bodySearchText?: string | undefined; traceIds?: Array | undefined; spanIds?: Array | undefined; + attributes?: Record | 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() diff --git a/Common/UI/Components/LogsViewer/LogsViewer.tsx b/Common/UI/Components/LogsViewer/LogsViewer.tsx index 9e80c482da..7073bdeda3 100644 --- a/Common/UI/Components/LogsViewer/LogsViewer.tsx +++ b/Common/UI/Components/LogsViewer/LogsViewer.tsx @@ -87,6 +87,7 @@ export interface ComponentProps { onFacetExclude?: (facetKey: string, value: string) => void; showFacetSidebar?: boolean; activeFilters?: Array | undefined; + baseActiveFilters?: Array | undefined; onRemoveFilter?: ((facetKey: string, value: string) => void) | undefined; onClearAllFilters?: (() => void) | undefined; valueSuggestions?: Record> | undefined; @@ -829,11 +830,15 @@ const LogsViewer: FunctionComponent = ( )} - {/* 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)) && ( {})} onClearAll={props.onClearAllFilters || (() => {})} /> )} diff --git a/Common/UI/Components/LogsViewer/components/ActiveFilterChips.tsx b/Common/UI/Components/LogsViewer/components/ActiveFilterChips.tsx index 201abce03e..f193fb6c5f 100644 --- a/Common/UI/Components/LogsViewer/components/ActiveFilterChips.tsx +++ b/Common/UI/Components/LogsViewer/components/ActiveFilterChips.tsx @@ -16,9 +16,39 @@ const ActiveFilterChips: FunctionComponent = ( return null; } + const readOnlyFilters: Array = props.filters.filter( + (f: ActiveFilter) => { + return f.readOnly; + }, + ); + const removableFilters: Array = props.filters.filter( + (f: ActiveFilter) => { + return !f.readOnly; + }, + ); + return (
- {props.filters.map((filter: ActiveFilter) => { + {readOnlyFilters.map((filter: ActiveFilter) => { + const chipKey: string = `readonly:${filter.facetKey}:${filter.value}`; + return ( + + + + {filter.displayKey}: + + {filter.displayValue} + + ); + })} + {removableFilters.map((filter: ActiveFilter) => { const chipKey: string = `${filter.facetKey}:${filter.value}`; return ( = ( ); })} - {props.filters.length > 1 && ( + {removableFilters.length > 1 && (