feat: include tap attributes in cmd-K full-text search
Add attributes_text flattened field to OpenSearch indexing for both execution and processor levels. Include in full-text search queries, wildcard matching, and highlighting. Merge processor-level attributes into ExecutionSummary. Add 'attribute' category to CommandPalette (design-system 0.1.17) with per-key-value results in the search UI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -130,8 +130,10 @@ public class OpenSearchIndex implements SearchIndex {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final List<String> HIGHLIGHT_FIELDS = List.of(
|
private static final List<String> HIGHLIGHT_FIELDS = List.of(
|
||||||
"error_message", "processors.input_body", "processors.output_body",
|
"error_message", "attributes_text",
|
||||||
"processors.input_headers", "processors.output_headers");
|
"processors.input_body", "processors.output_body",
|
||||||
|
"processors.input_headers", "processors.output_headers",
|
||||||
|
"processors.attributes_text");
|
||||||
|
|
||||||
private org.opensearch.client.opensearch.core.SearchRequest buildSearchRequest(
|
private org.opensearch.client.opensearch.core.SearchRequest buildSearchRequest(
|
||||||
SearchRequest request, int size) {
|
SearchRequest request, int size) {
|
||||||
@@ -197,11 +199,13 @@ public class OpenSearchIndex implements SearchIndex {
|
|||||||
// Search top-level text fields (analyzed match + wildcard for substring)
|
// Search top-level text fields (analyzed match + wildcard for substring)
|
||||||
textQueries.add(Query.of(q -> q.multiMatch(m -> m
|
textQueries.add(Query.of(q -> q.multiMatch(m -> m
|
||||||
.query(text)
|
.query(text)
|
||||||
.fields("error_message", "error_stacktrace"))));
|
.fields("error_message", "error_stacktrace", "attributes_text"))));
|
||||||
textQueries.add(Query.of(q -> q.wildcard(w -> w
|
textQueries.add(Query.of(q -> q.wildcard(w -> w
|
||||||
.field("error_message").value(wildcard).caseInsensitive(true))));
|
.field("error_message").value(wildcard).caseInsensitive(true))));
|
||||||
textQueries.add(Query.of(q -> q.wildcard(w -> w
|
textQueries.add(Query.of(q -> q.wildcard(w -> w
|
||||||
.field("error_stacktrace").value(wildcard).caseInsensitive(true))));
|
.field("error_stacktrace").value(wildcard).caseInsensitive(true))));
|
||||||
|
textQueries.add(Query.of(q -> q.wildcard(w -> w
|
||||||
|
.field("attributes_text").value(wildcard).caseInsensitive(true))));
|
||||||
|
|
||||||
// Search nested processor fields (analyzed match + wildcard)
|
// Search nested processor fields (analyzed match + wildcard)
|
||||||
textQueries.add(Query.of(q -> q.nested(n -> n
|
textQueries.add(Query.of(q -> q.nested(n -> n
|
||||||
@@ -210,14 +214,16 @@ public class OpenSearchIndex implements SearchIndex {
|
|||||||
.query(text)
|
.query(text)
|
||||||
.fields("processors.input_body", "processors.output_body",
|
.fields("processors.input_body", "processors.output_body",
|
||||||
"processors.input_headers", "processors.output_headers",
|
"processors.input_headers", "processors.output_headers",
|
||||||
"processors.error_message", "processors.error_stacktrace"))))));
|
"processors.error_message", "processors.error_stacktrace",
|
||||||
|
"processors.attributes_text"))))));
|
||||||
textQueries.add(Query.of(q -> q.nested(n -> n
|
textQueries.add(Query.of(q -> q.nested(n -> n
|
||||||
.path("processors")
|
.path("processors")
|
||||||
.query(nq -> nq.bool(nb -> nb.should(
|
.query(nq -> nq.bool(nb -> nb.should(
|
||||||
wildcardQuery("processors.input_body", wildcard),
|
wildcardQuery("processors.input_body", wildcard),
|
||||||
wildcardQuery("processors.output_body", wildcard),
|
wildcardQuery("processors.output_body", wildcard),
|
||||||
wildcardQuery("processors.input_headers", wildcard),
|
wildcardQuery("processors.input_headers", wildcard),
|
||||||
wildcardQuery("processors.output_headers", wildcard)
|
wildcardQuery("processors.output_headers", wildcard),
|
||||||
|
wildcardQuery("processors.attributes_text", wildcard)
|
||||||
).minimumShouldMatch("1"))))));
|
).minimumShouldMatch("1"))))));
|
||||||
|
|
||||||
// Also try keyword fields for exact matches
|
// Also try keyword fields for exact matches
|
||||||
@@ -319,7 +325,9 @@ public class OpenSearchIndex implements SearchIndex {
|
|||||||
map.put("error_message", doc.errorMessage());
|
map.put("error_message", doc.errorMessage());
|
||||||
map.put("error_stacktrace", doc.errorStacktrace());
|
map.put("error_stacktrace", doc.errorStacktrace());
|
||||||
if (doc.attributes() != null) {
|
if (doc.attributes() != null) {
|
||||||
map.put("attributes", parseAttributesJson(doc.attributes()));
|
Map<String, String> attrs = parseAttributesJson(doc.attributes());
|
||||||
|
map.put("attributes", attrs);
|
||||||
|
map.put("attributes_text", flattenAttributes(attrs));
|
||||||
}
|
}
|
||||||
if (doc.processors() != null) {
|
if (doc.processors() != null) {
|
||||||
map.put("processors", doc.processors().stream().map(p -> {
|
map.put("processors", doc.processors().stream().map(p -> {
|
||||||
@@ -334,7 +342,9 @@ public class OpenSearchIndex implements SearchIndex {
|
|||||||
pm.put("input_headers", p.inputHeaders());
|
pm.put("input_headers", p.inputHeaders());
|
||||||
pm.put("output_headers", p.outputHeaders());
|
pm.put("output_headers", p.outputHeaders());
|
||||||
if (p.attributes() != null) {
|
if (p.attributes() != null) {
|
||||||
pm.put("attributes", parseAttributesJson(p.attributes()));
|
Map<String, String> pAttrs = parseAttributesJson(p.attributes());
|
||||||
|
pm.put("attributes", pAttrs);
|
||||||
|
pm.put("attributes_text", flattenAttributes(pAttrs));
|
||||||
}
|
}
|
||||||
return pm;
|
return pm;
|
||||||
}).toList());
|
}).toList());
|
||||||
@@ -348,7 +358,20 @@ public class OpenSearchIndex implements SearchIndex {
|
|||||||
if (src == null) return null;
|
if (src == null) return null;
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Map<String, String> attributes = src.get("attributes") instanceof Map
|
Map<String, String> attributes = src.get("attributes") instanceof Map
|
||||||
? (Map<String, String>) src.get("attributes") : null;
|
? new LinkedHashMap<>((Map<String, String>) src.get("attributes")) : null;
|
||||||
|
// Merge processor-level attributes (execution-level takes precedence)
|
||||||
|
if (src.get("processors") instanceof List<?> procs) {
|
||||||
|
for (Object pObj : procs) {
|
||||||
|
if (pObj instanceof Map<?, ?> pm && pm.get("attributes") instanceof Map<?, ?> pa) {
|
||||||
|
if (attributes == null) attributes = new LinkedHashMap<>();
|
||||||
|
for (var entry : pa.entrySet()) {
|
||||||
|
attributes.putIfAbsent(
|
||||||
|
String.valueOf(entry.getKey()),
|
||||||
|
String.valueOf(entry.getValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return new ExecutionSummary(
|
return new ExecutionSummary(
|
||||||
(String) src.get("execution_id"),
|
(String) src.get("execution_id"),
|
||||||
(String) src.get("route_id"),
|
(String) src.get("route_id"),
|
||||||
@@ -384,4 +407,11 @@ public class OpenSearchIndex implements SearchIndex {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String flattenAttributes(Map<String, String> attrs) {
|
||||||
|
if (attrs == null || attrs.isEmpty()) return "";
|
||||||
|
return attrs.entrySet().stream()
|
||||||
|
.map(e -> e.getKey() + "=" + e.getValue())
|
||||||
|
.collect(Collectors.joining(" "));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
ui/package-lock.json
generated
8
ui/package-lock.json
generated
@@ -8,7 +8,7 @@
|
|||||||
"name": "ui",
|
"name": "ui",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cameleer/design-system": "^0.1.16",
|
"@cameleer/design-system": "^0.1.17",
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
"openapi-fetch": "^0.17.0",
|
"openapi-fetch": "^0.17.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
@@ -276,9 +276,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cameleer/design-system": {
|
"node_modules/@cameleer/design-system": {
|
||||||
"version": "0.1.16",
|
"version": "0.1.17",
|
||||||
"resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.16/design-system-0.1.16.tgz",
|
"resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.17/design-system-0.1.17.tgz",
|
||||||
"integrity": "sha512-8ahFv2cirfV2ZZUoELl4ac2iSCOCpKQf4MznwJwoe9WJDYgmQf81m4Qi5uY+rQDVGpECI2MnLjPQ00HQLi5ybQ==",
|
"integrity": "sha512-THK6yN+xSrxEJadEQ4AZiVhPvoI2rq6gvmMonpxVhUw93dOPO5p06pRS5csJc1miFD1thOrazsoDzSTAbNaELw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"generate-api:live": "curl -s http://localhost:8081/api/v1/api-docs -o src/api/openapi.json && openapi-typescript src/api/openapi.json -o src/api/schema.d.ts"
|
"generate-api:live": "curl -s http://localhost:8081/api/v1/api-docs -o src/api/openapi.json && openapi-typescript src/api/openapi.json -o src/api/schema.d.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cameleer/design-system": "^0.1.16",
|
"@cameleer/design-system": "^0.1.17",
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
"openapi-fetch": "^0.17.0",
|
"openapi-fetch": "^0.17.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
|
|||||||
@@ -140,8 +140,30 @@ function LayoutContent() {
|
|||||||
serverFiltered: true,
|
serverFiltered: true,
|
||||||
matchContext: e.highlight ?? undefined,
|
matchContext: e.highlight ?? undefined,
|
||||||
}));
|
}));
|
||||||
return [...catalogData, ...exchangeItems];
|
|
||||||
}, [catalogData, exchangeResults]);
|
const attributeItems: SearchResult[] = [];
|
||||||
|
if (debouncedQuery) {
|
||||||
|
const q = debouncedQuery.toLowerCase();
|
||||||
|
for (const e of exchangeResults?.data || []) {
|
||||||
|
if (!e.attributes) continue;
|
||||||
|
for (const [key, value] of Object.entries(e.attributes as Record<string, string>)) {
|
||||||
|
if (key.toLowerCase().includes(q) || String(value).toLowerCase().includes(q)) {
|
||||||
|
attributeItems.push({
|
||||||
|
id: `${e.executionId}-attr-${key}`,
|
||||||
|
category: 'attribute' as const,
|
||||||
|
title: `${key} = "${value}"`,
|
||||||
|
badges: [{ label: e.status, color: statusToColor(e.status) }],
|
||||||
|
meta: `${e.executionId} · ${e.routeId} · ${e.applicationName ?? ''}`,
|
||||||
|
path: `/exchanges/${e.executionId}`,
|
||||||
|
serverFiltered: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...catalogData, ...exchangeItems, ...attributeItems];
|
||||||
|
}, [catalogData, exchangeResults, debouncedQuery]);
|
||||||
|
|
||||||
const breadcrumb = useMemo(() => {
|
const breadcrumb = useMemo(() => {
|
||||||
const LABELS: Record<string, string> = {
|
const LABELS: Record<string, string> = {
|
||||||
|
|||||||
Reference in New Issue
Block a user