feat: Implement tags auto-complete for Trace and Log (#85)

This commit is contained in:
Fine0830 2022-05-12 17:00:07 +08:00 committed by GitHub
parent 0e0f2704b3
commit b492787027
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 244 additions and 84 deletions

View File

@ -63,3 +63,15 @@ export const QueryLogsByKeywords = {
query: ` query: `
support: supportQueryLogsByKeywords`, support: supportQueryLogsByKeywords`,
}; };
export const LogTagKeys = {
variable: "$duration: Duration!",
query: `
tagKeys: queryLogTagAutocompleteKeys(duration: $duration)`,
};
export const LogTagValues = {
variable: "$tagKey: String!, $duration: Duration!",
query: `
tagValues: queryLogTagAutocompleteValues(tagKey: $tagKey, duration: $duration)`,
};

View File

@ -74,3 +74,14 @@ export const TraceSpans = {
} }
`, `,
}; };
export const TraceTagKeys = {
variable: "$duration: Duration!",
query: `
tagKeys: queryTraceTagAutocompleteKeys(duration: $duration)`,
};
export const TraceTagValues = {
variable: "$tagKey: String!, $duration: Duration!",
query: `
tagValues: queryTraceTagAutocompleteValues(tagKey: $tagKey, duration: $duration)`,
};

View File

@ -19,9 +19,13 @@ import {
QueryBrowserErrorLogs, QueryBrowserErrorLogs,
QueryServiceLogs, QueryServiceLogs,
QueryLogsByKeywords, QueryLogsByKeywords,
LogTagValues,
LogTagKeys,
} from "../fragments/log"; } from "../fragments/log";
export const queryBrowserErrorLogs = `query queryBrowserErrorLogs(${QueryBrowserErrorLogs.variable}) { export const queryBrowserErrorLogs = `query queryBrowserErrorLogs(${QueryBrowserErrorLogs.variable}) {
${QueryBrowserErrorLogs.query}}`; ${QueryBrowserErrorLogs.query}}`;
export const queryServiceLogs = `query queryLogs(${QueryServiceLogs.variable}) {${QueryServiceLogs.query}}`; export const queryServiceLogs = `query queryLogs(${QueryServiceLogs.variable}) {${QueryServiceLogs.query}}`;
export const queryLogsByKeywords = `query queryLogsByKeywords {${QueryLogsByKeywords.query}}`; export const queryLogsByKeywords = `query queryLogsByKeywords {${QueryLogsByKeywords.query}}`;
export const queryLogTagValues = `query queryTagValues(${LogTagValues.variable}) {${LogTagValues.query}}`;
export const queryLogTagKeys = `query queryTagKeys(${LogTagKeys.variable}) {${LogTagKeys.query}}`;

View File

@ -15,8 +15,17 @@
* limitations under the License. * limitations under the License.
*/ */
import { Traces, TraceSpans } from "../fragments/trace"; import {
Traces,
TraceSpans,
TraceTagKeys,
TraceTagValues,
} from "../fragments/trace";
export const queryTraces = `query queryTraces(${Traces.variable}) {${Traces.query}}`; export const queryTraces = `query queryTraces(${Traces.variable}) {${Traces.query}}`;
export const queryTrace = `query queryTrace(${TraceSpans.variable}) {${TraceSpans.query}}`; export const queryTrace = `query queryTrace(${TraceSpans.variable}) {${TraceSpans.query}}`;
export const queryTraceTagKeys = `query queryTraceTagKeys(${TraceTagKeys.variable}) {${TraceTagKeys.query}}`;
export const queryTraceTagValues = `query queryTraceTagValues(${TraceTagValues.variable}) {${TraceTagValues.query}}`;

View File

@ -326,7 +326,7 @@ const msg = {
addExcludingKeywordsOfContent: "Please input a keyword of excluding content", addExcludingKeywordsOfContent: "Please input a keyword of excluding content",
noticeTag: "Please press Enter after inputting a tag(key=value).", noticeTag: "Please press Enter after inputting a tag(key=value).",
conditionNotice: conditionNotice:
"Notice: Please press Enter after inputting a tag, key of content, exclude key of content(key=value).", "Notice: Please press Enter after inputting a key of content, exclude key of content(key=value).",
language: "Language", language: "Language",
}; };
export default msg; export default msg;

View File

@ -327,7 +327,7 @@ const msg = {
addExcludingKeywordsOfContent: "请输入一个内容不包含的关键词", addExcludingKeywordsOfContent: "请输入一个内容不包含的关键词",
noticeTag: "请输入一个标签(key=value)之后回车", noticeTag: "请输入一个标签(key=value)之后回车",
conditionNotice: conditionNotice:
"请输入一个标签、内容关键词或者内容不包含的关键词(key=value)之后回车", "请输入一个内容关键词或者内容不包含的关键词(key=value)之后回车",
language: "语言", language: "语言",
}; };
export default msg; export default msg;

View File

@ -151,6 +151,20 @@ export const logStore = defineStore({
this.logsTotal = res.data.data.queryBrowserErrorLogs.total; this.logsTotal = res.data.data.queryBrowserErrorLogs.total;
return res.data; return res.data;
}, },
async getLogTagKeys() {
const res: AxiosResponse = await graphql
.query("queryLogTagKeys")
.params({ duration: useAppStoreWithOut().durationTime });
return res.data;
},
async getLogTagValues(tagKey: string) {
const res: AxiosResponse = await graphql
.query("queryLogTagValues")
.params({ tagKey, duration: useAppStoreWithOut().durationTime });
return res.data;
},
}, },
}); });

View File

@ -161,6 +161,20 @@ export const traceStore = defineStore({
this.traceSpanLogsTotal = res.data.data.queryLogs.total; this.traceSpanLogsTotal = res.data.data.queryLogs.total;
return res.data; return res.data;
}, },
async getTagKeys() {
const res: AxiosResponse = await graphql
.query("queryTraceTagKeys")
.params({ duration: useAppStoreWithOut().durationTime });
return res.data;
},
async getTagValues(tagKey: string) {
const res: AxiosResponse = await graphql
.query("queryTraceTagValues")
.params({ tagKey, duration: useAppStoreWithOut().durationTime });
return res.data;
},
}, },
}); });

View File

@ -13,65 +13,97 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<div class="flex-h" :class="{ light: theme === 'light' }"> <div>
<div class="mr-5"> <span class="grey">{{ t("tags") }}: </span>
<span class="sm grey" v-show="theme === 'dark'">{{ t("tags") }}: </span> <span
<span v-if="tagsList.length"
class="trace-tags" class="trace-tags"
:style="type === 'LOG' ? `min-width: 122px;` : ''" :style="type === 'LOG' ? `min-width: 122px;` : ''"
> >
<span class="selected" v-for="(item, index) in tagsList" :key="index"> <span class="selected" v-for="(item, index) in tagsList" :key="index">
<span>{{ item }}</span> <span>{{ item }}</span>
<span class="remove-icon" @click="removeTags(index)">×</span> <span class="remove-icon" @click="removeTags(index)">×</span>
</span>
</span> </span>
</span>
<el-input
v-if="type === 'ALARM'"
size="small"
v-model="tags"
class="trace-new-tag"
@change="addLabels"
:placeholder="t('addTags')"
/>
<span v-else>
<el-input <el-input
size="small" size="small"
v-model="tags" v-model="tags"
class="trace-new-tag" class="trace-new-tag"
@change="addLabels" @click="showClick"
:placeholder="t('addTags')"
/> />
<span class="tags-tip"> <el-dropdown
<a ref="dropdownTag"
target="blank" trigger="contextmenu"
href="https://github.com/apache/skywalking/blob/master/docs/en/setup/backend/configuration-vocabulary.md" :hide-on-click="false"
> style="margin: 20px 0 0 -130px"
{{ t("tagsLink") }} v-if="tagArr.length"
</a> >
<el-tooltip <template #dropdown>
:content=" <el-dropdown-menu>
t( <el-dropdown-item v-for="(item, index) in tagArr" :key="index">
type === 'LOG' <span @click="selectTag(item)" class="tag-item">
? 'logTagsTip' {{ item }}
: type === 'TRACE' </span>
? 'traceTagsTip' </el-dropdown-item>
: 'alarmTagsTip' </el-dropdown-menu>
) </template>
" </el-dropdown>
> </span>
<span> <span
<Icon class="icon-help mr-5" iconName="help" size="middle" /> class="tags-tip"
</span> :class="type !== 'ALARM' && tagArr.length ? 'link-tips' : ''"
</el-tooltip> >
<b v-if="type !== 'LOG'">{{ t("noticeTag") }}</b> <a
</span> target="blank"
</div> href="https://github.com/apache/skywalking/blob/master/docs/en/setup/backend/configuration-vocabulary.md"
>
{{ t("tagsLink") }}
</a>
<el-tooltip :content="t(tipsMap[type])">
<span>
<Icon class="icon-help mr-5" iconName="help" size="middle" />
</span>
</el-tooltip>
<b v-if="type === 'AL'">{{ t("noticeTag") }}</b>
</span>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue"; import { ref } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useTraceStore } from "@/store/modules/trace";
import { useLogStore } from "@/store/modules/log";
import { ElMessage } from "element-plus";
/*global defineEmits, defineProps */ /*global Nullable, defineEmits, defineProps */
const emit = defineEmits(["update"]); const emit = defineEmits(["update"]);
defineProps({ const props = defineProps({
type: { type: String, default: "TRACE" }, type: { type: String, default: "TRACE" },
}); });
const traceStore = useTraceStore();
const logStore = useLogStore();
const { t } = useI18n(); const { t } = useI18n();
const theme = ref<string>("dark");
const tags = ref<string>(""); const tags = ref<string>("");
const tagsList = ref<string[]>([]); const tagsList = ref<string[]>([]);
const tagArr = ref<string[]>([]);
const tagKeys = ref<string[]>([]);
const tipsMap = {
LOG: "logTagsTip",
TRACE: "traceTagsTip",
ALARM: "alarmTagsTip",
};
const dropdownTag = ref<Nullable<any>>(null);
fetchTagKeys();
function removeTags(index: number) { function removeTags(index: number) {
tagsList.value.splice(index, 1); tagsList.value.splice(index, 1);
@ -95,6 +127,55 @@ function updateTags() {
}); });
emit("update", { tagsMap, tagsList: tagsList.value }); emit("update", { tagsMap, tagsList: tagsList.value });
} }
async function fetchTagKeys() {
let resp: any = {};
if (props.type === "TRACE") {
resp = await traceStore.getTagKeys();
} else {
resp = await logStore.getLogTagKeys();
}
if (resp.errors) {
ElMessage.error(resp.errors);
return;
}
tagArr.value = resp.data.tagKeys;
tagKeys.value = resp.data.tagKeys;
}
async function fetchTagValues() {
const param = tags.value.split("=")[0];
let resp: any = {};
if (props.type === "TRACE") {
resp = await traceStore.getTagValues(param);
} else {
resp = await logStore.getLogTagValues(param);
}
if (resp.errors) {
ElMessage.error(resp.errors);
return;
}
tagArr.value = resp.data.tagValues;
}
function selectTag(item: string) {
if (tags.value.includes("=")) {
tags.value += item;
addLabels();
tagArr.value = tagKeys.value;
dropdownTag.value.handleClose();
return;
}
tags.value = item + "=";
fetchTagValues();
}
function showClick() {
if (dropdownTag.value) {
dropdownTag.value.handleOpen();
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.trace-tags { .trace-tags {
@ -121,7 +202,6 @@ function updateTags() {
padding: 2px 5px; padding: 2px 5px;
border-radius: 3px; border-radius: 3px;
width: 250px; width: 250px;
margin-right: 3px;
} }
.remove-icon { .remove-icon {
@ -130,10 +210,20 @@ function updateTags() {
cursor: pointer; cursor: pointer;
} }
.tag-item {
display: inline-block;
min-width: 210px;
}
.tags-tip { .tags-tip {
color: #a7aebb; color: #a7aebb;
} }
.link-tips {
display: inline-block;
margin-left: 130px;
}
.light { .light {
color: #3d444f; color: #3d444f;

View File

@ -50,9 +50,14 @@ limitations under the License. -->
@query="searchEndpoints" @query="searchEndpoints"
/> />
</div> </div>
</div> <el-button
<div class="row tips"> class="search-btn"
<b>{{ t("conditionNotice") }}</b> size="small"
type="primary"
@click="searchLogs"
>
{{ t("search") }}
</el-button>
</div> </div>
<div class="flex-h row"> <div class="flex-h row">
<div class="mr-5 traceId" v-show="!isBrowser"> <div class="mr-5 traceId" v-show="!isBrowser">
@ -61,6 +66,9 @@ limitations under the License. -->
</div> </div>
<ConditionTags :type="'LOG'" @update="updateTags" /> <ConditionTags :type="'LOG'" @update="updateTags" />
</div> </div>
<div class="row tips">
<b>{{ t("conditionNotice") }}</b>
</div>
<div class="flex-h" v-show="!isBrowser"> <div class="flex-h" v-show="!isBrowser">
<div class="mr-5" v-show="logStore.supportQueryLogsByKeywords"> <div class="mr-5" v-show="logStore.supportQueryLogsByKeywords">
<span class="mr-5 grey">{{ t("keywordsOfContent") }}:</span> <span class="mr-5 grey">{{ t("keywordsOfContent") }}:</span>
@ -109,14 +117,6 @@ limitations under the License. -->
</span> </span>
</el-tooltip> </el-tooltip>
</div> </div>
<el-button
class="search-btn"
size="small"
type="primary"
@click="searchLogs"
>
{{ t("search") }}
</el-button>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -326,6 +326,7 @@ watch(
.row { .row {
margin-bottom: 5px; margin-bottom: 5px;
position: relative;
} }
.inputs-max { .inputs-max {
@ -337,8 +338,11 @@ watch(
} }
.search-btn { .search-btn {
margin-left: 20px; position: absolute;
top: 0;
right: 10px;
cursor: pointer; cursor: pointer;
width: 120px;
} }
.tips { .tips {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<div class="flex-h row"> <div class="flex-h row">
<div class="mr-5" v-if="dashboardStore.entity === EntityType[1].value"> <div class="mr-10" v-if="dashboardStore.entity === EntityType[1].value">
<span class="grey mr-5">{{ t("service") }}:</span> <span class="grey mr-5">{{ t("service") }}:</span>
<Selector <Selector
size="small" size="small"
@ -24,7 +24,7 @@ limitations under the License. -->
@change="changeField('service', $event)" @change="changeField('service', $event)"
/> />
</div> </div>
<div class="mr-5" v-if="dashboardStore.entity !== EntityType[3].value"> <div class="mr-10" v-if="dashboardStore.entity !== EntityType[3].value">
<span class="grey mr-5">{{ t("instance") }}:</span> <span class="grey mr-5">{{ t("instance") }}:</span>
<Selector <Selector
size="small" size="small"
@ -34,7 +34,7 @@ limitations under the License. -->
@change="changeField('instance', $event)" @change="changeField('instance', $event)"
/> />
</div> </div>
<div class="mr-5" v-if="dashboardStore.entity !== EntityType[2].value"> <div class="mr-10" v-if="dashboardStore.entity !== EntityType[2].value">
<span class="grey mr-5">{{ t("endpoint") }}:</span> <span class="grey mr-5">{{ t("endpoint") }}:</span>
<Selector <Selector
size="small" size="small"
@ -46,7 +46,7 @@ limitations under the License. -->
@query="searchEndpoints" @query="searchEndpoints"
/> />
</div> </div>
<div class="mr-5"> <div class="mr-10">
<span class="grey mr-5">{{ t("status") }}:</span> <span class="grey mr-5">{{ t("status") }}:</span>
<Selector <Selector
size="small" size="small"
@ -56,27 +56,29 @@ limitations under the License. -->
@change="changeField('status', $event)" @change="changeField('status', $event)"
/> />
</div> </div>
<div class="mr-5"> <el-button
size="small"
type="primary"
@click="searchTraces"
class="search-btn"
>
{{ t("search") }}
</el-button>
</div>
<div class="flex-h row">
<div class="mr-10">
<span class="grey mr-5">{{ t("traceID") }}:</span> <span class="grey mr-5">{{ t("traceID") }}:</span>
<el-input size="small" v-model="traceId" class="traceId" /> <el-input size="small" v-model="traceId" class="traceId" />
</div> </div>
</div> <div class="mr-10">
<div class="flex-h">
<div class="mr-5">
<span class="sm b grey mr-5">{{ t("duration") }}:</span> <span class="sm b grey mr-5">{{ t("duration") }}:</span>
<el-input size="small" class="inputs mr-5" v-model="minTraceDuration" /> <el-input size="small" class="inputs mr-5" v-model="minTraceDuration" />
<span class="grey mr-5">-</span> <span class="grey mr-5">-</span>
<el-input size="small" class="inputs" v-model="maxTraceDuration" /> <el-input size="small" class="inputs" v-model="maxTraceDuration" />
</div> </div>
</div>
<div class="flex-h">
<ConditionTags :type="'TRACE'" @update="updateTags" /> <ConditionTags :type="'TRACE'" @update="updateTags" />
<el-button
class="search-btn"
size="small"
type="primary"
@click="searchTraces"
>
{{ t("search") }}
</el-button>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -109,10 +111,6 @@ const state = reactive<any>({
service: { value: "", label: "" }, service: { value: "", label: "" },
}); });
// const dateTime = computed(() => [
// appStore.durationRow.start,
// appStore.durationRow.end,
// ]);
init(); init();
async function init() { async function init() {
if (dashboardStore.entity === EntityType[1].value) { if (dashboardStore.entity === EntityType[1].value) {
@ -241,6 +239,7 @@ watch(
.row { .row {
margin-bottom: 5px; margin-bottom: 5px;
position: relative;
} }
.traceId { .traceId {
@ -248,7 +247,10 @@ watch(
} }
.search-btn { .search-btn {
margin-left: 20px;
cursor: pointer; cursor: pointer;
width: 120px;
position: absolute;
top: 0;
right: 10px;
} }
</style> </style>

View File

@ -200,9 +200,9 @@ export default defineComponent({
dom.style.background = "rgba(0, 0, 0, 0.1)"; dom.style.background = "rgba(0, 0, 0, 0.1)";
} }
function selectSpan(event: any) { function selectSpan(event: any) {
const dom = event.path.find((d: any) => const dom = event
d.className.includes("trace-item") .composedPath()
); .find((d: any) => d.className.includes("trace-item"));
emit("select", props.data); emit("select", props.data);
if (props.headerType === "profile") { if (props.headerType === "profile") {
@ -212,9 +212,9 @@ export default defineComponent({
viewSpanDetail(dom); viewSpanDetail(dom);
} }
function viewSpan(event: any) { function viewSpan(event: any) {
const dom = event.path.find((d: any) => const dom = event
d.className.includes("trace-item") .composedPath()
); .find((d: any) => d.className.includes("trace-item"));
emit("select", props.data); emit("select", props.data);
viewSpanDetail(dom); viewSpanDetail(dom);
} }