mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 16:09:05 +00:00
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:
co-authored by
Claude Opus 5.5
parent
97c0905ace
commit
2281dfd30b
@@ -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,
|
||||
|
||||
@@ -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"];
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user