mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat: add support for log query attributes and enhance active filter management in LogsViewer
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 || (() => {})}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface ActiveFilter {
|
||||
value: string;
|
||||
displayKey: string;
|
||||
displayValue: string;
|
||||
readOnly?: boolean | undefined;
|
||||
}
|
||||
|
||||
export type LogsViewMode = "list" | "analytics";
|
||||
|
||||
Reference in New Issue
Block a user