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
This commit is contained in:
Nawaz Dhandala
2026-03-12 10:42:09 +00:00
parent a8a9022ea2
commit fa7bde4aca
2 changed files with 141 additions and 6 deletions

View File

@@ -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<string> = 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<string, string> = {
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 {

View File

@@ -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<Service>,
): 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<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
@@ -356,6 +372,23 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
const queryFilter: Record<string, unknown> = queryStringToFilter(
searchQuery,
) as Record<string, unknown>;
// 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<Log> = {
...filterData,
...queryFilter,
@@ -456,6 +489,54 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
const showSidebar: boolean =
props.showFacetSidebar !== false && Boolean(props.facetData);
// Replace serviceId UUIDs with human-readable names in value suggestions
const resolvedValueSuggestions: Record<string, Array<string>> | undefined =
useMemo(() => {
if (!props.valueSuggestions) {
return undefined;
}
const suggestions: Record<string, Array<string>> = {
...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 (
<div className="space-y-2">
{props.showFilters && (
@@ -465,8 +546,8 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
searchQuery={searchQuery}
onSearchQueryChange={setSearchQuery}
onSearchSubmit={handleSearchSubmit}
valueSuggestions={props.valueSuggestions}
onFieldValueSelect={props.onFieldValueSelect}
valueSuggestions={resolvedValueSuggestions}
onFieldValueSelect={handleFieldValueSelectWithServiceResolve}
toolbar={<LogsViewerToolbar {...toolbarProps} />}
/>
</div>