feat: traces UI (#589)

* added traces initial plugin and route

* added composable useTraces

* added trace details UI

* add: added api call to get trace spans list

* add: added span details sidebar

* fix: converted bar sizing from px to %

* updated: traces UI

* added gantt chart in trace details page

* fix: on close of trace details pop up reset span details sidebar data

* updated sql query to get traces

* updated traces UI

1. Removed select stream
2. Moved service_name and operation_name at top
3. Disabled sql mode
4. Remove add field in table icon from field list
5. change query added group by trace_id
6. Removed time interval
6. changed histogram chart to scatter

* add: updated custom time range selection in trace details chart

* fix: type errors and resplaced _timestamp with dynamic timestamp key from store

* fix: removed unused files
This commit is contained in:
omkarK06
2023-04-28 21:28:12 +05:30
committed by GitHub
parent 4a1a4a352b
commit f8a9590de5
24 changed files with 5643 additions and 7 deletions
@@ -0,0 +1,3 @@
<svg width="40" height="36" viewBox="0 0 40 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26.2 35.75V29.5H18.5V9H13.85V15.5H0V-0.25H13.85V6H26.2V-0.25H40V15.5H26.2V9H21.5V26.5H26.2V20H40V35.75H26.2ZM29.2 12.5H37V2.75H29.2V12.5ZM29.2 32.75H37V23H29.2V32.75ZM3 12.5H10.85V2.75H3V12.5Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 322 B

+9
View File
@@ -21,6 +21,7 @@ import AddPanel from "@/views/Dashboards/addPanel/AddPanel.vue";
import MemberSubscription from "@/views/MemberSubscription.vue";
import Search from "@/views/Search.vue";
import AppMetrics from "@/views/AppMetrics.vue";
import AppTraces from "@/views/AppTraces.vue";
import LogStream from "@/views/LogStream.vue";
import {
FunctionList,
@@ -71,6 +72,14 @@ const useRoutes = () => {
keepAlive: true,
},
},
{
path: "traces",
name: "traces",
component: AppTraces,
meta: {
keepAlive: true,
},
},
{
path: "logstreams",
name: "logstreams",
+158
View File
@@ -0,0 +1,158 @@
// Copyright 2022 Zinc Labs Inc. and Contributors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http:www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { reactive } from "vue";
import { useLocalTraceFilterField } from "@/utils/zincutils";
const defaultObject = {
organizationIdetifier: "",
runQuery: false,
loading: false,
config: {
splitterModel: 20,
lastSplitterPosition: 0,
splitterLimit: [0, 40],
refreshTimes: [
[
{ label: "5 sec", value: 5 },
{ label: "1 min", value: 60 },
{ label: "1 hr", value: 3600 },
],
[
{ label: "10 sec", value: 10 },
{ label: "5 min", value: 300 },
{ label: "2 hr", value: 7200 },
],
[
{ label: "15 sec", value: 15 },
{ label: "15 min", value: 900 },
{ label: "1 day", value: 86400 },
],
[
{ label: "30 sec", value: 30 },
{ label: "30 min", value: 1800 },
],
],
},
meta: {
refreshInterval: "0",
refreshIntervalLabel: "Off",
showFields: true,
showQuery: true,
showHistogram: true,
showDetailTab: false,
showTraceDetails: false,
sqlMode: false,
resultGrid: {
wrapCells: false,
manualRemoveFields: false,
rowsPerPage: 150,
chartInterval: "1 second",
chartKeyFormat: "HH:mm:ss",
navigation: {
currentRowIndex: 0,
},
},
scrollInfo: {},
},
data: {
query: "",
parsedQuery: {},
errorMsg: "",
errorCode: 0,
additionalErrorMsg: "",
stream: {
streamLists: [],
selectedStream: { label: "", value: "" },
selectedStreamFields: [],
selectedFields: <string[]>[],
filterField: "",
addToFilter: "",
functions: [],
},
resultGrid: {
currentDateTime: new Date(),
currentPage: 0,
columns: <any>[],
},
transforms: <any>[],
queryResults: <any>[],
sortedQueryResults: <any>[],
streamResults: <any>[],
histogram: <any>{},
editorValue: "",
datetime: {
tab: "relative",
relative: {
period: {
label: "Minutes",
value: "Minutes",
},
value: 15,
},
absolute: {
date: {
from: "",
to: "",
},
startTime: "00:00",
endTime: "23:59",
},
},
searchAround: {
indexTimestamp: 0,
size: <number>10,
histogramHide: false,
},
traceDetails: {
traceId: "",
spanList: [],
loading: false,
selectedSpanId: null,
showSpanDetails: false,
},
},
};
// let searchObj = reactive(structuredClone(defaultObject));
// let localLogsObj:any = useLocalLogsObj();
// let searchObj = {};
// if (typeof localLogsObj === "object") {
// searchObj = localLogsObj.value;
// } else {
let searchObj = reactive(Object.assign({}, defaultObject));
// }
const useLogs = () => {
const resetSearchObj = () => {
// delete searchObj.data;
searchObj = reactive(Object.assign({}, defaultObject));
};
const updatedLocalLogFilterField = (): void => {
const identifier: string = searchObj.organizationIdetifier || "default";
const selectedFields: any =
useLocalTraceFilterField()?.value != null
? useLocalTraceFilterField()?.value
: {};
selectedFields[
`${identifier}_${searchObj.data.stream.selectedStream.value}`
] = searchObj.data.stream.selectedFields;
useLocalTraceFilterField(selectedFields);
};
return { searchObj, resetSearchObj, updatedLocalLogFilterField };
};
export default useLogs;
+5
View File
@@ -348,6 +348,11 @@ export default defineComponent({
icon: "img:" + getImageURL("images/left_nav/metrics.svg"),
link: "/metrics",
},
{
title: t("menu.traces"),
icon: "img:" + getImageURL("images/left_nav/traces.svg"),
link: "/traces",
},
{
title: t("menu.dashboard"),
icon: "img:" + getImageURL("images/left_nav/dashb2.svg"),
+1
View File
@@ -29,6 +29,7 @@
"user": "Users",
"index": "Streams",
"search": "Logs",
"traces": "Traces",
"template": "Template",
"signOut": "Sign Out",
"account": "Account",
+2
View File
@@ -14,10 +14,12 @@
import Logs from "./logs/Index.vue";
import AppMetrics from "./metrics/Index.vue";
import AppTraces from "./traces/Index.vue";
export default {
install: (app: any, options: any) => {
app.component("zinc-logs", Logs);
app.component("zinc-metrics", AppMetrics);
app.component("zinc-traces", AppTraces);
},
};
+55
View File
@@ -0,0 +1,55 @@
<!-- Copyright 2022 Zinc Labs Inc. and Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http:www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<template>
<div>
<div class="flex items-start">
<slot name="header" />
</div>
<div v-if="isOpen">
<slot name="content"></slot>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
name: "AppCollapse",
props: {
open: {
type: Boolean,
default: () => false,
},
headerStyle: {
type: Object,
default: () => {},
},
},
setup(props, { emit }) {
const isOpen: any = ref(props.open);
function updateCollapse() {
isOpen.value = !isOpen.value;
}
return {
isOpen,
updateCollapse,
};
},
});
</script>
<style scoped></style>
+398
View File
@@ -0,0 +1,398 @@
<!-- Copyright 2022 Zinc Labs Inc. and Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http:www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<template>
<q-card class="column full-height no-wrap searchdetaildialog">
<q-card-section class="q-pa-md q-pb-md">
<div class="row items-center no-wrap">
<div class="col">
<div
class="text-body1 text-bold text-dark"
data-test="log-detail-title-text"
>
{{ t("search.rowDetail") }}
</div>
</div>
<div class="col-auto">
<q-btn
v-close-popup
round
flat
:icon="'img:' + getImageURL('images/common/close_icon.svg')"
/>
</div>
</div>
</q-card-section>
<q-separator />
<div class="row justify-between">
<div class="col-10">
<q-tabs v-model="tab" shrink align="left">
<q-tab data-test="log-detail-table-tab" name="table" label="Table" />
<q-tab data-test="log-detail-json-tab" name="json" label="JSON" />
</q-tabs>
</div>
<div
v-show="tab === 'table'"
class="col-2 flex justify-end align-center q-pr-md"
>
<q-toggle
data-test="log-detail-wrap-values-toggle-btn"
v-model="shouldWrapValues"
label="Wrap"
color="primary"
size="xs"
@update:model-value="toggleWrapLogDetails"
/>
</div>
</div>
<q-separator />
<q-tab-panels data-test="log-detail-tab-container" v-model="tab" animated>
<q-tab-panel name="table">
<q-card-section
class="q-pa-none q-mb-lg"
data-test="log-detail-table-content"
>
<div
v-if="rowData.length == 0"
class="q-pt-md"
style="max-width: 350px"
>
No data available.
</div>
<div v-else class="indexDetailsContainer">
<q-list
separator
style="height: calc(100vh - 290)"
class="q-px-none q-py-none"
>
<q-item class="list-head">
<q-item-section class="text-bold col-3">
{{ t("search.sourceName") }}
</q-item-section>
<q-item-section class="text-bold col-9">
{{ t("search.sourceValue") }}
</q-item-section>
</q-item>
<q-item
v-for="(key, value) in rowData"
:key="'field_' + value"
class="list-item"
>
<q-item-section
:data-test="`log-detail-${value}-key`"
class="col-3"
>{{ value }}</q-item-section
>
<q-item-section
class="col-9"
:class="!shouldWrapValues ? 'ellipsis' : ''"
style="display: inline;relative"
>
<q-btn-dropdown
data-test="log-details-include-exclude-field-btn"
size="0.5rem"
outlined
filled
dense
class="q-mr-sm pointer"
:name="'img:' + getImageURL('images/common/add_icon.svg')"
>
<q-list>
<q-item clickable v-close-popup>
<q-item-section>
<q-item-label
data-test="log-details-include-field-btn"
@click="
toggleIncludeSearchTerm(`${value}='${key}'`)
"
><q-icon
title="Add to search query"
:name="
'img:' +
getImageURL('images/common/add_icon.svg')
"
size="1rem"
outlined
filled
dense
class="q-mr-sm pointer"
></q-icon
>Inlcude Search Term</q-item-label
>
</q-item-section>
</q-item>
<q-item clickable v-close-popup>
<q-item-section>
<q-item-label
data-test="log-details-exclude-field-btn"
@click="
toggleExcludeSearchTerm(`${value}!='${key}'`)
"
><q-icon
title="Add to search query"
:name="
'img:' +
getImageURL('images/common/add_icon.svg')
"
size="1rem"
outlined
filled
dense
class="q-mr-sm pointer"
></q-icon
>Exclude Search Term</q-item-label
>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
<pre
:data-test="`log-detail-${value}-value`"
class="table-pre"
:style="{
'white-space': !shouldWrapValues ? 'nowrap' : 'pre-wrap',
}"
>{{ key }}</pre
>
</q-item-section>
</q-item>
<q-item></q-item>
</q-list>
</div>
</q-card-section>
</q-tab-panel>
<q-tab-panel name="json" class="q-pa-none">
<q-card-section
data-test="log-detail-json-content"
class="q-pa-none q-mb-lg"
>
<div class="indexDetailsContainer">
<pre class="json-pre">{{ rowData }}</pre>
</div>
</q-card-section>
</q-tab-panel>
</q-tab-panels>
<q-separator />
<q-card-section class="q-pa-md q-pb-md">
<div class="row items-center no-wrap">
<div class="col-2">
<q-btn
class="text-bold"
text-color="light-text"
no-caps
:disabled="currentIndex <= 0"
outline
@click="$emit('showPrevDetail', false, true)"
icon="navigate_before"
label="Previous"
/>
</div>
<div class="col-8 row justify-center align-center q-gutter-sm">
<div style="line-height: 40px; font-weight: bold">No of Records:</div>
<div class="" style="minwidth: 150px">
<q-select
v-model="selectedRelativeValue"
:options="recordSizeOptions"
dense
filled
></q-select>
</div>
<div class="">
<q-btn
data-test="logs-detail-table-search-around-btn"
class="text-bold"
text-color="light-text"
no-caps
:disabled="currentIndex >= totalLength - 1"
outline
label="Search Around"
@click="searchTimeBoxed(rowData, Number(selectedRelativeValue))"
padding="sm md"
/>
</div>
</div>
<div class="col-2 items-end">
<q-btn
data-test="log-detail-next-detail-btn"
class="text-bold"
text-color="light-text"
no-caps
:disabled="currentIndex >= totalLength - 1"
outline
@click="$emit('showNextDetail', true, false)"
icon-right="navigate_next"
label="Next"
/>
</div>
</div>
</q-card-section>
</q-card>
</template>
<script lang="ts">
import { defineComponent, ref, onBeforeMount, onBeforeUnmount } from "vue";
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";
import { getImageURL } from "../../utils/zincutils";
const defaultValue: any = () => {
return {
data: {},
};
};
export default defineComponent({
name: "SearchDetail",
emits: [
"showPrevDetail",
"showNextDetail",
"add:searchterm",
"remove:searchterm",
"search:timeboxed",
],
props: {
modelValue: {
type: Object,
default: () => defaultValue(),
},
currentIndex: {
type: Number,
required: true,
},
totalLength: {
type: Number,
required: true,
},
},
methods: {
toggleIncludeSearchTerm(term: string) {
// if (flag == false) {
this.$emit("add:searchterm", term);
// } else {
// this.$emit("remove:searchterm", term);
// }
},
toggleExcludeSearchTerm(term: string) {
// if (flag == false) {
this.$emit("add:searchterm", term);
// } else {
// this.$emit("remove:searchterm", term);
// }
},
searchTimeBoxed(rowData: any, size: number) {
this.$emit("search:timeboxed", {
key: rowData._timestamp,
size: size,
});
},
},
setup() {
const { t } = useI18n();
const rowData: any = ref({});
const router = useRouter();
const tab = ref("table");
const selectedRelativeValue = ref("10");
const recordSizeOptions: any = ref([10, 20, 50, 100, 200, 500, 1000]);
const shouldWrapValues: any = ref(false);
onBeforeMount(() => {
if (window.localStorage.getItem("wrap-log-details") === null) {
window.localStorage.setItem("wrap-log-details", "false");
}
shouldWrapValues.value =
window.localStorage.getItem("wrap-log-details") === "true";
});
const toggleWrapLogDetails = () => {
window.localStorage.setItem(
"wrap-log-details",
shouldWrapValues.value ? "true" : "false"
);
};
const flattenJSONObject = (obj: any, param: string) => {
let newObj: any = {};
for (let key in obj) {
if (typeof obj[key] === "object") {
// let childJSON = JSON.stringify(obj[key]);
// let unflatten = flattenJSONObject(obj[key], key + ".");
newObj = {
...newObj,
...flattenJSONObject(obj[key], param + key + "."),
};
} else {
newObj[param + key] = obj[key];
}
}
return newObj;
};
return {
t,
router,
rowData,
tab,
flattenJSONObject,
selectedRelativeValue,
recordSizeOptions,
getImageURL,
shouldWrapValues,
toggleWrapLogDetails,
};
},
async created() {
const newObj = await this.flattenJSONObject(this.modelValue, "");
this.rowData = newObj;
},
});
</script>
<style scoped>
.searchdetaildialog {
width: 60vw;
}
.q-item__section {
word-break: break-all;
}
.indexDetailsContainer .q-list .q-item {
height: auto;
}
.q-icon {
cursor: pointer;
}
.table-pre {
white-space: pre-wrap;
word-wrap: break-word;
display: inline;
font-weight: normal;
font-family: Nunito Sans, sans-serif;
}
.json-pre {
height: calc(100vh - 290px);
white-space: pre-wrap;
word-wrap: break-word;
}
</style>
File diff suppressed because it is too large Load Diff
+601
View File
@@ -0,0 +1,601 @@
<!-- Copyright 2022 Zinc Labs Inc. and Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http:www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<template>
<div class="column index-menu">
<div class="index-table q-mt-xs">
<q-table
data-test="log-search-index-list-fields-table"
v-model="searchObj.data.stream.selectedFields"
:visible-columns="['name']"
:rows="searchObj.data.stream.selectedStreamFields"
row-key="name"
:filter="searchObj.data.stream.filterField"
:filter-method="filterFieldFn"
:pagination="{ rowsPerPage: 10000 }"
hide-header
hide-bottom
:wrap-cells="searchObj.meta.resultGrid.wrapCells"
class="field-table"
id="tracesFieldList"
>
<template #body-cell-name="props">
<q-tr :props="props">
<q-td
:props="props"
class="field_list"
:class="
searchObj.data.stream.selectedFields.includes(props.row.name)
? 'selected'
: ''
"
>
<!-- TODO OK : Repeated code make seperate component to display field -->
<div
v-if="props.row.ftsKey || !props.row.showValues"
class="field-container flex content-center ellipsis q-pl-lg q-pr-sm"
:title="props.row.name"
>
<div class="field_label ellipsis">
{{ props.row.name }}
</div>
<div class="field_overlay">
<q-icon
:name="'img:' + getImageURL('images/common/add_icon.svg')"
:data-test="`log-search-index-list-filter-${props.row.name}-field-btn`"
style="margin-right: 0.375rem"
size="1rem"
@click.stop="addToFilter(props.row.name)"
/>
</div>
</div>
<q-expansion-item
v-else
dense
switch-toggle-side
:label="props.row.name"
expand-icon-class="field-expansion-icon"
:expand-icon="
'img:' + getImageURL('images/common/down-solid.svg')
"
:expanded-icon="
'img:' + getImageURL('images/common/up-solid.svg')
"
@before-show="(event: any) => openFilterCreator(event, props.row)"
>
<template v-slot:header>
<div
class="flex content-center ellipsis"
:title="props.row.name"
>
<div class="field_label ellipsis">
{{ props.row.name }}
</div>
<div class="field_overlay">
<q-icon
:data-test="`log-search-index-list-filter-${props.row.name}-field-btn`"
:name="
'img:' + getImageURL('images/common/add_icon.svg')
"
style="margin-right: 0.375rem"
size="1rem"
@click.stop="addToFilter(props.row.name)"
/>
</div>
</div>
</template>
<q-card>
<q-card-section class="q-pl-md q-pr-xs q-py-xs">
<div class="filter-values-container">
<div
v-show="fieldValues[props.row.name]?.isLoading"
class="q-pl-md q-py-xs"
style="height: 60px"
>
<q-inner-loading
size="xs"
:showing="fieldValues[props.row.name]?.isLoading"
label="Fetching values..."
label-style="font-size: 1.1em"
/>
</div>
<div
v-show="
!fieldValues[props.row.name]?.values?.length &&
!fieldValues[props.row.name]?.isLoading
"
class="q-pl-md q-py-xs text-subtitle2"
>
No values found
</div>
<div
v-for="value in fieldValues[props.row.name]?.values ||
[]"
:key="value.key"
>
<q-list dense>
<q-item tag="label" class="q-pr-none">
<div
class="flex row wrap justify-between"
style="width: calc(100% - 36px)"
>
<div
:title="value.key"
class="ellipsis q-pr-xs"
style="width: calc(100% - 50px)"
>
{{ value.key }}
</div>
<div
:title="value.count"
class="ellipsis text-right q-pr-sm"
style="width: 50px"
>
{{ value.count }}
</div>
</div>
<div class="flex row">
<q-icon
:name="
'img:' +
getImageURL('images/common/add_icon.svg')
"
class="q-mr-xs"
size="1rem"
@click="
addSearchTerm(
`${props.row.name}='${value.key}'`
)
"
/>
<q-icon
:name="
'img:' +
getImageURL('images/common/remove_icon.svg')
"
size="1rem"
@click="
addSearchTerm(
`${props.row.name}!='${value.key}'`
)
"
/>
</div>
</q-item>
</q-list>
</div>
</div>
</q-card-section>
</q-card>
</q-expansion-item>
</q-td>
</q-tr>
</template>
<template #top-right>
<q-input
data-test="log-search-index-list-field-search-input"
v-model="searchObj.data.stream.filterField"
data-cy="index-field-search-input"
filled
borderless
dense
clearable
debounce="1"
:placeholder="t('search.searchField')"
>
<template #prepend>
<q-icon name="search" />
</template>
</q-input>
</template>
</q-table>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, type Ref } from "vue";
import { useI18n } from "vue-i18n";
import { useStore } from "vuex";
import { useQuasar } from "quasar";
import { useRouter } from "vue-router";
import useTraces from "../../composables/useTraces";
import { getImageURL } from "../../utils/zincutils";
import streamService from "../../services/stream";
import { getConsumableDateTime } from "@/utils/commons";
export default defineComponent({
name: "ComponentSearchIndexSelect",
setup() {
const store = useStore();
const router = useRouter();
const { t } = useI18n();
const $q = useQuasar();
const { searchObj, updatedLocalLogFilterField } = useTraces();
const streamOptions: any = ref(searchObj.data.stream.streamLists);
const fieldValues: Ref<{
[key: string | number]: {
isLoading: boolean;
values: { key: string; count: string }[];
};
}> = ref({});
const filterStreamFn = (val: string, update: any) => {
update(() => {
streamOptions.value = searchObj.data.stream.streamLists;
const needle = val.toLowerCase();
streamOptions.value = streamOptions.value.filter(
(v: any) => v.label.toLowerCase().indexOf(needle) > -1
);
});
};
const filterFieldFn = (rows: any, terms: any) => {
var filtered = [];
if (terms != "") {
terms = terms.toLowerCase();
for (var i = 0; i < rows.length; i++) {
if (rows[i]["name"].toLowerCase().includes(terms)) {
filtered.push(rows[i]);
}
}
}
return filtered;
};
const addToFilter = (field: any) => {
searchObj.data.stream.addToFilter = field;
};
function clickFieldFn(row: { name: never }, pageIndex: number) {
if (searchObj.data.stream.selectedFields.includes(row.name)) {
searchObj.data.stream.selectedFields =
searchObj.data.stream.selectedFields.filter(
(v: any) => v !== row.name
);
} else {
searchObj.data.stream.selectedFields.push(row.name);
}
searchObj.organizationIdetifier =
store.state.selectedOrganization.identifier;
updatedLocalLogFilterField();
}
const openFilterCreator = (event: any, { name, ftsKey }: any) => {
if (ftsKey) {
event.stopPropagation();
event.preventDefault();
return;
}
const timestamps = getConsumableDateTime(searchObj.data.datetime);
const startISOTimestamp: any =
new Date(timestamps.start_time.toISOString()).getTime() * 1000;
const endISOTimestamp: any =
new Date(timestamps.end_time.toISOString()).getTime() * 1000;
fieldValues.value[name] = {
isLoading: true,
values: [],
};
streamService
.fieldValues({
org_identifier: store.state.selectedOrganization.identifier,
stream_name: searchObj.data.stream.selectedStream.value,
start_time: startISOTimestamp,
end_time: endISOTimestamp,
fields: [name],
size: 10,
type: "traces",
})
.then((res: any) => {
if (res.data.hits.length) {
fieldValues.value[name]["values"] = res.data.hits
.find((field: any) => field.field === name)
.values.map((value: any) => {
return {
key: value.key ? value.key : "null",
count: formatNumberWithPrefix(value.num),
};
});
}
})
.catch(() => {
$q.notify({
type: "negative",
message: `Error while fetching values for ${name}`,
});
})
.finally(() => {
fieldValues.value[name]["isLoading"] = false;
});
};
function formatNumberWithPrefix(number: number) {
if (number >= 1000000000) {
return (number / 1000000000).toFixed(1) + "B";
} else if (number >= 1000000) {
return (number / 1000000).toFixed(1) + "M";
} else if (number >= 1000) {
return (number / 1000).toFixed(1) + "K";
} else {
return number.toString();
}
}
const addSearchTerm = (term: string) => {
// searchObj.meta.showDetailTab = false;
searchObj.data.stream.addToFilter = term;
};
return {
t,
store,
router,
searchObj,
streamOptions,
filterFieldFn,
addToFilter,
clickFieldFn,
getImageURL,
filterStreamFn,
openFilterCreator,
addSearchTerm,
fieldValues,
};
},
});
</script>
<style lang="scss" scoped>
.q-menu {
box-shadow: 0px 3px 15px rgba(0, 0, 0, 0.1);
transform: translateY(0.5rem);
border-radius: 0px;
.q-virtual-scroll__content {
padding: 0.5rem;
}
}
.index-menu {
width: 100%;
.q-field {
&__control {
height: 35px;
padding: 0px 5px;
min-height: auto !important;
&-container {
padding-top: 0px !important;
}
}
&__native :first-of-type {
padding-top: 0.25rem;
}
}
.index-table {
width: 100%;
// border: 1px solid rgba(0, 0, 0, 0.02);
.q-table {
display: table;
table-layout: fixed !important;
}
tr {
margin-bottom: 1px;
}
tbody,
tr,
td {
width: 100%;
display: block;
height: fit-content;
overflow: hidden;
}
.q-table__control,
label.q-field {
width: 100%;
}
.q-table thead tr,
.q-table tbody td {
height: auto;
}
.q-table__top {
border-bottom: unset;
}
}
.field-table {
width: 100%;
}
.field_list {
padding: 0px;
margin-bottom: 0.125rem;
position: relative;
overflow: visible;
cursor: default;
.field_label {
pointer-events: none;
font-size: 0.825rem;
position: relative;
display: inline;
z-index: 2;
left: 0;
// text-transform: capitalize;
}
.field-container {
height: 25px;
}
.field_overlay {
position: absolute;
height: 100%;
right: 0;
top: 0;
z-index: 5;
background-color: #e8e8e8;
padding: 0 6px;
visibility: hidden;
display: flex;
align-items: center;
.q-icon {
cursor: pointer;
opacity: 0;
margin: 0 1px;
}
}
&.selected {
.field_overlay {
background-color: rgba(89, 96, 178, 0.3);
.field_icons {
opacity: 0;
}
}
}
&:hover {
.field-container {
background-color: #e8e8e8;
}
}
}
}
.q-item {
color: $dark-page;
min-height: 1.3rem;
padding: 5px 10px;
&__label {
font-size: 0.75rem;
}
&.q-manual-focusable--focused > .q-focus-helper {
background: none !important;
opacity: 0.3 !important;
}
&.q-manual-focusable--focused > .q-focus-helper,
&--active {
background-color: $selected-list-bg !important;
}
&.q-manual-focusable--focused > .q-focus-helper,
&:hover,
&--active {
color: $primary;
}
}
.q-field--dense .q-field__before,
.q-field--dense .q-field__prepend {
padding: 0px 0px 0px 0px;
height: auto;
line-height: auto;
}
.q-field__native,
.q-field__input {
padding: 0px 0px 0px 0px;
}
.q-field--dense .q-field__label {
top: 5px;
}
.q-field--dense .q-field__control,
.q-field--dense .q-field__marginal {
height: 34px;
}
</style>
<style lang="scss">
.index-table {
.q-table {
width: 100%;
table-layout: fixed;
.q-expansion-item {
.q-item {
display: flex;
align-items: center;
padding: 0;
height: 25px !important;
min-height: 25px !important;
}
.q-item__section--avatar {
min-width: 12px;
max-width: 12px;
margin-right: 8px;
}
.filter-values-container {
.q-item {
padding-left: 4px;
.q-focus-helper {
background: none !important;
}
}
}
.q-item-type {
&:hover {
.field_overlay {
visibility: visible;
.q-icon {
opacity: 1;
}
}
}
}
.field-expansion-icon {
img {
width: 12px;
height: 12px;
}
}
}
.field-container {
&:hover {
.field_overlay {
visibility: visible;
.q-icon {
opacity: 1;
}
}
}
}
.field_list {
&.selected {
.q-expansion-item {
background-color: rgba(89, 96, 178, 0.3);
}
.field_overlay {
background-color: #ffffff;
}
}
}
}
}
</style>
+332
View File
@@ -0,0 +1,332 @@
<!-- Copyright 2022 Zinc Labs Inc. and Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http:www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<template>
<div data-test="query-editor" ref="editorRef" id="editor"></div>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted, onUpdated } from "vue";
import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
import { useStore } from "vuex";
export default defineComponent({
props: {
query: {
type: String,
default: "",
},
fields: {
type: Array,
default: [],
},
functions: {
type: Array,
default: [],
},
},
emits: ["update-query", "run-query"],
setup(props, { emit }) {
const editorRef: any = ref();
let editorObj: any = null;
const store = useStore();
const createDependencyProposals = (range: any) => {
const keywords = [
{
label: "and",
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: "and ",
range: range,
},
{
label: "or",
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: "or ",
range: range,
},
{
label: "like",
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: "like '%${1:params}%' ",
range: range,
},
{
label: "in",
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: "in ('${1:params}') ",
range: range,
insertTextRules:
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
},
{
label: "not in",
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: "not in ('${1:params}') ",
range: range,
insertTextRules:
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
},
{
label: "between",
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: "between '${1:params}' and '${1:params}' ",
range: range,
insertTextRules:
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
},
{
label: "not between",
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: "not between '${1:params}' and '${1:params}' ",
range: range,
insertTextRules:
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
},
{
label: "is null",
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: "is null ",
range: range,
},
{
label: "is not null",
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: "is not null ",
range: range,
},
{
label: ">",
kind: monaco.languages.CompletionItemKind.Operator,
insertText: "> ",
range: range,
},
{
label: "<",
kind: monaco.languages.CompletionItemKind.Operator,
insertText: "< ",
range: range,
},
{
label: ">=",
kind: monaco.languages.CompletionItemKind.Operator,
insertText: ">= ",
range: range,
},
{
label: "<=",
kind: monaco.languages.CompletionItemKind.Operator,
insertText: "<= ",
range: range,
},
{
label: "<>",
kind: monaco.languages.CompletionItemKind.Operator,
insertText: "<> ",
range: range,
},
{
label: "=",
kind: monaco.languages.CompletionItemKind.Operator,
insertText: "= ",
range: range,
},
{
label: "!=",
kind: monaco.languages.CompletionItemKind.Operator,
insertText: "!= ",
range: range,
},
{
label: "()",
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: "(${1:condition}) ",
range: range,
insertTextRules:
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
},
];
props.fields.forEach((field: any) => {
if (field.name == store.state.zoConfig.timestamp_column) {
return;
}
let itemObj = {
label: field.name,
kind: monaco.languages.CompletionItemKind.Text,
insertText: field.name,
range: range,
insertTextRules:
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
};
keywords.push(itemObj);
});
props.functions.forEach((field: any) => {
let itemObj = {
label: field.name,
kind: monaco.languages.CompletionItemKind.Text,
insertText: field.name + field.args,
range: range,
insertTextRules:
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
};
keywords.push(itemObj);
});
return keywords;
};
onMounted(async () => {
// editorRef.value.addEventListener("keyup", onKeyUp);
monaco.editor.defineTheme("myCustomTheme", {
base: "vs", // can also be vs-dark or hc-black
inherit: true, // can also be false to completely replace the builtin rules
rules: [{ token: "comment", background: "FFFFFF" }],
colors: {
"editor.foreground": "#000000",
"editor.background": "#fafafa",
"editorCursor.foreground": "#000000",
"editor.lineHighlightBackground": "#FFFFFF",
"editorLineNumber.foreground": "#ececec",
"editor.border": "#ececec",
},
});
monaco.languages.registerCompletionItemProvider("sql", {
provideCompletionItems: function (model, position) {
// find out if we are completing a property in the 'dependencies' object.
var textUntilPosition = model.getValueInRange({
startLineNumber: 1,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column,
});
var word = model.getWordUntilPosition(position);
var range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn,
};
let arr = textUntilPosition.trim().split(" ");
let filteredSuggestions = [];
filteredSuggestions = createDependencyProposals(range);
filteredSuggestions = filteredSuggestions.filter((item) => {
return item.label.toLowerCase().includes(word.word.toLowerCase());
});
// if (filteredSuggestions.length == 0) {
const lastElement = arr.pop();
filteredSuggestions.push({
label: `match_all('${lastElement}')`,
kind: monaco.languages.CompletionItemKind.Text,
insertText: `match_all('${lastElement}')`,
range: range,
});
filteredSuggestions.push({
label: `match_all_ignore_case('${lastElement}')`,
kind: monaco.languages.CompletionItemKind.Text,
insertText: `match_all_ignore_case('${lastElement}')`,
range: range,
});
return {
suggestions: filteredSuggestions,
};
// } else {
// return {
// suggestions: filteredSuggestions,
// };
// }
},
});
editorObj = monaco.editor.create(editorRef.value, {
value: props.query,
language: "sql",
theme: "myCustomTheme",
showFoldingControls: "never",
wordWrap: "on",
lineNumbers: "on",
lineNumbersMinChars: 0,
overviewRulerLanes: 0,
fixedOverflowWidgets: false,
overviewRulerBorder: false,
lineDecorationsWidth: 15,
hideCursorInOverviewRuler: true,
renderLineHighlight: "none",
glyphMargin: false,
folding: false,
scrollBeyondLastColumn: 0,
scrollBeyondLastLine: true,
scrollbar: { horizontal: "auto", vertical: "visible" },
find: {
addExtraSpaceOnTop: false,
autoFindInSelection: "never",
seedSearchStringFromSelection: "never",
},
minimap: { enabled: false },
});
editorObj.onDidChangeModelContent((e: any) => {
emit("update-query", editorObj.getValue());
});
editorObj.createContextKey("ctrlenter", true);
editorObj.addCommand(
monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,
function () {
emit("run-query");
},
"ctrlenter"
);
// editorObj.onDidBlurEditorWidget(() => {
// // onBlur();
// });
// // editorObj.dispose();
});
const setValue = (value: string) => {
if (editorObj?.setValue) editorObj.setValue(value);
};
return {
editorRef,
editorObj,
setValue,
};
},
});
</script>
<style>
#editor {
min-height: 4rem;
border-radius: 5px;
border: 0px solid #dbdbdb;
}
.monaco-editor,
.monaco-editor .monaco-editor {
padding: 0px 0px 0px 0px !important;
}
</style>
+431
View File
@@ -0,0 +1,431 @@
<!-- Copyright 2022 Zinc Labs Inc. and Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http:www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<template>
<div class="search-bar-component" id="searchBarComponent">
<!-- {{ searchObj.data }} -->
<div class="row q-my-xs">
<div class="float-right col">
<q-toggle
data-test="logs-search-bar-show-query-toggle-btn"
v-model="searchObj.meta.showQuery"
:label="t('search.showQueryLabel')"
/>
<q-toggle
data-test="logs-search-bar-show-fields-toggle-btn"
v-model="searchObj.meta.showFields"
:label="t('search.showFieldLabel')"
/>
<q-toggle
data-test="logs-search-bar-show-histogram-toggle-btn"
v-bind:disable="searchObj.meta.sqlMode"
v-model="searchObj.meta.showHistogram"
:label="t('search.showHistogramLabel')"
/>
<syntax-guide
data-test="logs-search-bar-sql-mode-toggle-btn"
:sqlmode="searchObj.meta.sqlMode"
></syntax-guide>
</div>
<div class="float-right col-auto">
<q-btn
v-if="searchObj.data.queryResults.hits"
class="q-mr-sm float-left download-logs-btn"
size="sm"
:disable="!searchObj.data.queryResults.hits.length"
icon="download"
title="Export logs"
@click="downloadLogs"
></q-btn>
<div class="float-left">
<date-time
data-test="logs-search-bar-date-time-dropdown"
@date-change="updateDateTime"
/>
</div>
<div class="search-time q-pl-sm float-left">
<q-btn
data-test="logs-search-bar-refresh-btn"
data-cy="search-bar-refresh-button"
dense
rounded="sm"
flat
title="Run query"
class="q-pa-none search-button"
@click="searchData"
:disable="
searchObj.loading || searchObj.data.streamResults.length == 0
"
>Run query</q-btn
>
</div>
</div>
</div>
<div class="row" v-if="searchObj.meta.showQuery">
<div class="col">
<query-editor
ref="queryEditorRef"
class="monaco-editor"
v-model:query="searchObj.data.query"
v-model:fields="searchObj.data.stream.selectedStreamFields"
v-model:functions="searchObj.data.stream.functions"
@update-query="updateQueryValue"
@run-query="searchData"
></query-editor>
</div>
</div>
</div>
</template>
<script lang="ts">
// @ts-nocheck
import { defineComponent, ref } from "vue";
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";
import { useQuasar } from "quasar";
import DateTime from "@/components/DateTime.vue";
import useTraces from "@/composables/useTraces";
import QueryEditor from "./QueryEditor.vue";
import SyntaxGuide from "./SyntaxGuide.vue";
import { Parser } from "node-sql-parser";
import segment from "@/services/segment_analytics";
import config from "@/aws-exports";
export default defineComponent({
name: "ComponentSearchSearchBar",
components: {
DateTime,
QueryEditor,
SyntaxGuide,
},
emits: ["searchdata"],
methods: {
searchData() {
if (this.searchObj.loading == false) {
// this.searchObj.runQuery = true;
this.$emit("searchdata");
}
},
},
setup() {
const router = useRouter();
const { t } = useI18n();
const $q = useQuasar();
const btnRefreshInterval = ref(null);
const { searchObj } = useTraces();
const queryEditorRef = ref(null);
const parser = new Parser();
let streamName = "";
const refreshTimeChange = (item) => {
searchObj.meta.refreshInterval = item.value;
searchObj.meta.refreshIntervalLabel = item.label;
btnRefreshInterval.value = false;
};
const updateQueryValue = (value: string) => {
searchObj.data.editorValue = value;
searchObj.data.query = value;
if (searchObj.meta.sqlMode == true) {
searchObj.data.parsedQuery = parser.astify(value);
if (searchObj.data.parsedQuery.from.length > 0) {
if (
searchObj.data.parsedQuery.from[0].table !==
searchObj.data.stream.selectedStream.value &&
searchObj.data.parsedQuery.from[0].table !== streamName
) {
let streamFound = false;
streamName = searchObj.data.parsedQuery.from[0].table;
searchObj.data.streamResults.list.forEach((stream) => {
if (stream.name == searchObj.data.parsedQuery.from[0].table) {
streamFound = true;
let itemObj = {
label: stream.name,
value: stream.name,
};
searchObj.data.stream.selectedStream = itemObj;
stream.schema.forEach((field) => {
searchObj.data.stream.selectedStreamFields.push({
name: field.name,
});
});
}
});
if (streamFound == false) {
searchObj.data.stream.selectedStream = { label: "", value: "" };
searchObj.data.stream.selectedStreamFields = [];
$q.notify({
message: "Stream not found",
color: "negative",
position: "top",
timeout: 2000,
});
}
}
}
}
};
const updateDateTime = (value: object) => {
searchObj.data.datetime = value;
if (config.isZincObserveCloud == "true" && value.userChangedValue) {
let dateTimeVal;
if (value.tab === "relative") {
dateTimeVal = value.relative;
} else {
dateTimeVal = value.absolute;
}
segment.track("Button Click", {
button: "Date Change",
tab: value.tab,
value: dateTimeVal,
//user_org: this.store.state.selectedOrganization.identifier,
//user_id: this.store.state.userInfo.email,
stream_name: searchObj.data.stream.selectedStream.value,
page: "Search Logs",
});
}
};
const udpateQuery = () => {
// alert(searchObj.data.query);
if (queryEditorRef.value?.setValue)
queryEditorRef.value.setValue(searchObj.data.query);
};
const jsonToCsv = (jsonData) => {
const replacer = (key, value) => (value === null ? "" : value);
const header = Object.keys(jsonData[0]);
let csv = header.join(",") + "\r\n";
for (let i = 0; i < jsonData.length; i++) {
const row = header
.map((fieldName) => JSON.stringify(jsonData[i][fieldName], replacer))
.join(",");
csv += row + "\r\n";
}
return csv;
};
const downloadLogs = () => {
const filename = "logs-data.csv";
const data = jsonToCsv(searchObj.data.queryResults.hits);
const file = new File([data], filename, {
type: "text/csv",
});
const url = URL.createObjectURL(file);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
return {
t,
router,
searchObj,
queryEditorRef,
btnRefreshInterval,
refreshTimes: searchObj.config.refreshTimes,
refreshTimeChange,
updateQueryValue,
updateDateTime,
udpateQuery,
downloadLogs,
};
},
computed: {
addSearchTerm() {
return this.searchObj.data.stream.addToFilter;
},
},
watch: {
addSearchTerm() {
if (this.searchObj.data.stream.addToFilter != "") {
let currentQuery = this.searchObj.data.editorValue.split("|");
let filter = this.searchObj.data.stream.addToFilter;
const isFilterValueNull = filter.split(/=|!=/)[1] === "'null'";
if (isFilterValueNull) {
filter = filter
.replace(/=|!=/, (match) => {
return match === "=" ? " is " : " is not ";
})
.replace(/'null'/, "null");
}
if (currentQuery.length > 1) {
if (currentQuery[1].trim() != "") {
currentQuery[1] += " and " + filter;
} else {
currentQuery[1] = filter;
}
this.searchObj.data.query = currentQuery.join("| ");
} else {
if (currentQuery != "") {
currentQuery += " and " + filter;
} else {
currentQuery = filter;
}
this.searchObj.data.query = currentQuery;
}
this.searchObj.data.stream.addToFilter = "";
if (this.queryEditorRef?.setValue)
this.queryEditorRef.setValue(this.searchObj.data.query);
}
},
},
});
</script>
<style lang="scss">
.search-bar-component {
border-bottom: 1px solid #e0e0e0;
padding-bottom: 1px;
.q-toggle__inner {
font-size: 30px;
}
.q-toggle__label {
font-size: 12px;
}
.casesensitive-btn {
padding: 8px;
margin-left: -6px;
background-color: #d5d5d5;
border-radius: 0px 3px 3px 0px;
}
.search-field .q-field {
&__control {
border-radius: 3px 0px 0px 3px !important;
}
&__native {
font-weight: 600;
}
}
.search-time {
// width: 120px;
margin-right: 10px;
.q-btn-group {
border-radius: 3px;
.q-btn {
min-height: auto;
}
}
}
.search-dropdown {
padding: 0px;
.block {
color: $dark-page;
font-weight: 600;
font-size: 12px;
}
.q-btn-dropdown__arrow-container {
color: $light-text2;
}
}
.refresh-rate-dropdown-container {
width: 220px;
* .q-btn {
font-size: 12px !important;
padding-left: 8px;
padding-right: 8px;
}
}
.flex-start {
justify-content: flex-start;
align-items: flex-start;
display: flex;
}
.resultsOverChart {
margin-bottom: 0.75rem;
font-size: 0.875rem;
color: $dark-page;
font-weight: 700;
}
.ddlWrapper {
position: relative;
z-index: 10;
.listWrapper {
box-shadow: 0px 3px 15px rgba(0, 0, 0, 0.1);
transition: height 0.25s ease;
height: calc(100vh - 146px);
background-color: white;
position: absolute;
top: 2.75rem;
width: 100%;
left: 0;
&:empty {
height: 0;
}
&,
.q-list {
border-radius: 3px;
}
}
}
.fields_autocomplete {
max-height: 250px;
}
.monaco-editor {
width: 100% !important;
height: 70px !important;
}
.search-button {
width: 96px;
line-height: 29px;
font-weight: bold;
text-transform: initial;
font-size: 11px;
color: white;
.q-btn__content {
background: $primary;
border-radius: 0px 3px 3px 0px;
.q-icon {
font-size: 15px;
color: #ffffff;
}
}
}
.download-logs-btn {
height: 30px;
}
}
</style>
+512
View File
@@ -0,0 +1,512 @@
<!-- Copyright 2022 Zinc Labs Inc. and Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http:www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- eslint-disable vue/v-on-event-hyphenation -->
<!-- eslint-disable vue/attribute-hyphenation -->
<template>
<div class="col column oveflow-hidden">
<div class="search-list" style="width: 100%">
<trace-chart
data-test="logs-search-result-bar-chart"
ref="plotChart"
id="traces_scatter_chart"
v-show="searchObj.meta.showHistogram"
:chart="searchObj.data.histogram"
/>
<q-virtual-scroll
data-test="logs-search-result-logs-table"
id="tracesSearchGridComponent"
type="table"
ref="searchTableRef"
:virtual-scroll-item-size="25"
:virtual-scroll-sticky-size-start="0"
:virtual-scroll-sticky-size-end="0"
:virtual-scroll-slice-size="300"
:virtual-scroll-slice-ratio-before="10"
:items="searchObj.data.queryResults.hits"
@virtual-scroll="onScroll"
>
<template v-slot:before>
<thead class="thead-sticky text-left">
<tr>
<th
v-for="(col, index) in searchObj.data.resultGrid.columns"
:key="'result_' + index"
class="table-header"
:data-test="`log-search-result-table-th-${col.label}`"
>
<q-chip
v-if="col.closable"
:data-test="`logs-search-result-table-th-remove-${col.label}-btn`"
:icon-remove="
'img:' + getImageURL('images/common/close_icon.svg')
"
class="q-ma-none table-head-chip"
removable
square
@remove="closeColumn(col)"
>
{{ col.label }}
</q-chip>
<span v-else class="table-head-label">
{{ col.label }}
</span>
</th>
</tr>
</thead>
</template>
<template v-slot="{ item: row, index }">
<q-tr
:data-test="`logs-search-result-detail-${
row[store.state.zoConfig.timestamp_column]
}`"
:key="'expand_' + index"
@click="expandRowDetail(row, index)"
style="cursor: pointer"
:style="
row[store.state.zoConfig.timestamp_column] ==
searchObj.data.searchAround.indexTimestamp
? 'background-color:lightgray'
: ''
"
>
<q-td
v-for="column in searchObj.data.resultGrid.columns"
:key="index + '-' + column.name"
class="field_list"
>
<high-light
:content="
column.name == 'source'
? column.prop(row)
: column.prop(row, column.name).length > 100
? column.prop(row, column.name).substr(0, 100) + '...'
: column.name === 'duration'
? column.format(row[column.name])
: column.name != '@timestamp'
? row[column.name]
: column.prop(row, column.name)
"
:query-string="
searchObj.meta.sqlMode
? searchObj.data.query.split('where')[1]
: searchObj.data.query
"
:title="
column.prop(row, column.name).length > 100 &&
column.name != 'source'
? column.prop(row, column.name)
: ''
"
></high-light>
<div
v-if="column.closable && row[column.name]"
class="field_overlay"
:title="row.name"
>
<q-icon
:name="'img:' + getImageURL('images/common/add_icon.svg')"
size="1rem"
title="Add to search query"
@click.prevent.stop="
addSearchTerm(`${column.name}='${row[column.name]}'`)
"
/>
<q-icon
:name="'img:' + getImageURL('images/common/remove_icon.svg')"
size="1rem"
title="Add to search query"
@click.prevent.stop="
addSearchTerm(`${column.name}!='${row[column.name]}'`)
"
/>
</div>
</q-td> </q-tr
></template>
</q-virtual-scroll>
<q-dialog
v-model="searchObj.meta.showTraceDetails"
position="right"
full-height
full-width
maximized
@hide="closeTraceDetails"
>
<trace-details />
</q-dialog>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, nextTick, ref } from "vue";
import { useQuasar, date } from "quasar";
import { useStore } from "vuex";
import { useI18n } from "vue-i18n";
import HighLight from "../../components/HighLight.vue";
import { byString } from "../../utils/json";
import useTraces from "../../composables/useTraces";
import { getImageURL } from "../../utils/zincutils";
import TraceDetails from "./TraceDetails.vue";
import TraceChart from "./TraceChart.vue";
import { redraw } from "plotly.js";
import { cloneDeep } from "lodash";
export default defineComponent({
name: "SearchResult",
components: {
HighLight,
TraceDetails,
TraceChart,
},
emits: [
"update:scroll",
"update:datetime",
"remove:searchTerm",
"search:timeboxed",
"get:traceDetails",
],
methods: {
closeColumn(col: any) {
const RGIndex = this.searchObj.data.resultGrid.columns.indexOf(col.name);
this.searchObj.data.resultGrid.columns.splice(RGIndex, 1);
const SFIndex = this.searchObj.data.stream.selectedFields.indexOf(
col.name
);
this.searchObj.data.stream.selectedFields.splice(SFIndex, 1);
this.searchObj.organizationIdetifier =
this.store.state.selectedOrganization.identifier;
this.updatedLocalLogFilterField();
},
onChartUpdate({ start, end }: { start: any; end: any }) {
this.searchObj.meta.showDetailTab = false;
this.searchObj.runQuery = true;
this.searchObj.data.datetime.tab = "absolute";
this.searchObj.data.datetime.absolute.date.from = start.split(" ")[0];
this.searchObj.data.datetime.absolute.date.to = end.split(" ")[0];
this.searchObj.data.datetime.absolute.startTime =
start.split(" ")[1].split(":")[0] +
":" +
start.split(" ")[1].split(":")[1] +
":" +
start.split(" ")[1].split(":")[2];
this.searchObj.data.datetime.absolute.endTime =
end.split(" ")[1].split(":")[0] +
":" +
end.split(" ")[1].split(":")[1] +
":" +
end.split(" ")[1].split(":")[2];
this.searchObj.runQuery = true;
this.$emit("update:datetime");
},
onScroll(info: any) {
this.searchObj.meta.scrollInfo = info;
if (
info.ref.items.length / info.index <= 2 &&
this.searchObj.loading == false &&
this.searchObj.data.resultGrid.currentPage <=
this.searchObj.data.queryResults.from /
this.searchObj.meta.resultGrid.rowsPerPage
) {
this.searchObj.data.resultGrid.currentPage += 1;
this.$emit("update:scroll");
}
},
onTimeBoxed(obj: any) {
this.searchObj.meta.showDetailTab = false;
this.searchObj.data.searchAround.indexTimestamp = obj.key;
this.$emit("search:timeboxed", obj);
},
},
setup(props, { emit }) {
// Accessing nested JavaScript objects and arrays by string path
// https://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-and-arrays-by-string-path
const { t } = useI18n();
const store = useStore();
const $q = useQuasar();
const showTraceDetails = ref(true);
const { searchObj, updatedLocalLogFilterField } = useTraces();
const totalHeight = ref(0);
const searchTableRef: any = ref(null);
const plotChart: any = ref(null);
const reDrawChart = () => {
if (
// eslint-disable-next-line no-prototype-builtins
searchObj.data.histogram.data &&
searchObj.data.histogram.layout
) {
nextTick(() => {
plotChart.value.reDraw();
plotChart.value.forceReLayout();
});
}
};
const changeMaxRecordToReturn = (val: any) => {
// searchObj.meta.resultGrid.pagination.rowsPerPage = val;
};
const expandRowDetail = (props: any, index: number) => {
searchObj.meta.showTraceDetails = true;
emit("get:traceDetails", props.trace_id);
};
const getRowIndex = (next: boolean, prev: boolean, oldIndex: number) => {
if (next) {
return oldIndex + 1;
} else {
return oldIndex - 1;
}
};
const navigateRowDetail = (isNext: boolean, isPrev: boolean) => {
const newIndex = getRowIndex(
isNext,
isPrev,
Number(searchObj.meta.resultGrid.navigation.currentRowIndex)
);
searchObj.meta.resultGrid.navigation.currentRowIndex = newIndex;
};
const addSearchTerm = (term: string) => {
// searchObj.meta.showDetailTab = false;
searchObj.data.stream.addToFilter = term;
};
const removeSearchTerm = (term: string) => {
emit("remove:searchTerm", term);
};
const closeTraceDetails = () => {
searchObj.meta.showTraceDetails = false;
searchObj.data.traceDetails.showSpanDetails = false;
searchObj.data.traceDetails.selectedSpanId = null;
};
return {
t,
store,
plotChart,
searchObj,
updatedLocalLogFilterField,
byString,
searchTableRef,
addSearchTerm,
removeSearchTerm,
expandRowDetail,
changeMaxRecordToReturn,
navigateRowDetail,
totalHeight,
reDrawChart,
getImageURL,
showTraceDetails,
closeTraceDetails,
};
},
});
</script>
<style lang="scss" scoped>
.max-result {
width: 170px;
}
.search-list {
width: 100%;
.chart {
width: 100%;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
.my-sticky-header-table {
.q-table__top,
.q-table__bottom,
thead tr:first-child th {
/* bg color is important for th; just specify one */
background-color: white;
}
thead tr th {
position: sticky;
z-index: 1;
}
thead tr:first-child th {
top: 0;
}
/* this is when the loading indicator appears */
&.q-table--loading thead tr:last-child th {
/* height of all previous header rows */
top: 48px;
}
}
.q-table__top {
padding-left: 0;
padding-top: 0;
}
.q-table thead tr,
.q-table tbody td,
.q-table th,
.q-table td {
height: 25px;
padding: 0px 5px;
font-size: 0.75rem;
}
.q-table__bottom {
width: 100%;
}
.q-table__bottom {
min-height: 40px;
padding-top: 0;
padding-bottom: 0;
}
.q-td {
overflow: hidden;
min-width: 100px;
.expanded {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-all;
}
}
.highlight {
background-color: rgb(255, 213, 0);
}
.table-header {
// text-transform: capitalize;
.table-head-chip {
background-color: $accent;
padding: 0px;
.q-chip__content {
margin-right: 0.5rem;
font-size: 0.75rem;
color: $dark;
}
.q-chip__icon--remove {
height: 1rem;
width: 1rem;
opacity: 1;
margin: 0;
&:hover {
opacity: 0.7;
}
}
.q-table th.sortable {
cursor: pointer;
text-transform: capitalize;
font-weight: bold;
}
}
&.isClosable {
padding-right: 26px;
position: relative;
.q-table-col-close {
transform: translateX(26px);
position: absolute;
margin-top: 2px;
color: grey;
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
}
}
.q-table th.sortable {
cursor: pointer;
text-transform: capitalize;
font-weight: bold;
}
}
}
.thead-sticky tr > *,
.tfoot-sticky tr > * {
position: sticky;
opacity: 1;
z-index: 1;
background: #f5f5f5;
color: #090909;
}
.thead-sticky tr:last-child > * {
top: 0;
}
.tfoot-sticky tr:first-child > * {
bottom: 0;
}
.field_list {
padding: 0px;
margin-bottom: 0.125rem;
position: relative;
overflow: visible;
cursor: default;
.field_overlay {
position: absolute;
height: 100%;
right: 0;
top: 0;
background-color: #ffffff;
border-radius: 6px;
padding: 0 6px;
visibility: hidden;
display: flex;
align-items: center;
transition: all 0.3s linear;
.q-icon {
cursor: pointer;
opacity: 0;
transition: all 0.3s linear;
margin: 0 1px;
}
}
&:hover {
.field_overlay {
visibility: visible;
.q-icon {
opacity: 1;
}
}
}
}
</style>
+203
View File
@@ -0,0 +1,203 @@
<!-- Copyright 2022 Zinc Labs Inc. and Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http:www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<template>
<div
class="flex justify-between items-center cursor-pointer span-block relative-position"
:style="{
width: '100%',
height: spanDimensions.height + 'px',
}"
:class="!isSpanSelected ? 'defocus' : ''"
ref="spanBlock"
@click="selectSpan"
>
<div
:style="{
width: '100%',
overflow: 'hidden',
}"
class="cursor-pointer flex items-center no-wrap position-relative"
:class="!isSpanSelected ? 'defocus' : ''"
@click="selectSpan"
>
<div
:style="{
width: getWidth + '%',
height: spanDimensions.barHeight + 'px',
borderRadius: '2px',
left: getLeftPosition + '%',
position: 'relative',
backgroundColor: span.style?.color || '#58508d',
zIndex: 1,
}"
/>
<div
:style="{
position: 'absolute',
...getDurationStyle,
transition: 'all 0.5s ease',
zIndex: 1,
}"
class="text-caption"
>
{{ formatTimeWithSuffix(span.durationMs) }}
</div>
<q-resize-observer debounce="300" @resize="onResize" />
</div>
</div>
</template>
<script lang="ts">
import {
defineComponent,
computed,
ref,
watch,
onMounted,
watchEffect,
} from "vue";
import useTraces from "@/composables/useTraces";
import { getImageURL } from "@/utils/zincutils";
export default defineComponent({
name: "SpanBlock",
props: {
span: {
type: Object,
default: () => null,
},
baseTracePosition: {
type: Object,
default: () => null,
},
depth: {
type: Number,
default: 0,
},
styleObj: {
type: Object,
default: () => {},
},
showCollapse: {
type: Boolean,
default: true,
},
isCollapsed: {
type: Boolean,
default: false,
},
spanDimensions: {
type: Object,
default: () => {},
},
},
emits: ["toggleCollapse"],
setup(props, { emit }) {
const { searchObj } = useTraces();
const spanBlock: any = ref(null);
const spanBlockWidth = ref(0);
const onePixelPercent = ref(0);
function formatTimeWithSuffix(ns: number) {
if (ns < 10000) {
return `${ns}ms`;
} else {
return `${(ns / 1000).toFixed(2)}s`;
}
}
const isSpanSelected = computed(() => {
if (!searchObj.data.traceDetails.selectedSpanId) return true;
return searchObj.data.traceDetails.selectedSpanId === props.span.spanId;
});
const selectSpan = () => {
searchObj.data.traceDetails.showSpanDetails = true;
searchObj.data.traceDetails.selectedSpanId = props.span.spanId;
};
const toggleSpanCollapse = () => {
emit("toggleCollapse", props.span.spanId);
};
const getLeftPosition = computed(() => {
const left =
props.span.startTimeMs - props.baseTracePosition["startTimeMs"];
return (left / props.baseTracePosition?.durationMs) * 100;
});
const getWidth = computed(() => {
return Number(
(
(props.span?.durationMs / props.baseTracePosition?.durationMs) *
100
).toFixed(2)
);
});
const getDurationStyle = computed(() => {
const style: any = {
top: "2px",
};
const onePercent = Number((spanBlockWidth.value / 100).toFixed(2));
const labelWidth = 60;
if (
(getLeftPosition.value + getWidth.value) * onePercent + labelWidth >
spanBlockWidth.value
) {
style.right = 0;
style.top = "-8px";
} else if (getLeftPosition.value > 50) {
style.left = getLeftPosition.value * onePercent - labelWidth + "px";
} else {
style.left =
(getLeftPosition.value + getWidth.value) * onePercent + 10 + "px";
}
return style;
});
const onResize = () => {
spanBlockWidth.value = spanBlock.value.clientWidth;
};
return {
formatTimeWithSuffix,
selectSpan,
isSpanSelected,
toggleSpanCollapse,
getImageURL,
getLeftPosition,
getWidth,
getDurationStyle,
spanBlock,
onResize,
onePixelPercent,
};
},
});
</script>
<style scoped lang="scss">
.defocus {
opacity: 0.3;
}
.collapse-btn {
background-color: #ffffff;
opacity: 0.4;
}
.collapse-container {
border: 1px solid #ececec;
}
.light-grey {
background-color: #ececec;
}
</style>
+97
View File
@@ -0,0 +1,97 @@
<!-- Copyright 2022 Zinc Labs Inc. and Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http:www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<template>
<div class="relative-position q-pt-sm" :style="{ height: '100%' }">
<template v-if="spans?.length">
<template v-for="span in spans as any[]" :key="span.spanId">
<span-block
:span="span"
:depth="depth"
:baseTracePosition="baseTracePosition"
:styleObj="{
position: 'absolute',
top: span.style.top,
left: span.style.left,
height: '60px',
}"
:spanDimensions="spanDimensions"
:isCollapsed="collapseMapping[span.spanId]"
@toggle-collapse="toggleSpanCollapse"
/>
</template>
</template>
</div>
</template>
<script lang="ts">
import { defineComponent, onActivated, onBeforeMount, ref } from "vue";
import SpanBlock from "./SpanBlock.vue";
export default defineComponent({
name: "SpanRenderer",
props: {
spans: {
type: Array,
default: () => [],
},
isCollapsed: {
type: Boolean,
default: false,
},
collapseMapping: {
type: Object,
default: () => {},
},
baseTracePosition: {
type: Object,
default: () => null,
},
depth: {
type: Number,
default: 0,
},
spanDimensions: {
type: Object,
default: () => {},
},
},
emits: ["toggleCollapse"],
setup(props, { emit }) {
function formatTimeWithSuffix(ns: number) {
if (ns < 10000) {
return `${ns}ms`;
} else {
return `${(ns / 1000).toFixed(2)}s`;
}
}
function toggleSpanCollapse(spanId: number | string) {
emit("toggleCollapse", spanId);
}
return {
formatTimeWithSuffix,
toggleSpanCollapse,
};
},
components: { SpanBlock },
});
</script>
<style scoped lang="scss">
.spans-container {
position: relative;
}
</style>
+227
View File
@@ -0,0 +1,227 @@
<!-- Copyright 2022 Zinc Labs Inc. and Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http:www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<template>
<q-btn
data-cy="syntax-guide-button"
size="sm"
dense
flat
class="q-ml-sm q-pa-xs syntax-guide-button"
:class="sqlmode ? 'sql-mode' : 'normal-mode'"
label="Syntax Guide"
icon="help"
>
<q-menu>
<q-card flat v-if="!sqlmode">
<q-card-section class="syntax-guide-title">
<div class="label">Syntax Guide</div>
</q-card-section>
<q-separator />
<q-card-section class="q-pt-none answers">
<div class="syntax-section">
<div class="syntax-guide-text">
<ul class="guide-list">
<li>
For full text search of value 'error' use
<span class="bg-lightgray"
>match_all('error') in query editor</span
>
</li>
<li>
For case-insensitive full text search of value 'error' use
<span class="bg-lightgray"
>match_all_ignore_case('error')</span
>
</li>
<li>
For column search of value 'error' use
<span class="bg-lightgray"
>str_match(<b>fieldname</b>, 'error')</span
>
</li>
<li>
To search value 200 for code column use
<span class="bg-lightgray">code=200</span>
</li>
<li>
To search value 'stderr' for stream column use
<span class="bg-lightgray">stream='stderr'</span>
</li>
<li>
To search and use query function <i>extract_ip</i> on cloumn
log use
<span class="bg-lightgray">extract_ip(log) | code=200</span>
</li>
<li>
For additional examples,
<a
href="https://docs.zinc.dev/example-queries/"
target="_blank"
>click here</a
>.
</li>
</ul>
</div>
</div>
</q-card-section>
</q-card>
<q-card flat v-else>
<q-card-section class="syntax-guide-title">
<div class="label">Syntax Guide: SQL Mode</div>
</q-card-section>
<q-separator />
<q-card-section class="q-pt-none answers">
<div class="syntax-section">
<div class="syntax-guide-text">
<ul class="guide-list">
<li>
For full text search of value 'error' use
<span class="bg-lightgray"
>SELECT * FROM <b>stream</b> WHERE match_all('error')</span
>
</li>
<li>
For case-insensitive full text search of value 'error' use
<span class="bg-lightgray"
>SELECT * FROM <b>stream</b> WHERE
match_all_ignore_case('error')</span
>
</li>
<li>
For column search of value 'error' use
<span class="bg-lightgray"
>SELECT * FROM <b>stream</b> WHERE
str_match(<b>fieldname</b>, 'error')</span
>
</li>
<li>
To search value 200 for code column use
<span class="bg-lightgray"
>SELECT * FROM <b>stream</b> WHERE code=200</span
>
</li>
<li>
To search value 'stderr' for stream column use
<span class="bg-lightgray"
>SELECT * FROM <b>stream</b> WHERE stream='stderr'</span
>
</li>
<li>
To search and use query function <i>extract_ip</i> on cloumn
log use
<span class="bg-lightgray"
>SELECT extract_ip(log) FROM <b>stream</b> WHERE
code=200</span
>
</li>
<li>
For additional examples,
<a
href="https://docs.zinc.dev/example-queries/"
target="_blank"
>click here</a
>.
</li>
</ul>
</div>
</div>
</q-card-section>
</q-card>
</q-menu>
</q-btn>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { useI18n } from "vue-i18n";
export default defineComponent({
name: "ComponentSearchSyntaxGuide",
props: {
sqlmode: {
type: Boolean,
default: false,
},
},
setup() {
const { t } = useI18n();
return {
t,
};
},
});
</script>
<style lang="scss" scoped>
.guide-list {
padding: 0 10px;
margin: 10px 0 0 0;
}
.guide-list li {
font-size: 14px;
line-height: 23px;
}
.q-btn:before {
border: 0px solid #d5d5d5;
}
.syntax-guide-button {
cursor: pointer;
text-transform: capitalize;
padding: 5px 5px;
font-weight: bold;
border: 1px solid rgba(89, 96, 178, 0.3);
}
.normal-mode {
background-color: rgba(0, 0, 0, 0.05);
color: #202224;
}
.sql-mode {
background-color: rgba(89, 96, 178, 0.8);
color: #ffffff;
}
.syntax-guide-title {
width: 420px;
.label {
font-size: 15px;
font-weight: bold;
}
}
.syntax-guide-sub-title {
color: $primary;
font-size: 15px;
margin-left: 5px;
}
.syntax-guide-text {
font-size: 12px;
margin-left: 5px;
}
.syntax-section {
margin-bottom: 5px;
}
.bg-lightgray {
background-color: $selected-list-bg;
padding-left: 5px;
padding-right: 5px;
}
</style>
+215
View File
@@ -0,0 +1,215 @@
<!-- Copyright 2022 Zinc Labs Inc. and Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http:www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<template>
<div ref="plotref" :id="id" style="width: 100%" />
</template>
<script lang="ts">
import Plotly from "plotly.js";
import { cloneDeep } from "lodash-es";
import { defineComponent, onMounted, onUpdated, ref } from "vue";
export default defineComponent({
name: "TraceChart",
emits: ["updated:chart"],
props: {
height: {
type: Number,
default: 300,
},
width: {
type: Number,
default: 600,
},
chart: {
type: Object,
default: () => {},
},
id: {
type: String,
default: "",
},
},
setup(props, { emit }) {
const plotref: any = ref(null);
const zoomFlag: any = ref(false);
let traces: any = [{}];
let layout: any = {
width: "100%",
scrollZoom: true,
title: {
text: "Trace",
font: {
size: 12,
},
},
font: { size: 12 },
autosize: true,
height: 200,
margin: {
l: 32,
r: 16,
t: 32,
b: 32,
},
xaxis: {
ticklen: 5,
nticks: 10,
type: "-",
ticksuffix: "ms",
showgrid: true,
zeroline: true,
rangeslider: {
visible: true,
},
},
yaxis: {
showgrid: false,
zeroline: true,
autorange: true,
showticklabels: false,
},
shapes: [
{
x0: "5",
x1: "10",
y0: 0,
y1: 0.4,
line: {
width: 0,
},
type: "rect",
xref: "x",
yref: "y",
opacity: 1,
fillcolor: "rgb(0, 0, 0)",
},
{
x0: "2",
x1: "19",
y0: 0.4,
y1: 0.8,
line: {
width: 0,
},
type: "rect",
xref: "x",
yref: "y",
opacity: 1,
fillcolor: "rgb(139, 0, 0)",
},
{
x0: "0",
x1: "20",
y0: 0.8,
y1: 1.2,
line: {
width: 0,
},
type: "rect",
xref: "x",
yref: "y",
opacity: 1,
fillcolor: "rgb(0, 0, 0)",
},
],
hovermode: "closest",
showlegend: true,
};
onMounted(async () => {
layout = props.chart.layout || {};
traces = props.chart.data || [{}];
await Plotly.newPlot(props.id, traces, layout, {
responsive: true,
displayModeBar: false,
});
plotref.value.on("plotly_relayout", onPlotZoom);
});
onUpdated(async () => {
const update: any = {
"xaxis.autorange": true,
"yaxis.autorange": true,
};
if (document.getElementById(props.id) != null) {
Plotly.relayout(plotref.value, update);
}
});
const reDraw = () => {
traces = props.chart.data;
layout = props.chart.layout;
if (document.getElementById(props.id) != null) {
Plotly.react(props.id, traces, layout);
}
};
// created force relayout function to avoid infinite loop
const forceReLayout = (yaxis: any, flag = true) => {
zoomFlag.value = flag;
const update: any = {
"xaxis.autorange": true,
"yaxis.autorange": true,
};
if (document.getElementById(props.id) != null) {
Plotly.relayout(plotref.value, update);
}
};
const onPlotZoom = (e: any) => {
if (
e &&
e["xaxis.range[0]"] &&
e["xaxis.range[1]"] &&
zoomFlag.value == false
) {
const start = plotref.value.layout.xaxis.range[0];
const end = plotref.value.layout.xaxis.range[1];
if (start != "" && end != "") {
zoomFlag.value = true;
let start_d = new Date(start);
let end_d = new Date(end);
emit("updated:chart", {
data: e,
start: start_d.toLocaleString("sv-SE"),
end: end_d.toLocaleString("sv-SE"),
});
}
} else if (e["xaxis.range"]) {
emit("updated:chart", {
data: e,
});
} else {
emit("updated:chart", {
data: e,
});
zoomFlag.value = false;
}
};
return {
plotref,
reDraw,
onPlotZoom,
forceReLayout,
};
},
});
</script>
+546
View File
@@ -0,0 +1,546 @@
<!-- Copyright 2022 Zinc Labs Inc. and Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http:www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<template>
<div
class="trace-details"
:style="{
width: '97vw !important',
background: '#ffffff',
}"
>
<div
class="row q-px-sm"
v-if="traceTree.length && !searchObj.data.traceDetails.loading"
>
<div class="q-py-sm flex items-center justify-start col-12 toolbar">
<div class="text-h6 q-mr-lg">
{{ traceTree[0]["operationName"] }}
</div>
<div>Spans: {{ spanList.length - 1 }}</div>
</div>
<trace-chart
id="trace_details_gantt_chart"
ref="plotChart"
:chart="traceChart"
@updated:chart="updateChart"
/>
<div
:class="
isSidebarOpen ? 'histogram-container' : 'histogram-container-full'
"
>
<trace-header
:baseTracePosition="baseTracePosition"
:splitterWidth="splitterModel"
/>
<div class="histogram-spans-container">
<q-splitter v-model="splitterModel" :style="{ height: '100%' }">
<template v-slot:before>
<div class="trace-tree-container">
<trace-tree
:collapseMapping="collapseMapping"
:spans="spanPositionList"
:baseTracePosition="baseTracePosition"
:spanDimensions="spanDimensions"
class="trace-tree"
@toggle-collapse="toggleSpanCollapse"
/>
</div>
</template>
<template v-slot:after>
<SpanRenderer
:collapseMapping="collapseMapping"
:spans="spanPositionList"
:baseTracePosition="baseTracePosition"
:spanDimensions="spanDimensions"
ref="traceRootSpan"
/>
</template>
</q-splitter>
</div>
</div>
<q-separator vertical />
<div v-if="isSidebarOpen && selectedSpanId" class="histogram-sidebar">
<trace-details-sidebar
:span="spanMapping[selectedSpanId]"
@close="closeSidebar"
/>
</div>
</div>
<div
v-else-if="searchObj.data.traceDetails.loading"
class="flex column items-center justify-center"
:style="{ height: '100%' }"
>
<q-spinner color="primary" size="3em" :thickness="2" />
<div class="q-pt-sm">Getting trace data</div>
</div>
</div>
</template>
<script lang="ts">
import {
defineComponent,
ref,
type Ref,
onMounted,
watch,
onActivated,
} from "vue";
import { cloneDeep, range } from "lodash";
import SpanRenderer from "./SpanRenderer.vue";
import useTraces from "@/composables/useTraces";
import { computed } from "vue";
import TraceDetailsSidebar from "./TraceDetailsSidebar.vue";
import TraceTree from "./TraceTree.vue";
import TraceHeader from "./TraceHeader.vue";
import TraceChart from "./TraceChart.vue";
import { useStore } from "vuex";
export default defineComponent({
name: "TraceDetails",
props: {
traceId: {
type: String,
default: "",
},
},
components: {
SpanRenderer,
TraceDetailsSidebar,
TraceTree,
TraceHeader,
TraceChart,
},
setup() {
const traceTree: any = ref([]);
const spanMapping: any = ref({});
const { searchObj } = useTraces();
const baseTracePosition: any = ref({});
const collapseMapping: any = ref({});
const traceRootSpan: any = ref(null);
const spanPositionList: any = ref([]);
const splitterModel = ref(25);
const timeRange: any = ref({ start: 0, end: 0 });
const store = useStore();
const spanDimensions = {
height: 25,
barHeight: 8,
textHeight: 25,
gap: 15,
collapseHeight: 8,
collapseWidth: 8,
connectorPadding: 2,
paddingLeft: 8,
hConnectorWidth: 20,
dotConnectorWidth: 6,
dotConnectorHeight: 6,
colors: ["#b7885e", "#1ab8be", "#ffcb99", "#f89570", "#839ae2"],
};
const traceChart = ref({
data: [{}],
layout: {},
});
const plotChart: any = ref(null);
const spanList: any = computed(() => {
return searchObj.data.traceDetails.spanList;
});
onActivated(() => {
buildTracesTree();
});
onMounted(() => {
buildTracesTree();
});
watch(
() => spanList.value.length,
() => {
if (spanList.value.length) {
buildTracesTree();
} else traceTree.value = [];
},
{ immediate: true }
);
const isSidebarOpen = computed(() => {
return searchObj.data.traceDetails.showSpanDetails;
});
const selectedSpanId = computed(() => {
return searchObj.data.traceDetails.selectedSpanId;
});
const calculateTracePosition = () => {
const tics = [];
baseTracePosition.value["durationMs"] = timeRange.value.end;
baseTracePosition.value["startTimeMs"] =
traceTree.value[0].startTimeMs + timeRange.value.start;
const quarterMs = (timeRange.value.end - timeRange.value.start) / 4;
let time = timeRange.value.start;
for (let i = 0; i <= 4; i++) {
tics.push({
value: Number(time.toFixed(2)),
label: `${time.toFixed(2)}ms`,
left: `${25 * i}%`,
});
time += quarterMs;
}
baseTracePosition.value["tics"] = tics;
};
// Find out spans who has reference_parent_span_id as span_id of first span in sampleTrace
const buildTracesTree = async () => {
spanMapping.value = {};
traceTree.value = [];
spanPositionList.value = [];
collapseMapping.value = {};
let lowestStartTime: number = spanList.value[0].start_time;
let highestEndTime: number = spanList.value[0].end_time;
const traceTreeMock: any = {};
const serviceColorMapping: any = {};
if (!spanList.value?.length) return;
spanMapping.value[spanList.value[0].span_id] = spanList.value[0];
let colorIndex = 0;
let noParentSpans = [];
for (let i = 0; i < spanList.value.length; i++) {
if (spanList.value[i].start_time < lowestStartTime) {
lowestStartTime = spanList.value[i].start_time;
}
if (spanList.value[i].end_time > highestEndTime) {
highestEndTime = spanList.value[i].end_time;
}
spanMapping.value[spanList.value[i].span_id] = cloneDeep(
spanList.value[i]
);
const span = getFormattedSpan(spanList.value[i]);
if (span.serviceName && !serviceColorMapping[span.serviceName]) {
serviceColorMapping[span.serviceName] =
spanDimensions.colors[colorIndex];
colorIndex++;
if (colorIndex > spanDimensions.colors.length - 1) colorIndex = 0;
}
span.style.color = serviceColorMapping[span.serviceName];
span.index = i;
collapseMapping.value[span.spanId] = true;
if (span.parentId && !traceTreeMock[span.parentId]) {
noParentSpans.push(span);
}
// if (span.parentId && !traceTreeMock[span.parentId]) {
// traceTreeMock[span.parentId] = [];
// }
if (span.parentId && traceTreeMock[span.parentId])
traceTreeMock[span.parentId].push(span);
if (!traceTreeMock[span.spanId]) traceTreeMock[span.spanId] = [];
if (!span["spans"]) span["spans"] = traceTreeMock[span.spanId];
}
traceTree.value = [];
traceTree.value.push(getFormattedSpan(spanList.value[0]));
traceTree.value[0]["index"] = 0;
traceTree.value[0].lowestStartTime =
converTimeFromNsToMs(lowestStartTime);
traceTree.value[0].highestEndTime = converTimeFromNsToMs(highestEndTime);
traceTree.value[0].style.color =
serviceColorMapping[traceTree.value[0].serviceName];
traceTree.value[0]["spans"] = cloneDeep(
traceTreeMock[spanList.value[0]["span_id"]] || []
);
traceTree.value.push(...noParentSpans);
traceTree.value.forEach((span: any) => {
addSpansPositions(span, 0);
});
timeRange.value.end = (
traceTree.value[0].highestEndTime - traceTree.value[0].lowestStartTime
).toFixed(2);
timeRange.value.start = 0;
calculateTracePosition();
buildTraceChart();
};
let index = 0;
const addSpansPositions = (span: any, depth: number) => {
if (!span.index) index = 0;
span.depth = depth;
spanPositionList.value.push(
Object.assign(span, {
style: {
color: span.style.color,
top: index * spanDimensions.height + "px",
left: spanDimensions.gap * depth + "px",
},
hasChildSpans: !!span.spans.length,
currentIndex: index,
})
);
if (collapseMapping.value[span.spanId]) {
if (span.spans.length) {
span.spans.forEach((childSpan: any) => {
index = index + 1;
childSpan.totalSpans = addSpansPositions(childSpan, depth + 1);
});
span.totalSpans = span.spans.reduce(
(acc: number, span: any) =>
acc + ((span?.spans?.length || 0) + (span?.totalSpans || 0)),
0
);
}
return (span?.spans?.length || 0) + (span?.totalSpans || 0);
} else {
return 0;
}
};
// Convert span object to required format
// Converting ns to ms
const getFormattedSpan = (span: any) => {
return {
[store.state.zoConfig.timestamp_column]:
span[store.state.zoConfig.timestamp_column],
startTimeMs: converTimeFromNsToMs(span.start_time),
endTimeMs: converTimeFromNsToMs(span.end_time),
durationMs: convertTime(span.duration),
idleMs: convertTime(span.idle_ns),
busyMs: convertTime(span.busy_ns),
spanId: span.span_id,
operationName: span.operation_name,
serviceName: span.service_name,
spanKind: getSpanKind(span.span_kind.toString()),
parentId: span.reference_parent_span_id,
spans: null,
index: 0,
style: {
color: "",
},
};
};
const convertTime = (time: number) => {
return Number((time / 1000000).toFixed(2));
};
const converTimeFromNsToMs = (time: number) => {
const nanoseconds = time;
const milliseconds = Math.floor(nanoseconds / 1000000);
const date = new Date(milliseconds);
return date.getTime();
};
const getSpanKind = (spanKind: string) => {
const spanKindMapping: { [key: string]: string } = {
"1": "Client",
"2": "Server",
"3": "Producer",
"4": "Consumer",
"5": "Internal",
};
return spanKindMapping[spanKind];
};
const closeSidebar = () => {
searchObj.data.traceDetails.showSpanDetails = false;
searchObj.data.traceDetails.selectedSpanId = null;
};
const toggleSpanCollapse = (spanId: number | string) => {
collapseMapping.value[spanId] = !collapseMapping.value[spanId];
index = 0;
spanPositionList.value = [];
traceTree.value.forEach((span: any) => {
addSpansPositions(span, 0);
});
};
const buildTraceChart = () => {
const layout: any = {
autosize: true,
scrollZoom: true,
title: {
text: "",
font: {
size: 12,
},
},
font: { size: 12 },
height: 200,
margin: {
l: 16,
r: 16,
t: 16,
b: 16,
},
xaxis: {
ticklen: 5,
nticks: 10,
tickvals: [],
type: "-",
ticksuffix: "ms",
showgrid: true,
zeroline: true,
rangeslider: {
visible: true,
},
},
yaxis: {
showgrid: false,
zeroline: false,
autorange: true,
showticklabels: false,
},
shapes: [],
hovermode: "closest",
showlegend: true,
};
const shapes: any = [];
let size = 0;
for (let i = spanPositionList.value.length - 1; i > -1; i--) {
const absoluteStartTime =
spanPositionList.value[i].startTimeMs -
traceTree.value[0].lowestStartTime;
shapes.push({
x0: absoluteStartTime,
x1: Number(
(absoluteStartTime + spanPositionList.value[i].durationMs).toFixed(
2
)
),
y0: size,
y1: size + 0.5,
line: {
width: 0,
},
type: "rect",
xref: "x",
yref: "y",
opacity: 1,
fillcolor: spanPositionList.value[i].style.color,
});
size += 0.5;
}
baseTracePosition.value.tics.forEach((tic: any) => {
layout.xaxis.tickvals.push(tic.value);
});
layout.shapes = shapes;
traceChart.value.layout = layout;
if (plotChart.value) plotChart.value?.reDraw();
};
const updateChart = ({ data }: { data: any }) => {
let range1 = 0;
let range2 = 0;
if (data["xaxis.range[0]"] && data["xaxis.range[1]"]) {
range1 = data["xaxis.range[0]"];
range2 = data["xaxis.range[1]"];
} else if (data["xaxis.range"]?.length) {
range1 = data["xaxis.range"][0];
range2 = data["xaxis.range"][1];
} else {
timeRange.value.start = 0;
range2 = Number(
(
traceTree.value[0].highestEndTime -
traceTree.value[0].lowestStartTime
).toFixed(2)
);
}
timeRange.value.start = range1;
timeRange.value.end = range2;
calculateTracePosition();
};
return {
traceTree,
collapseMapping,
traceRootSpan,
baseTracePosition,
searchObj,
spanList,
isSidebarOpen,
selectedSpanId,
spanMapping,
closeSidebar,
toggleSpanCollapse,
spanPositionList,
spanDimensions,
splitterModel,
plotChart,
traceChart,
updateChart,
};
},
});
</script>
<style scoped lang="scss">
$sidebarWidth: 300px;
$seperatorWidth: 2px;
$toolbarHeight: 60px;
$traceHeaderHeight: 30px;
$traceChartHeight: 200px;
.toolbar {
height: $toolbarHeight;
}
.trace-details {
height: 100vh;
overflow: auto;
}
.histogram-container-full {
width: 100%;
}
.histogram-container {
width: calc(100% - $sidebarWidth - $seperatorWidth);
}
.histogram-sidebar {
width: $sidebarWidth;
height: calc(100vh - $toolbarHeight - $traceChartHeight);
overflow-y: scroll;
overflow-x: hidden;
}
.histogram-spans-container {
height: calc(100vh - $toolbarHeight - $traceHeaderHeight - $traceChartHeight);
overflow-y: auto;
position: relative;
overflow-x: hidden;
}
.trace-tree-container {
overflow: auto;
}
</style>
<style lang="scss">
.trace-details {
.q-splitter__before,
.q-splitter__after {
overflow: revert !important;
}
}
</style>
@@ -0,0 +1,111 @@
<!-- Copyright 2022 Zinc Labs Inc. and Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http:www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<template>
<div
class="flex justify-start items-center q-px-sm bg-grey-11"
:style="{ height: '30px' }"
>
<div :style="{ width: 'calc(100% - 22px)' }" class="q-pb-none ellipsis">
Span Details
</div>
<q-btn
dense
icon="close"
class="align-right no-border q-pa-xs"
size="xs"
@click="closeSidebar"
></q-btn>
</div>
<div class="q-pb-sm">
<div
:title="span.operation_name"
class="q-px-sm q-pb-none text-h6 ellipsis non-selectable"
>
{{ span.operation_name }}
</div>
<div
class="q-px-sm text-caption ellipsis non-selectable"
:title="span.service_name"
>
{{ span.service_name }}
</div>
</div>
<q-tabs
v-model="activeTab"
dense
inline-label
class="text-bold q-mx-sm span_details_tabs"
>
<q-tab name="tags" label="Attributes" style="text-transform: capitalize" />
</q-tabs>
<q-separator style="width: 100%" />
<q-tab-panels v-model="activeTab" class="span_details_tab-panels">
<q-tab-panel name="tags">
<div v-for="key in Object.keys(span)" :key="key">
<div class="row q-py-sm q-px-sm border-bottom">
<div class="col-12 text-subtitle2 text-grey-8">{{ key }}</div>
<div class="col-12 text-subtitle2">{{ span[key] }}</div>
</div>
</div>
</q-tab-panel>
</q-tab-panels>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
name: "TraceDetailsSidebar",
props: {
span: {
type: Object,
default: () => null,
},
},
emits: ["close"],
setup(props, { emit }) {
const activeTab = ref("tags");
const closeSidebar = () => {
emit("close");
};
return {
activeTab,
closeSidebar,
};
},
});
</script>
<style scoped lang="scss">
.span_details_tab-panels {
height: calc(100% - 130px);
overflow-y: scroll;
overflow-x: hidden;
}
</style>
<style lang="scss">
.span_details_tabs {
.span_details_tab-panels {
.q-tab-panel {
padding: 8px;
}
}
.q-tab__indicator {
display: none;
}
}
</style>
+105
View File
@@ -0,0 +1,105 @@
<!-- Copyright 2022 Zinc Labs Inc. and Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http:www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<template>
<div
:style="{ height: '30px', border: '1px solid #ececec' }"
class="flex justify-start items-center bg-grey-11"
>
<div
class="flex justify-start items-center no-wrap row q-px-sm bg-grey-11"
:style="{
width: splitterWidth + '%',
}"
>
Operation Name
</div>
<div
class="flex justify-start items-center no-wrap row bg-grey-11 relative-position"
:style="{
width: 100 - splitterWidth + '%',
}"
>
<div class="col-3 text-caption q-pl-xs">
{{ baseTracePosition.tics[0].label }}
</div>
<div class="col-3 text-caption q-pl-xs">
{{ baseTracePosition.tics[1].label }}
</div>
<div class="col-3 text-caption q-pl-xs">
{{ baseTracePosition.tics[2].label }}
</div>
<div class="col-3 text-caption flex justify-between items-center q-px-xs">
<div>{{ baseTracePosition.tics[3].label }}</div>
<div>{{ baseTracePosition.tics[4].label }}</div>
</div>
<div
v-for="tick in baseTracePosition['tics']"
class="trace-tic"
:key="tick.value"
:style="{
position: 'absolute',
left: tick.left,
top: '0px',
width: '1px',
backgroundColor: '#ececec',
}"
></div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "TraceNavbar",
props: {
baseTracePosition: {
type: Object,
default: () => null,
},
spanDimensions: {
type: Object,
default: () => {},
},
splitterWidth: {
type: Number,
default: 0,
},
},
setup() {
return {};
},
});
</script>
<style scoped lang="scss">
$toolbarHeight: 60px;
$traceHeaderHeight: 30px;
$traceChartHeight: 200px;
.spans-container {
position: relative;
}
.collapse-btn {
width: 10px;
height: 10px;
}
.trace-tic {
height: calc(100vh - $toolbarHeight - 5px - $traceChartHeight);
}
</style>
+133
View File
@@ -0,0 +1,133 @@
<!-- Copyright 2022 Zinc Labs Inc. and Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http:www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<template>
<div class="q-pl-xs q-pt-sm">
<template v-for="span in spans as any[]" :key="span.spanId">
<div :style="{ position: 'relative', width: '100%', overflow: 'hidden' }">
<div
:style="{
height: spanDimensions.textHeight - 8 + 'px',
margin: `4px 0px 4px ${
span.hasChildSpans
? span.style.left
: parseInt(span.style.left) +
spanDimensions.collapseWidth +
'px'
}`,
}"
class="flex items-center no-wrap justify-start ellipsis"
:title="span.operationName"
>
<div
v-if="span.hasChildSpans"
:style="{
width: spanDimensions.collapseWidth + 'px',
height: spanDimensions.collapseHeight + 'px',
}"
class="flex justify-center items-center collapse-container cursor-pointer"
@click.stop="toggleSpanCollapse(span.spanId)"
>
<q-icon
dense
round
flat
:name="'img:' + getImageURL('images/common/down-solid.svg')"
class="collapse-btn"
:style="{
rotate: collapseMapping[span.spanId] ? '0deg' : '270deg',
}"
/>
</div>
<div
class="text-blue-grey-6 ellipsis q-ml-xs"
:style="{
paddingLeft: '4px',
borderLeft: `3px solid ${span.style.color}`,
}"
>
{{ span.operationName }}
</div>
</div>
</div>
</template>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { getImageURL } from "@/utils/zincutils";
import { computed } from "vue";
export default defineComponent({
name: "TraceTree",
props: {
spans: {
type: Array,
default: () => [],
},
isCollapsed: {
type: Boolean,
default: false,
},
collapseMapping: {
type: Object,
default: () => {},
},
baseTracePosition: {
type: Object,
default: () => null,
},
depth: {
type: Number,
default: 0,
},
spanDimensions: {
type: Object,
default: () => {},
},
},
emits: ["toggleCollapse"],
setup(props, { emit }) {
function formatTimeWithSuffix(ns: number) {
if (ns < 10000) {
return `${ns} ms`;
} else {
return `${(ns / 1000).toFixed(2)} s`;
}
}
function toggleSpanCollapse(spanId: number | string) {
emit("toggleCollapse", spanId);
}
return {
formatTimeWithSuffix,
toggleSpanCollapse,
getImageURL,
};
},
});
</script>
<style scoped lang="scss">
.spans-container {
position: relative;
}
.collapse-btn {
width: 10px;
height: 10px;
}
</style>
+14 -7
View File
@@ -62,18 +62,25 @@ const stream = {
start_time,
end_time,
query_context,
query_fn
query_fn,
type,
}: any) => {
const fieldsString = fields.join(",");
let url = `/api/${org_identifier}/${stream_name}/_values?fields=${fieldsString}&size=${size}&start_time=${start_time}&end_time=${end_time}&sql=${query_context}`;
if (query_fn.trim() != "") {
url = url + `&query_fn=${query_fn}`;
}
let url = `/api/${org_identifier}/${stream_name}/_values?fields=${fieldsString}&size=${size}&start_time=${start_time}&end_time=${end_time}`;
if (query_context) url = url + `&sql=${query_context}`;
if (query_fn?.trim()) url = url + `&query_fn=${query_fn}`;
if (type) url += "&type=" + type;
return http().get(url);
},
delete: (org_identifier: string, stream_name: string, stream_type: string) => {
return http().delete(`/api/${org_identifier}/${stream_name}?type=${stream_type}`);
delete: (
org_identifier: string,
stream_name: string,
stream_type: string
) => {
return http().delete(
`/api/${org_identifier}/${stream_name}?type=${stream_type}`
);
},
};
+4
View File
@@ -172,6 +172,10 @@ export const useLocalLogFilterField = (val = "", isDelete = false) => {
return useLocalStorage("logFilterField", val, isDelete, true);
};
export const useLocalTraceFilterField = (val = "", isDelete = false) => {
return useLocalStorage("traceFilterField", val, isDelete, true);
};
export const useLocalUserInfo = (val = "", isDelete = false) => {
const userInfo: any = useLocalStorage("userInfo", val, isDelete);
return userInfo.value;
+21
View File
@@ -0,0 +1,21 @@
<!-- Copyright 2022 Zinc Labs Inc. and Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http:www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- eslint-disable vue/attribute-hyphenation -->
<!-- eslint-disable vue/v-on-event-hyphenation -->
<template>
<!-- <zinc-logs pageType="logs"></zinc-logs> -->
<zinc-traces></zinc-traces>
</template>