feat(logs): show and filter the source stream in multi-stream search (#14922)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Mina Karam
2026-09-29 10:38:35 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 97c0905ace
commit 2281dfd30b
12 changed files with 376 additions and 31 deletions
+8 -4
View File
@@ -184,8 +184,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<ODropdownItem
v-if="
searchObj.data.stream.selectedStreamFields.some((item: any) =>
item.name === row.field ? item.isSchemaField : '',
isFilterableLogField(
row.field,
searchObj.data.stream.selectedStreamFields,
)
"
data-test="log-details-include-field-btn"
@@ -196,8 +197,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</ODropdownItem>
<ODropdownItem
v-if="
searchObj.data.stream.selectedStreamFields.some((item: any) =>
item.name === row.field ? item.isSchemaField : '',
isFilterableLogField(
row.field,
searchObj.data.stream.selectedStreamFields,
)
"
data-test="log-details-exclude-field-btn"
@@ -515,6 +517,7 @@ import OTable from "@/lib/core/Table/OTable.vue";
import OSearchInput from "@/lib/forms/SearchInput/OSearchInput.vue";
import OSeparator from "@/lib/core/Separator/OSeparator.vue";
import { isSafeNavigableUrl } from "@/utils/safeUrl";
import { isFilterableLogField } from "@/utils/logs/streamNameColumn";
const defaultValue: any = () => {
return {
data: {},
@@ -1106,6 +1109,7 @@ export default defineComponent({
recordSizeOptions,
getImageURL,
tableDropdownOpenMap,
isFilterableLogField,
shouldWrapValues,
toggleWrapLogDetails,
copyContentToClipboard,
+7
View File
@@ -638,6 +638,13 @@ describe("JsonPreview Component", () => {
});
describe("Field Operations", () => {
it("offers include/exclude for schema fields in every stream and for _stream_name", () => {
expect(wrapper.vm.canFilterOnField("field1")).toBe(true);
expect(wrapper.vm.canFilterOnField("field3")).toBe(false);
expect(wrapper.vm.canFilterOnField("nested")).toBe(false);
expect(wrapper.vm.canFilterOnField("_stream_name")).toBe(true);
});
it("should return correct label for adding field to table", () => {
wrapper.vm.searchObj.data.stream.selectedFields = ["field1"];
+10 -14
View File
@@ -102,13 +102,7 @@
</OButton>
</template>
<ODropdownItem
v-if="
!hideSearchTermActions &&
searchObj.data.stream.selectedStreamFields.some((item: any) =>
item.name === key ? item.isSchemaField : '',
) &&
multiStreamFields.includes(key)
"
v-if="!hideSearchTermActions && canFilterOnField(key)"
data-test="log-details-include-field-btn"
@select.stop="addSearchTerm(key, value[key], 'include')"
>
@@ -116,13 +110,7 @@
{{ t("common.includeSearchTerm") }}
</ODropdownItem>
<ODropdownItem
v-if="
!hideSearchTermActions &&
searchObj.data.stream.selectedStreamFields.some((item: any) =>
item.name === key ? item.isSchemaField : '',
) &&
multiStreamFields.includes(key)
"
v-if="!hideSearchTermActions && canFilterOnField(key)"
data-test="log-details-exclude-field-btn"
@select.stop="addSearchTerm(key, value[key], 'exclude')"
>
@@ -283,6 +271,7 @@ import { copyToClipboard } from "@/utils/clipboard";
import { timestampToTimezoneDate } from "@/utils/timezone";
import { toast } from "@/lib/feedback/Toast/useToast";
import { isSafeNavigableUrl } from "@/utils/safeUrl";
import { isFilterableLogField, STREAM_NAME_FIELD } from "@/utils/logs/streamNameColumn";
export default {
name: "JsonPreview",
@@ -678,6 +667,12 @@ export default {
}
};
// `_stream_name` is in no schema; a filter on it is rewritten per stream.
const canFilterOnField = (key: string) =>
key === STREAM_NAME_FIELD ||
(isFilterableLogField(key, searchObj.data.stream.selectedStreamFields) &&
multiStreamFields.value.includes(key));
const updateMultiStreamFields = () => {
searchObj.data.stream.selectedStreamFields.forEach((item: any) => {
if (item.streams?.length == searchObj.data.stream.selectedStream.length) {
@@ -871,6 +866,7 @@ export default {
store,
searchObj,
multiStreamFields,
canFilterOnField,
redirectToTraces,
openCorrelation,
filteredTracesStreamOptions,
+2 -4
View File
@@ -888,6 +888,7 @@ import CellActions from "@/plugins/logs/data-table/CellActions.vue";
import O2AIContextAddBtn from "@/components/common/O2AIContextAddBtn.vue";
import { useLogsHighlighter } from "@/composables/useLogsHighlighter";
import { extractStatusFromLog } from "@/utils/logs/statusParser";
import { isFilterableLogField } from "@/utils/logs/streamNameColumn";
import useBreakpoint from "@/composables/useBreakpoint";
import {
buildPatternVolumeContext,
@@ -2237,10 +2238,7 @@ export default defineComponent({
const contextCellIsStreamField = computed(() => {
const columnId = contextCell.value?.columnId;
if (!columnId) return false;
return (
searchObj.data.stream.selectedStreamFields?.find((field: any) => field.name === columnId)
?.isSchemaField ?? false
);
return isFilterableLogField(columnId, searchObj.data.stream.selectedStreamFields);
});
// Mirrors O2AIContextAddBtn's own gate — the AI actions only exist on
@@ -134,6 +134,17 @@ describe("CellActions", () => {
expect(wrapper.vm.isStreamField).toBe(false);
});
it("returns true for _stream_name, which is in no stream schema", () => {
const wrapper = mountComponent({
column: { id: "_stream_name" },
row: { _stream_name: "app" },
selectedStreamFields: [{ name: "status", isSchemaField: true }],
});
expect(wrapper.vm.isStreamField).toBe(true);
expect(wrapper.find('[data-test="log-details-include-field-app"]').exists()).toBe(true);
expect(wrapper.find('[data-test="log-details-exclude-field-app"]').exists()).toBe(true);
});
it("returns false when selectedStreamFields is empty", () => {
const wrapper = mountComponent({
selectedStreamFields: [],
@@ -58,6 +58,7 @@
import { computed } from "vue";
import type { PropType } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { isFilterableLogField } from "@/utils/logs/streamNameColumn";
import EqualIcon from "@/components/icons/EqualIcon.vue";
import NotEqualIcon from "@/components/icons/NotEqualIcon.vue";
import O2AIContextAddBtn from "@/components/common/O2AIContextAddBtn.vue";
@@ -112,8 +113,7 @@ const sendToAiChat = (value: any) => {
emit("sendToAiChat", value);
};
const isStreamField = computed(() => {
const field: any = props.selectedStreamFields?.find((item: any) => item.name === props.column.id);
return field?.isSchemaField ?? false;
});
const isStreamField = computed(() =>
isFilterableLogField(props.column.id, props.selectedStreamFields),
);
</script>