From fa7bde4aca0719007403a8a866a000e53bed7cb8 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Thu, 12 Mar 2026 10:42:09 +0000 Subject: [PATCH] feat(LogsViewer): add service name resolution and improve value suggestions handling feat(LogQueryToFilter): normalize severity values and enhance filter application logic chore(config): update OpenTelemetry exporter endpoint and headers --- Common/Types/Log/LogQueryToFilter.ts | 62 +++++++++++++- .../UI/Components/LogsViewer/LogsViewer.tsx | 85 ++++++++++++++++++- 2 files changed, 141 insertions(+), 6 deletions(-) diff --git a/Common/Types/Log/LogQueryToFilter.ts b/Common/Types/Log/LogQueryToFilter.ts index f25bfcf0a1..6d6d0a1254 100644 --- a/Common/Types/Log/LogQueryToFilter.ts +++ b/Common/Types/Log/LogQueryToFilter.ts @@ -10,6 +10,7 @@ import { parseLogQuery, } from "./LogQueryParser"; import Search from "../BaseDatabase/Search"; +import NotEqual from "../BaseDatabase/NotEqual"; import GreaterThan from "../BaseDatabase/GreaterThan"; import GreaterThanOrEqual from "../BaseDatabase/GreaterThanOrEqual"; import LessThan from "../BaseDatabase/LessThan"; @@ -33,12 +34,35 @@ const TOP_LEVEL_FIELDS: Set = new Set([ "body", ]); +// Severity values stored in the database use title case (e.g. "Error", "Debug"). +// Normalise user input so that "error" matches "Error", etc. +const SEVERITY_CANONICAL: Record = { + fatal: "Fatal", + error: "Error", + warning: "Warning", + warn: "Warning", + information: "Information", + info: "Information", + debug: "Debug", + trace: "Trace", + unspecified: "Unspecified", +}; + +function normalizeSeverityValue(value: string): string { + return SEVERITY_CANONICAL[value.toLowerCase()] || value; +} + function applyFieldFilter(filter: LogFilter, token: ParsedToken): void { if (!token.field) { return; } - const value: string = token.value; + let value: string = token.value; + + // Normalise severity values to match database casing + if (token.field === "severityText") { + value = normalizeSeverityValue(value); + } if (TOP_LEVEL_FIELDS.has(token.field)) { applyTopLevelFilter(filter, token.field, value, token.operator); @@ -70,8 +94,10 @@ function applyTopLevelFilter( case FilterOperator.LessThanOrEqual: filter[field] = new LessThanOrEqual(parseNumericOrString(value)); break; - case FilterOperator.Equals: case FilterOperator.NotEquals: + filter[field] = new NotEqual(value); + break; + case FilterOperator.Equals: default: filter[field] = value; break; @@ -82,13 +108,41 @@ function applyAttributeFilter( filter: LogFilter, field: string, value: string, - _operator: FilterOperator, + operator: FilterOperator, ): void { if (!filter.attributes) { filter.attributes = {}; } - filter.attributes[field] = value; + switch (operator) { + case FilterOperator.Contains: + case FilterOperator.Wildcard: + filter.attributes[field] = new Search(value.replace(/\*/g, "")); + break; + case FilterOperator.NotEquals: + filter.attributes[field] = new NotEqual(value); + break; + case FilterOperator.GreaterThan: + filter.attributes[field] = new GreaterThan(parseNumericOrString(value)); + break; + case FilterOperator.GreaterThanOrEqual: + filter.attributes[field] = new GreaterThanOrEqual( + parseNumericOrString(value), + ); + break; + case FilterOperator.LessThan: + filter.attributes[field] = new LessThan(parseNumericOrString(value)); + break; + case FilterOperator.LessThanOrEqual: + filter.attributes[field] = new LessThanOrEqual( + parseNumericOrString(value), + ); + break; + case FilterOperator.Equals: + default: + filter.attributes[field] = value; + break; + } } function applyFreeTextFilter(filter: LogFilter, token: ParsedToken): void { diff --git a/Common/UI/Components/LogsViewer/LogsViewer.tsx b/Common/UI/Components/LogsViewer/LogsViewer.tsx index 7c7b71805b..e1d036feeb 100644 --- a/Common/UI/Components/LogsViewer/LogsViewer.tsx +++ b/Common/UI/Components/LogsViewer/LogsViewer.tsx @@ -110,6 +110,22 @@ const getSeverityWeight: (severity: string | undefined) => number = ( return severityWeight[normalized] || 0; }; +// Resolve a human-readable service name to its UUID using the serviceMap. +function resolveServiceNameToId( + name: string, + serviceMap: Dictionary, +): string | undefined { + const lowerName: string = name.toLowerCase(); + + for (const [id, service] of Object.entries(serviceMap)) { + if (service?.name && service.name.toLowerCase() === lowerName) { + return id; + } + } + + return undefined; +} + const LogsViewer: FunctionComponent = ( props: ComponentProps, ): ReactElement => { @@ -356,6 +372,23 @@ const LogsViewer: FunctionComponent = ( const queryFilter: Record = queryStringToFilter( searchQuery, ) as Record; + + // Resolve human-readable service name to UUID if needed + if ( + queryFilter["serviceId"] && + typeof queryFilter["serviceId"] === "string" + ) { + const serviceName: string = queryFilter["serviceId"] as string; + const resolvedId: string | undefined = resolveServiceNameToId( + serviceName, + serviceMap, + ); + + if (resolvedId) { + queryFilter["serviceId"] = resolvedId; + } + } + const mergedFilter: Query = { ...filterData, ...queryFilter, @@ -456,6 +489,54 @@ const LogsViewer: FunctionComponent = ( const showSidebar: boolean = props.showFacetSidebar !== false && Boolean(props.facetData); + // Replace serviceId UUIDs with human-readable names in value suggestions + const resolvedValueSuggestions: Record> | undefined = + useMemo(() => { + if (!props.valueSuggestions) { + return undefined; + } + + const suggestions: Record> = { + ...props.valueSuggestions, + }; + + if (suggestions["serviceId"] && Object.keys(serviceMap).length > 0) { + suggestions["serviceId"] = suggestions["serviceId"].map( + (id: string) => { + const service: Service | undefined = serviceMap[id]; + return service?.name || id; + }, + ); + } + + return suggestions; + }, [props.valueSuggestions, serviceMap]); + + // Wrap onFieldValueSelect to resolve service names back to UUIDs + const handleFieldValueSelectWithServiceResolve: + | ((fieldKey: string, value: string) => void) + | undefined = useMemo(() => { + if (!props.onFieldValueSelect) { + return undefined; + } + + return (fieldKey: string, value: string): void => { + if (fieldKey === "service") { + const resolvedId: string | undefined = resolveServiceNameToId( + value, + serviceMap, + ); + + if (resolvedId) { + props.onFieldValueSelect!(fieldKey, resolvedId); + return; + } + } + + props.onFieldValueSelect!(fieldKey, value); + }; + }, [props.onFieldValueSelect, serviceMap]); + return (
{props.showFilters && ( @@ -465,8 +546,8 @@ const LogsViewer: FunctionComponent = ( searchQuery={searchQuery} onSearchQueryChange={setSearchQuery} onSearchSubmit={handleSearchSubmit} - valueSuggestions={props.valueSuggestions} - onFieldValueSelect={props.onFieldValueSelect} + valueSuggestions={resolvedValueSuggestions} + onFieldValueSelect={handleFieldValueSelectWithServiceResolve} toolbar={} />