feat: Logs Traces correlation (#3705)
fix: #2369 #3379 #4090 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit - **New Features** - Introduced new UI components for managing organization settings with validation. - Added "View Logs" button functionality in the trace sidebar. - Enhanced span logs management with new selection and viewing features. - Added search capabilities for log stream selection in trace details. - Added new routes for Trace Details and Organization Settings with access control. - **Improvements** - Updated layout and styling for better user experience, including full-width trace details. - Integrated services for fetching trace metadata and log streams. - Enhanced user feedback mechanisms for actions like copying trace URLs. - Improved interactivity and responsiveness in span-related components. - Refined navigation and routing for trace details and organization settings. - Improved error handling and state management in log fetching functionalities. - **Bug Fixes** - Adjusted CSS to correct layout issues in trace details and sidebar views. - Improved test reliability in UI interaction testing by extending wait times. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Subhradeep Chakraborty <chakrabortysubhradeep556@gmail.com> Co-authored-by: Bhargav <BJP232004@GMAIL.COM>
This commit is contained in:
parent
337e973a70
commit
cfdc09fcba
|
@ -101,18 +101,32 @@ fn default_scrape_interval() -> u32 {
|
|||
config::get_config().common.default_scrape_interval
|
||||
}
|
||||
|
||||
fn default_trace_id_field_name() -> String {
|
||||
"traceId".to_string()
|
||||
}
|
||||
|
||||
fn default_span_id_field_name() -> String {
|
||||
"spanId".to_string()
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Deserialize, Debug, Clone)]
|
||||
pub struct OrganizationSetting {
|
||||
/// Ideally this should be the same as prometheus-scrape-interval (in
|
||||
/// seconds).
|
||||
#[serde(default = "default_scrape_interval")]
|
||||
pub scrape_interval: u32,
|
||||
#[serde(default = "default_trace_id_field_name")]
|
||||
pub trace_id_field_name: String,
|
||||
#[serde(default = "default_span_id_field_name")]
|
||||
pub span_id_field_name: String,
|
||||
}
|
||||
|
||||
impl Default for OrganizationSetting {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scrape_interval: default_scrape_interval(),
|
||||
trace_id_field_name: default_trace_id_field_name(),
|
||||
span_id_field_name: default_span_id_field_name(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ pub struct Span {
|
|||
#[serde(skip_serializing_if = "HashMap::is_empty")]
|
||||
pub service: HashMap<String, json::Value>,
|
||||
pub events: String,
|
||||
pub links: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
|
@ -61,6 +62,30 @@ pub struct Event {
|
|||
pub attributes: HashMap<String, json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SpanLinkContext {
|
||||
pub trace_id: String,
|
||||
pub span_id: String,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub trace_flags: Option<u32>,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub trace_state: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SpanLink {
|
||||
pub context: SpanLinkContext,
|
||||
#[serde(flatten)]
|
||||
#[serde(skip_serializing_if = "HashMap::is_empty")]
|
||||
pub attributes: HashMap<String, json::Value>,
|
||||
#[serde(default)]
|
||||
pub dropped_attributes_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, ToSchema)]
|
||||
pub struct ExportTraceServiceResponse {
|
||||
// The details of a partially successful export request.
|
||||
|
|
|
@ -44,7 +44,7 @@ use crate::{
|
|||
alerts::Alert,
|
||||
http::HttpResponse as MetaHttpResponse,
|
||||
stream::{SchemaRecords, StreamParams},
|
||||
traces::{Event, Span, SpanRefType},
|
||||
traces::{Event, Span, SpanLink, SpanLinkContext, SpanRefType},
|
||||
},
|
||||
service::{
|
||||
db, format_stream_name,
|
||||
|
@ -198,6 +198,24 @@ pub async fn handle_trace_request(
|
|||
})
|
||||
}
|
||||
|
||||
let mut links = vec![];
|
||||
let mut link_att_map: HashMap<String, json::Value> = HashMap::new();
|
||||
for link in span.links {
|
||||
for link_att in link.attributes {
|
||||
link_att_map.insert(link_att.key, get_val(&link_att.value.as_ref()));
|
||||
}
|
||||
links.push(SpanLink {
|
||||
context: SpanLinkContext {
|
||||
span_id: String::from_utf8(link.span_id).unwrap(),
|
||||
trace_id: String::from_utf8(link.trace_id).unwrap(),
|
||||
trace_flags: Some(link.flags),
|
||||
trace_state: Some(link.trace_state),
|
||||
},
|
||||
attributes: link_att_map.clone(),
|
||||
dropped_attributes_count: link.dropped_attributes_count, // TODO: add appropriate value
|
||||
})
|
||||
}
|
||||
|
||||
let timestamp = (start_time / 1000) as i64;
|
||||
if timestamp < min_ts {
|
||||
partial_success.rejected_spans += 1;
|
||||
|
@ -220,6 +238,7 @@ pub async fn handle_trace_request(
|
|||
flags: 1, // TODO add appropriate value
|
||||
//_timestamp: timestamp,
|
||||
events: json::to_string(&events).unwrap(),
|
||||
links: json::to_string(&links).unwrap(),
|
||||
};
|
||||
let span_status_for_spanmetric = local_val.span_status.clone();
|
||||
|
||||
|
|
|
@ -31,7 +31,10 @@ use super::{BLOCK_FIELDS, PARENT_SPAN_ID, PARENT_TRACE_ID, REF_TYPE, SERVICE, SE
|
|||
use crate::{
|
||||
common::meta::{
|
||||
http::HttpResponse as MetaHttpResponse,
|
||||
traces::{Event, ExportTracePartialSuccess, ExportTraceServiceResponse, Span, SpanRefType},
|
||||
traces::{
|
||||
Event, ExportTracePartialSuccess, ExportTraceServiceResponse, Span, SpanLink,
|
||||
SpanLinkContext, SpanRefType,
|
||||
},
|
||||
},
|
||||
service::{
|
||||
db, format_stream_name, ingestion::grpc::get_val_for_attr,
|
||||
|
@ -131,6 +134,7 @@ pub async fn traces_json(
|
|||
let mut service_name: String = traces_stream_name.to_string();
|
||||
let mut json_data = Vec::with_capacity(64);
|
||||
let mut partial_success = ExportTracePartialSuccess::default();
|
||||
log::debug!("the spans here: {:#?}", spans);
|
||||
for res_span in spans.iter() {
|
||||
let mut service_att_map: HashMap<String, json::Value> = HashMap::new();
|
||||
if res_span.get("resource").is_some() {
|
||||
|
@ -207,7 +211,9 @@ pub async fn traces_json(
|
|||
}
|
||||
|
||||
let mut events = vec![];
|
||||
let mut links = vec![];
|
||||
let mut event_att_map: HashMap<String, json::Value> = HashMap::new();
|
||||
let mut link_att_map: HashMap<String, json::Value> = HashMap::new();
|
||||
|
||||
let empty_vec = Vec::new();
|
||||
let span_events = match span.get("events") {
|
||||
|
@ -228,6 +234,75 @@ pub async fn traces_json(
|
|||
attributes: event_att_map.clone(),
|
||||
})
|
||||
}
|
||||
let span_links = match span.get("links") {
|
||||
Some(v) => v.as_array().unwrap(),
|
||||
None => &empty_vec,
|
||||
};
|
||||
for link in span_links {
|
||||
let attributes = link.get("attributes").unwrap().as_array().unwrap();
|
||||
for link_att in attributes {
|
||||
link_att_map.insert(
|
||||
link_att.get("key").unwrap().as_str().unwrap().to_string(),
|
||||
get_val_for_attr(link_att.get("value").unwrap().clone()),
|
||||
);
|
||||
}
|
||||
let (mut trace_id, mut span_id, mut trace_flags, mut trace_state) =
|
||||
(None, None, None, None);
|
||||
if let Some(link_trace) = link.get("traceId") {
|
||||
trace_id = Some(link_trace.as_str().unwrap().to_owned());
|
||||
}
|
||||
|
||||
if let Some(link_span) = link.get("spanId") {
|
||||
span_id = Some(link_span.as_str().unwrap().to_owned());
|
||||
}
|
||||
|
||||
if let Some(link_trace_flags) = link.get("traceFlags") {
|
||||
trace_flags = Some(link_trace_flags.as_u64().unwrap() as u32);
|
||||
}
|
||||
|
||||
if trace_flags.is_none() {
|
||||
if let Some(link_trace_flags) = link.get("flags") {
|
||||
trace_flags = Some(link_trace_flags.as_u64().unwrap() as u32);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(link_trace_state) = link.get("traceState") {
|
||||
trace_state = Some(link_trace_state.as_str().unwrap().to_owned());
|
||||
}
|
||||
|
||||
if trace_id.is_none()
|
||||
|| span_id.is_none()
|
||||
|| trace_flags.is_none()
|
||||
|| trace_state.is_none()
|
||||
{
|
||||
if let Some(span_context) = link.get("context") {
|
||||
let span_context: SpanLinkContext =
|
||||
json::from_value(span_context.to_owned()).unwrap();
|
||||
if trace_id.is_none() {
|
||||
trace_id = Some(span_context.trace_id);
|
||||
}
|
||||
if span_id.is_none() {
|
||||
span_id = Some(span_context.span_id);
|
||||
}
|
||||
if trace_flags.is_none() {
|
||||
trace_flags = span_context.trace_flags;
|
||||
}
|
||||
if trace_state.is_none() {
|
||||
trace_state = span_context.trace_state;
|
||||
}
|
||||
}
|
||||
}
|
||||
links.push(SpanLink {
|
||||
context: SpanLinkContext {
|
||||
span_id: span_id.unwrap(),
|
||||
trace_id: trace_id.unwrap(),
|
||||
trace_flags,
|
||||
trace_state,
|
||||
},
|
||||
attributes: link_att_map.clone(),
|
||||
dropped_attributes_count: 0, // TODO: add appropriate value
|
||||
})
|
||||
}
|
||||
|
||||
let timestamp = (start_time / 1000) as i64;
|
||||
if timestamp < min_ts {
|
||||
|
@ -253,6 +328,7 @@ pub async fn traces_json(
|
|||
service: service_att_map.clone(),
|
||||
flags: 1, // TODO add appropriate value
|
||||
events: json::to_string(&events).unwrap(),
|
||||
links: json::to_string(&links).unwrap(),
|
||||
};
|
||||
|
||||
let mut value: json::Value = json::to_value(local_val).unwrap();
|
||||
|
|
|
@ -166,14 +166,16 @@ await page.waitForTimeout(1000);
|
|||
force: true,
|
||||
});
|
||||
await page
|
||||
.locator(".q-pl-sm > .q-btn > .q-btn__content")
|
||||
.locator('[data-test="logs-search-bar-refresh-interval-btn-dropdown"]')
|
||||
.click({ force: true });
|
||||
await page.locator('[data-test="logs-search-bar-refresh-time-5"]').click({
|
||||
force: true,
|
||||
});
|
||||
await page.waitForTimeout(1000);
|
||||
await expect(page.locator(".q-notification__message")).toContainText(
|
||||
"Live mode is enabled"
|
||||
);
|
||||
await page.waitForTimeout(5000);
|
||||
await page
|
||||
.locator(".q-pl-sm > .q-btn > .q-btn__content")
|
||||
.click({ force: true });
|
||||
|
|
|
@ -564,7 +564,7 @@ test.describe("Sanity testcases", () => {
|
|||
await page.locator('[data-test="menu-link-\\/settings\\/-item"]').click();
|
||||
await page.waitForTimeout(2000);
|
||||
await page.getByText("General SettingsScrape").click();
|
||||
await page.getByRole("tab", { name: "Settings" }).click();
|
||||
await page.getByRole("tab", { name: "General Settings" }).click();
|
||||
await page.getByLabel("Scrape Interval (In Seconds) *").fill("16");
|
||||
await page.locator('[data-test="dashboard-add-submit"]').click();
|
||||
await page.getByText("Organization settings updated").click();
|
||||
|
|
|
@ -23,7 +23,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
data-cy="date-time-button"
|
||||
outline
|
||||
no-caps
|
||||
:label="displayValue"
|
||||
:label="getDisplayValue"
|
||||
icon="schedule"
|
||||
icon-right="arrow_drop_down"
|
||||
class="date-time-button"
|
||||
|
@ -315,6 +315,7 @@ import {
|
|||
onMounted,
|
||||
watch,
|
||||
nextTick,
|
||||
onActivated,
|
||||
} from "vue";
|
||||
import {
|
||||
getImageURL,
|
||||
|
@ -478,7 +479,7 @@ export default defineComponent({
|
|||
selectedType.value = props.defaultType;
|
||||
setAbsoluteTime(startTime, endTime);
|
||||
setRelativeTime(props.defaultRelativeTime);
|
||||
displayValue.value = getDisplayValue();
|
||||
// displayValue.value = getDisplayValue();
|
||||
|
||||
if (props.autoApply) saveDate(props.defaultType);
|
||||
} catch (e) {
|
||||
|
@ -510,12 +511,31 @@ export default defineComponent({
|
|||
router.currentRoute.value.query?.from;
|
||||
},
|
||||
() => {
|
||||
if(router.currentRoute.value.query.hasOwnProperty("from") && router.currentRoute.value.query.hasOwnProperty("to")) {
|
||||
if (
|
||||
router.currentRoute.value.query.hasOwnProperty("from") &&
|
||||
router.currentRoute.value.query.hasOwnProperty("to")
|
||||
) {
|
||||
selectedType.value = "absolute";
|
||||
selectedTime.value.startTime = timestampToTimezoneDate(router.currentRoute.value.query?.from/1000, store.state.timezone, "HH:mm");
|
||||
selectedTime.value.endTime = timestampToTimezoneDate(router.currentRoute.value.query?.to/1000, store.state.timezone, "HH:mm");
|
||||
selectedDate.value.from = timestampToTimezoneDate(router.currentRoute.value.query?.from/1000, store.state.timezone, "yyyy/MM/dd");
|
||||
selectedDate.value.to = timestampToTimezoneDate(router.currentRoute.value.query?.to/1000, store.state.timezone, "yyyy/MM/dd");
|
||||
selectedTime.value.startTime = timestampToTimezoneDate(
|
||||
router.currentRoute.value.query?.from / 1000,
|
||||
store.state.timezone,
|
||||
"HH:mm"
|
||||
);
|
||||
selectedTime.value.endTime = timestampToTimezoneDate(
|
||||
router.currentRoute.value.query?.to / 1000,
|
||||
store.state.timezone,
|
||||
"HH:mm"
|
||||
);
|
||||
selectedDate.value.from = timestampToTimezoneDate(
|
||||
router.currentRoute.value.query?.from / 1000,
|
||||
store.state.timezone,
|
||||
"yyyy/MM/dd"
|
||||
);
|
||||
selectedDate.value.to = timestampToTimezoneDate(
|
||||
router.currentRoute.value.query?.to / 1000,
|
||||
store.state.timezone,
|
||||
"yyyy/MM/dd"
|
||||
);
|
||||
saveDate("absolute");
|
||||
}
|
||||
},
|
||||
|
@ -568,6 +588,8 @@ export default defineComponent({
|
|||
if (periodValue) {
|
||||
relativeValue.value = parseInt(periodValue);
|
||||
}
|
||||
|
||||
selectedType.value = "relative";
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -623,7 +645,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
const saveDate = (dateType) => {
|
||||
displayValue.value = getDisplayValue();
|
||||
// displayValue.value = getDisplayValue();
|
||||
const date = getConsumableDateTime();
|
||||
if (isNaN(date.endTime) || isNaN(date.startTime)) {
|
||||
return false;
|
||||
|
@ -784,10 +806,10 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
displayValue.value = getDisplayValue();
|
||||
// displayValue.value = getDisplayValue();
|
||||
};
|
||||
|
||||
const getDisplayValue = () => {
|
||||
const getDisplayValue = computed(() => {
|
||||
if (selectedType.value === "relative") {
|
||||
return `Past ${relativeValue.value} ${getPeriodLabel.value}`;
|
||||
} else {
|
||||
|
@ -805,7 +827,7 @@ export default defineComponent({
|
|||
return `${todayDate} ${selectedTime.value.startTime} - ${todayDate} ${selectedTime.value.endTime}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const timezoneFilterFn = (val, update) => {
|
||||
filteredTimezone.value = filterColumns(timezoneOptions, val, update);
|
||||
|
@ -839,7 +861,7 @@ export default defineComponent({
|
|||
|
||||
const setDateType = (type) => {
|
||||
selectedType.value = type;
|
||||
displayValue.value = getDisplayValue();
|
||||
// displayValue.value = getDisplayValue();
|
||||
|
||||
if (props.autoApply)
|
||||
saveDate(type === "absolute" ? "absolute" : "relative-custom");
|
||||
|
@ -876,6 +898,8 @@ export default defineComponent({
|
|||
getConsumableDateTime,
|
||||
relativeDatesInHour,
|
||||
setAbsoluteTime,
|
||||
setRelativeTime,
|
||||
getDisplayValue,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,172 @@
|
|||
<template>
|
||||
<div class="q-px-md q-pt-md q-pb-md">
|
||||
<div class="text-body1 text-bold">
|
||||
{{ t("settings.logDetails") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mx-md q-mb-md">
|
||||
<div
|
||||
data-test="add-role-rolename-input-btn"
|
||||
class="trace-id-field-name o2-input q-mb-sm"
|
||||
>
|
||||
<q-input
|
||||
v-model.trim="traceIdFieldName"
|
||||
:label="t('settings.traceIdFieldName') + ' *'"
|
||||
color="input-border"
|
||||
bg-color="input-bg"
|
||||
class="q-py-md showLabelOnTop"
|
||||
outlined
|
||||
stack-label
|
||||
filled
|
||||
dense
|
||||
:rules="[
|
||||
(val: string) =>
|
||||
!!val
|
||||
? isValidTraceField ||
|
||||
`Use alphanumeric and '+=,.@-_' characters only, without spaces.`
|
||||
: t('common.nameRequired'),
|
||||
]"
|
||||
>
|
||||
<template v-slot:hint>
|
||||
Use alphanumeric and '+=,.@-_' characters only, without spaces.
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-test="add-role-rolename-input-btn"
|
||||
class="span-id-field-name o2-input"
|
||||
>
|
||||
<q-input
|
||||
v-model.trim="spanIdFieldName"
|
||||
:label="t('settings.spanIdFieldName') + ' *'"
|
||||
color="input-border"
|
||||
bg-color="input-bg"
|
||||
class="q-py-md showLabelOnTop"
|
||||
stack-label
|
||||
outlined
|
||||
filled
|
||||
dense
|
||||
:rules="[
|
||||
(val: string) =>
|
||||
!!val
|
||||
? isValidSpanField ||
|
||||
`Use alphanumeric and '+=,.@-_' characters only, without spaces.`
|
||||
: t('common.nameRequired'),
|
||||
]"
|
||||
@update:model-value="updateFieldName('span')"
|
||||
>
|
||||
<template v-slot:hint>
|
||||
Use alphanumeric and '+=,.@-_' characters only, without spaces.
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-start q-mt-lg">
|
||||
<q-btn
|
||||
data-test="add-alert-cancel-btn"
|
||||
v-close-popup="true"
|
||||
class="q-mb-md text-bold"
|
||||
:label="t('alerts.cancel')"
|
||||
text-color="light-text"
|
||||
padding="sm md"
|
||||
no-caps
|
||||
@click="$emit('cancel:hideform')"
|
||||
/>
|
||||
<q-btn
|
||||
data-test="add-alert-submit-btn"
|
||||
:label="t('alerts.save')"
|
||||
class="q-mb-md text-bold no-border q-ml-md"
|
||||
color="secondary"
|
||||
padding="sm xl"
|
||||
no-caps
|
||||
@click="saveOrgSettings"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import organizations from "@/services/organizations";
|
||||
import { useStore } from "vuex";
|
||||
import { useQuasar } from "quasar";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const traceIdFieldName = ref(
|
||||
store.state?.organizationData?.organizationSettings?.trace_id_field_name,
|
||||
);
|
||||
|
||||
const spanIdFieldName = ref(
|
||||
store.state?.organizationData?.organizationSettings?.span_id_field_name,
|
||||
);
|
||||
|
||||
const q = useQuasar();
|
||||
|
||||
const isValidSpanField = ref(true);
|
||||
const isValidTraceField = ref(true);
|
||||
|
||||
const isValidRoleName = computed(() => {
|
||||
const roleNameRegex = /^[a-zA-Z0-9+=,.@_-]+$/;
|
||||
// Check if the role name is valid
|
||||
return roleNameRegex.test(traceIdFieldName.value);
|
||||
});
|
||||
|
||||
const validateFieldName = (value: string) => {
|
||||
const roleNameRegex = /^[a-zA-Z0-9+=,.@_-]+$/;
|
||||
// Check if the role name is valid
|
||||
return roleNameRegex.test(value);
|
||||
};
|
||||
|
||||
const updateFieldName = (fieldName: string) => {
|
||||
if (fieldName === "span")
|
||||
isValidSpanField.value = validateFieldName(spanIdFieldName.value);
|
||||
|
||||
if (fieldName === "trace")
|
||||
isValidTraceField.value = validateFieldName(traceIdFieldName.value);
|
||||
};
|
||||
|
||||
const saveOrgSettings = async () => {
|
||||
try {
|
||||
await organizations.post_organization_settings(
|
||||
store.state.selectedOrganization.identifier,
|
||||
{
|
||||
trace_id_field_name: traceIdFieldName.value,
|
||||
span_id_field_name: spanIdFieldName.value,
|
||||
},
|
||||
);
|
||||
|
||||
store.dispatch("setOrganizationSettings", {
|
||||
...store.state?.organizationData?.organizationSettings,
|
||||
trace_id_field_name: traceIdFieldName.value,
|
||||
span_id_field_name: spanIdFieldName.value,
|
||||
});
|
||||
|
||||
q.notify({
|
||||
message: "Organization settings updated successfully",
|
||||
color: "positive",
|
||||
position: "bottom",
|
||||
timeout: 3000,
|
||||
});
|
||||
} catch (e: any) {
|
||||
q.notify({
|
||||
message: e?.message || "Error saving organization settings",
|
||||
color: "negative",
|
||||
position: "bottom",
|
||||
timeout: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.trace-id-field-name,
|
||||
.span-id-field-name {
|
||||
width: 400px;
|
||||
}
|
||||
</style>
|
|
@ -16,15 +16,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
||||
<!-- eslint-disable vue/x-invalid-end-tag -->
|
||||
<template>
|
||||
<q-page class="page q-pa-md">
|
||||
<div class="head q-table__title q-pb-md">
|
||||
<q-page class="page">
|
||||
<div class="head q-table__title q-mx-md q-my-sm">
|
||||
{{ t("settings.header") }}
|
||||
</div>
|
||||
<q-separator class="separator" />
|
||||
<q-splitter
|
||||
v-model="splitterModel"
|
||||
unit="px"
|
||||
style="min-height: calc(100vh - 136px)"
|
||||
style="min-height: calc(100vh - 104px)"
|
||||
>
|
||||
<template v-slot:before>
|
||||
<q-tabs
|
||||
|
@ -49,6 +49,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
:label="t('settings.generalLabel')"
|
||||
content-class="tab_content"
|
||||
/>
|
||||
<q-route-tab
|
||||
name="organization"
|
||||
:to="'/settings/organization'"
|
||||
:icon="outlinedSettings"
|
||||
:label="t('settings.orgLabel')"
|
||||
content-class="tab_content"
|
||||
/>
|
||||
</q-tabs>
|
||||
</template>
|
||||
|
||||
|
@ -77,7 +84,7 @@ import { outlinedSettings } from "@quasar/extras/material-icons-outlined";
|
|||
import useIsMetaOrg from "@/composables/useIsMetaOrg";
|
||||
|
||||
export default defineComponent({
|
||||
name: "PageIngestion",
|
||||
name: "AppSettings",
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
|
@ -126,7 +133,7 @@ export default defineComponent({
|
|||
router,
|
||||
config,
|
||||
settingsTab,
|
||||
splitterModel: ref(200),
|
||||
splitterModel: ref(250),
|
||||
outlinedSettings,
|
||||
isMetaOrg,
|
||||
};
|
||||
|
|
|
@ -25,6 +25,8 @@ const Search = () => import("@/views/Search.vue");
|
|||
const AppMetrics = () => import("@/views/AppMetrics.vue");
|
||||
const AppTraces = () => import("@/views/AppTraces.vue");
|
||||
|
||||
const TraceDetails = () => import("@/plugins/traces/TraceDetails.vue");
|
||||
|
||||
const ViewDashboard = () => import("@/views/Dashboards/ViewDashboard.vue");
|
||||
const AddPanel = () => import("@/views/Dashboards/addPanel/AddPanel.vue");
|
||||
const StreamExplorer = () => import("@/views/StreamExplorer.vue");
|
||||
|
@ -114,6 +116,17 @@ const useRoutes = () => {
|
|||
routeGuard(to, from, next);
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "traces/trace-details",
|
||||
name: "traceDetails",
|
||||
component: TraceDetails,
|
||||
meta: {
|
||||
keepAlive: true,
|
||||
},
|
||||
beforeEnter(to: any, from: any, next: any) {
|
||||
routeGuard(to, from, next);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "streamExplorer",
|
||||
path: "streams/stream-explore",
|
||||
|
|
|
@ -23,6 +23,15 @@ const useManagementRoutes = () => {
|
|||
routeGuard(to, from, next);
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "organization",
|
||||
name: "organizationSettings",
|
||||
component: () =>
|
||||
import("@/components/settings/OrganizationSettings.vue"),
|
||||
beforeEnter(to: any, from: any, next: any) {
|
||||
routeGuard(to, from, next);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -54,7 +54,6 @@ import searchService from "@/services/search";
|
|||
import type { LogsQueryPayload } from "@/ts/interfaces/query";
|
||||
import savedviewsService from "@/services/saved_views";
|
||||
import config from "@/aws-exports";
|
||||
import { fr } from "date-fns/locale";
|
||||
|
||||
const defaultObject = {
|
||||
organizationIdetifier: "",
|
||||
|
@ -130,6 +129,7 @@ const defaultObject = {
|
|||
clusters: [],
|
||||
useUserDefinedSchemas: "user_defined_schema",
|
||||
hasUserDefinedSchemas: false,
|
||||
selectedTraceStream: "",
|
||||
},
|
||||
data: {
|
||||
query: <any>"",
|
||||
|
@ -204,6 +204,7 @@ const defaultObject = {
|
|||
customDownloadQueryObj: <any>{},
|
||||
functionError: "",
|
||||
searchRequestTraceIds: <string[]>[],
|
||||
isOperationCancelled: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -448,7 +449,7 @@ const useLogs = () => {
|
|||
return await getStream(
|
||||
streamName,
|
||||
searchObj.data.stream.streamType || "logs",
|
||||
true
|
||||
true,
|
||||
).then((res) => {
|
||||
searchObj.loadingStream = false;
|
||||
return res;
|
||||
|
@ -525,7 +526,7 @@ const useLogs = () => {
|
|||
searchObj.data.tempFunctionContent != ""
|
||||
) {
|
||||
query["functionContent"] = b64EncodeUnicode(
|
||||
searchObj.data.tempFunctionContent
|
||||
searchObj.data.tempFunctionContent,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -578,10 +579,10 @@ const useLogs = () => {
|
|||
const validateFilterForMultiStream = () => {
|
||||
const filterCondition = searchObj.data.editorValue;
|
||||
const parsedSQL: any = parser.astify(
|
||||
"select * from stream where " + filterCondition
|
||||
"select * from stream where " + filterCondition,
|
||||
);
|
||||
searchObj.data.stream.filteredField = extractFilterColumns(
|
||||
parsedSQL?.where
|
||||
parsedSQL?.where,
|
||||
);
|
||||
|
||||
searchObj.data.filterErrMsg = "";
|
||||
|
@ -590,12 +591,12 @@ const useLogs = () => {
|
|||
for (const fieldName of searchObj.data.stream.filteredField) {
|
||||
const filteredFields: any =
|
||||
searchObj.data.stream.selectedStreamFields.filter(
|
||||
(field: any) => field.name === fieldName
|
||||
(field: any) => field.name === fieldName,
|
||||
);
|
||||
if (filteredFields.length > 0) {
|
||||
const streamsCount = filteredFields[0].streams.length;
|
||||
const allStreamsEqual = filteredFields.every(
|
||||
(field: any) => field.streams.length === streamsCount
|
||||
(field: any) => field.streams.length === streamsCount,
|
||||
);
|
||||
if (!allStreamsEqual) {
|
||||
searchObj.data.filterErrMsg += `Field '${fieldName}' exists in different number of streams.\n`;
|
||||
|
@ -611,12 +612,12 @@ const useLogs = () => {
|
|||
|
||||
searchObj.data.stream.missingStreamMultiStreamFilter =
|
||||
searchObj.data.stream.selectedStream.filter(
|
||||
(stream: any) => !fieldStreams.includes(stream)
|
||||
(stream: any) => !fieldStreams.includes(stream),
|
||||
);
|
||||
|
||||
if (searchObj.data.stream.missingStreamMultiStreamFilter.length > 0) {
|
||||
searchObj.data.missingStreamMessage = `One or more filter fields do not exist in "${searchObj.data.stream.missingStreamMultiStreamFilter.join(
|
||||
", "
|
||||
", ",
|
||||
)}", hence no search is performed in the mentioned stream.\n`;
|
||||
}
|
||||
}
|
||||
|
@ -663,14 +664,14 @@ const useLogs = () => {
|
|||
const streamData: any = getStreams(
|
||||
searchObj.data.stream.streamType,
|
||||
true,
|
||||
true
|
||||
true,
|
||||
);
|
||||
searchObj.data.stream.selectedStreamFields = streamData.schema;
|
||||
}
|
||||
|
||||
const streamFieldNames: any =
|
||||
searchObj.data.stream.selectedStreamFields.map(
|
||||
(item: any) => item.name
|
||||
(item: any) => item.name,
|
||||
);
|
||||
|
||||
for (
|
||||
|
@ -691,7 +692,7 @@ const useLogs = () => {
|
|||
if (searchObj.data.stream.selectedStream.length == 1) {
|
||||
req.query.sql = req.query.sql.replace(
|
||||
"[FIELD_LIST]",
|
||||
searchObj.data.stream.interestingFieldList.join(",")
|
||||
searchObj.data.stream.interestingFieldList.join(","),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
@ -701,7 +702,7 @@ const useLogs = () => {
|
|||
const timestamps: any =
|
||||
searchObj.data.datetime.type === "relative"
|
||||
? getConsumableRelativeTime(
|
||||
searchObj.data.datetime.relativeTimePeriod
|
||||
searchObj.data.datetime.relativeTimePeriod,
|
||||
)
|
||||
: cloneDeep(searchObj.data.datetime);
|
||||
|
||||
|
@ -751,7 +752,7 @@ const useLogs = () => {
|
|||
|
||||
req.aggs.histogram = req.aggs.histogram.replaceAll(
|
||||
"[INTERVAL]",
|
||||
searchObj.meta.resultGrid.chartInterval
|
||||
searchObj.meta.resultGrid.chartInterval,
|
||||
);
|
||||
} else {
|
||||
notificationMsg.value = "Invalid date format";
|
||||
|
@ -761,7 +762,7 @@ const useLogs = () => {
|
|||
if (searchObj.meta.sqlMode == true) {
|
||||
req.aggs.histogram = req.aggs.histogram.replace(
|
||||
"[INDEX_NAME]",
|
||||
searchObj.data.stream.selectedStream[0]
|
||||
searchObj.data.stream.selectedStream[0],
|
||||
);
|
||||
|
||||
req.aggs.histogram = req.aggs.histogram.replace("[WHERE_CLAUSE]", "");
|
||||
|
@ -769,7 +770,7 @@ const useLogs = () => {
|
|||
searchObj.data.query = query;
|
||||
const parsedSQL: any = fnParsedSQL();
|
||||
const histogramParsedSQL: any = fnHistogramParsedSQL(
|
||||
req.aggs.histogram
|
||||
req.aggs.histogram,
|
||||
);
|
||||
|
||||
histogramParsedSQL.where = parsedSQL.where;
|
||||
|
@ -869,12 +870,12 @@ const useLogs = () => {
|
|||
|
||||
req.query.sql = req.query.sql.replace(
|
||||
"[WHERE_CLAUSE]",
|
||||
" WHERE " + whereClause
|
||||
" WHERE " + whereClause,
|
||||
);
|
||||
|
||||
req.aggs.histogram = req.aggs.histogram.replace(
|
||||
"[WHERE_CLAUSE]",
|
||||
" WHERE " + whereClause
|
||||
" WHERE " + whereClause,
|
||||
);
|
||||
} else {
|
||||
req.query.sql = req.query.sql.replace("[WHERE_CLAUSE]", "");
|
||||
|
@ -883,7 +884,7 @@ const useLogs = () => {
|
|||
|
||||
req.query.sql = req.query.sql.replace(
|
||||
"[QUERY_FUNCTIONS]",
|
||||
queryFunctions
|
||||
queryFunctions,
|
||||
);
|
||||
|
||||
// in the case of multi stream, we need to pass query for each selected stream in the form of array
|
||||
|
@ -906,8 +907,8 @@ const useLogs = () => {
|
|||
streams = searchObj.data.stream.selectedStream.filter(
|
||||
(streams: any) =>
|
||||
!searchObj.data.stream.missingStreamMultiStreamFilter.includes(
|
||||
streams
|
||||
)
|
||||
streams,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -922,7 +923,7 @@ const useLogs = () => {
|
|||
.forEach((item: any) => {
|
||||
let finalQuery: string = preSQLQuery.replace(
|
||||
"[INDEX_NAME]",
|
||||
item
|
||||
item,
|
||||
);
|
||||
|
||||
// const finalHistogramQuery: string = preHistogramSQLQuery.replace(
|
||||
|
@ -952,7 +953,7 @@ const useLogs = () => {
|
|||
|
||||
finalQuery = finalQuery.replace(
|
||||
"[FIELD_LIST]",
|
||||
`'${item}' as _stream_name` + queryFieldList
|
||||
`'${item}' as _stream_name` + queryFieldList,
|
||||
);
|
||||
|
||||
// finalHistogramQuery = finalHistogramQuery.replace(
|
||||
|
@ -966,12 +967,12 @@ const useLogs = () => {
|
|||
} else {
|
||||
req.query.sql = req.query.sql.replace(
|
||||
"[INDEX_NAME]",
|
||||
searchObj.data.stream.selectedStream[0]
|
||||
searchObj.data.stream.selectedStream[0],
|
||||
);
|
||||
|
||||
req.aggs.histogram = req.aggs.histogram.replace(
|
||||
"[INDEX_NAME]",
|
||||
searchObj.data.stream.selectedStream[0]
|
||||
searchObj.data.stream.selectedStream[0],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1115,7 +1116,7 @@ const useLogs = () => {
|
|||
},
|
||||
];
|
||||
searchObj.data.queryResults.partitionDetail.paginations.push(
|
||||
pageObject
|
||||
pageObject,
|
||||
);
|
||||
searchObj.data.queryResults.partitionDetail.partitionTotal.push(-1);
|
||||
}
|
||||
|
@ -1132,7 +1133,6 @@ const useLogs = () => {
|
|||
traceparent,
|
||||
})
|
||||
.then(async (res: any) => {
|
||||
removeTraceId(traceId);
|
||||
searchObj.data.queryResults.partitionDetail = {
|
||||
partitions: [],
|
||||
partitionTotal: [],
|
||||
|
@ -1200,10 +1200,10 @@ const useLogs = () => {
|
|||
},
|
||||
];
|
||||
searchObj.data.queryResults.partitionDetail.paginations.push(
|
||||
pageObject
|
||||
pageObject,
|
||||
);
|
||||
searchObj.data.queryResults.partitionDetail.partitionTotal.push(
|
||||
-1
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1228,10 +1228,10 @@ const useLogs = () => {
|
|||
},
|
||||
];
|
||||
searchObj.data.queryResults.partitionDetail.paginations.push(
|
||||
pageObject
|
||||
pageObject,
|
||||
);
|
||||
searchObj.data.queryResults.partitionDetail.partitionTotal.push(
|
||||
-1
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1268,6 +1268,9 @@ const useLogs = () => {
|
|||
notificationMsg.value += " TraceID:" + trace_id;
|
||||
trace_id = "";
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
removeTraceId(traceId);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
@ -1311,7 +1314,7 @@ const useLogs = () => {
|
|||
},
|
||||
];
|
||||
searchObj.data.queryResults.partitionDetail.paginations.push(
|
||||
pageObject
|
||||
pageObject,
|
||||
);
|
||||
searchObj.data.queryResults.partitionDetail.partitionTotal.push(-1);
|
||||
}
|
||||
|
@ -1370,7 +1373,7 @@ const useLogs = () => {
|
|||
(accumulator: number, currentValue: any) =>
|
||||
accumulator +
|
||||
Math.max(parseInt(currentValue.zo_sql_num, 10), 0),
|
||||
0
|
||||
0,
|
||||
);
|
||||
partitionDetail.partitionTotal[0] =
|
||||
searchObj.data.queryResults.total;
|
||||
|
@ -1380,7 +1383,7 @@ const useLogs = () => {
|
|||
partitionDetail.partitionTotal.reduce(
|
||||
(accumulator: number, currentValue: number) =>
|
||||
accumulator + Math.max(currentValue, 0),
|
||||
0
|
||||
0,
|
||||
);
|
||||
}
|
||||
// partitionDetail.partitions.forEach((item: any, index: number) => {
|
||||
|
@ -1526,7 +1529,7 @@ const useLogs = () => {
|
|||
`Build Search operation took ${
|
||||
searchObjDebug["buildSearchEndTime"] -
|
||||
searchObjDebug["buildSearchStartTime"]
|
||||
} milliseconds to complete`
|
||||
} milliseconds to complete`,
|
||||
);
|
||||
if (queryReq == false) {
|
||||
throw new Error(notificationMsg.value || "Something went wrong.");
|
||||
|
@ -1541,7 +1544,7 @@ const useLogs = () => {
|
|||
`Partition operation took ${
|
||||
searchObjDebug["partitionEndTime"] -
|
||||
searchObjDebug["partitionStartTime"]
|
||||
} milliseconds to complete`
|
||||
} milliseconds to complete`,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1560,7 +1563,7 @@ const useLogs = () => {
|
|||
searchObj.meta.toggleFunction
|
||||
) {
|
||||
queryReq.query["query_fn"] = b64EncodeUnicode(
|
||||
searchObj.data.tempFunctionContent
|
||||
searchObj.data.tempFunctionContent,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1608,7 +1611,7 @@ const useLogs = () => {
|
|||
delete searchObj.data.histogramQuery.aggs;
|
||||
delete queryReq.aggs;
|
||||
searchObj.data.customDownloadQueryObj = JSON.parse(
|
||||
JSON.stringify(queryReq)
|
||||
JSON.stringify(queryReq),
|
||||
);
|
||||
// get the current page detail and set it into query request
|
||||
queryReq.query.start_time =
|
||||
|
@ -1665,7 +1668,7 @@ const useLogs = () => {
|
|||
`Get Paginated Data with API took ${
|
||||
searchObjDebug["paginatedDatawithAPIEndTime"] -
|
||||
searchObjDebug["paginatedDatawithAPIStartTime"]
|
||||
} milliseconds to complete`
|
||||
} milliseconds to complete`,
|
||||
);
|
||||
const parsedSQL: any = fnParsedSQL();
|
||||
|
||||
|
@ -1711,8 +1714,8 @@ const useLogs = () => {
|
|||
|
||||
const partitions = JSON.parse(
|
||||
JSON.stringify(
|
||||
searchObj.data.queryResults.partitionDetail.partitions
|
||||
)
|
||||
searchObj.data.queryResults.partitionDetail.partitions,
|
||||
),
|
||||
);
|
||||
|
||||
// is _timestamp orderby ASC then reverse the partition array
|
||||
|
@ -1764,7 +1767,7 @@ const useLogs = () => {
|
|||
`Total count took ${
|
||||
searchObjDebug["pagecountEndTime"] -
|
||||
searchObjDebug["pagecountStartTime"]
|
||||
} milliseconds to complete`
|
||||
} milliseconds to complete`,
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
|
@ -1795,13 +1798,13 @@ const useLogs = () => {
|
|||
`Entire operation took ${
|
||||
searchObjDebug["queryDataEndTime"] -
|
||||
searchObjDebug["queryDataStartTime"]
|
||||
} milliseconds to complete`
|
||||
} milliseconds to complete`,
|
||||
);
|
||||
console.log("=================== getQueryData Debug ===================");
|
||||
} catch (e: any) {
|
||||
searchObj.loading = false;
|
||||
showErrorNotification(
|
||||
notificationMsg.value || "Error occurred during the search operation."
|
||||
notificationMsg.value || "Error occurred during the search operation.",
|
||||
);
|
||||
notificationMsg.value = "";
|
||||
}
|
||||
|
@ -1838,7 +1841,7 @@ const useLogs = () => {
|
|||
|
||||
let date = new Date();
|
||||
const startTimeDate = new Date(
|
||||
searchObj.data.customDownloadQueryObj.query.start_time / 1000
|
||||
searchObj.data.customDownloadQueryObj.query.start_time / 1000,
|
||||
); // Convert microseconds to milliseconds
|
||||
if (searchObj.meta.resultGrid.chartInterval.includes("second")) {
|
||||
startTimeDate.setSeconds(startTimeDate.getSeconds() > 30 ? 30 : 10, 0); // Round to the nearest whole minute
|
||||
|
@ -1849,9 +1852,9 @@ const useLogs = () => {
|
|||
// startTimeDate.setSeconds(0, 0); // Round to the nearest whole minute
|
||||
startTimeDate.setMinutes(
|
||||
parseInt(
|
||||
searchObj.meta.resultGrid.chartInterval.replace(" minute", "")
|
||||
searchObj.meta.resultGrid.chartInterval.replace(" minute", ""),
|
||||
),
|
||||
0
|
||||
0,
|
||||
); // Round to the nearest whole minute
|
||||
} else if (searchObj.meta.resultGrid.chartInterval.includes("hour")) {
|
||||
startTimeDate.setHours(startTimeDate.getHours() + 1);
|
||||
|
@ -1963,13 +1966,12 @@ const useLogs = () => {
|
|||
page_type: searchObj.data.stream.streamType,
|
||||
traceparent,
|
||||
},
|
||||
"UI"
|
||||
"UI",
|
||||
)
|
||||
.then(async (res) => {
|
||||
// check for total records update for the partition and update pagination accordingly
|
||||
// searchObj.data.queryResults.partitionDetail.partitions.forEach(
|
||||
// (item: any, index: number) => {
|
||||
removeTraceId(traceId);
|
||||
searchObj.data.queryResults.scan_size = res.data.scan_size;
|
||||
searchObj.data.queryResults.took += res.data.took;
|
||||
for (const [
|
||||
|
@ -2032,6 +2034,9 @@ const useLogs = () => {
|
|||
trace_id = "";
|
||||
}
|
||||
reject(false);
|
||||
})
|
||||
.finally(() => {
|
||||
removeTraceId(traceId);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -2052,7 +2057,7 @@ const useLogs = () => {
|
|||
|
||||
const getPaginatedData = async (
|
||||
queryReq: any,
|
||||
appendResult: boolean = false
|
||||
appendResult: boolean = false,
|
||||
) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// // set track_total_hits true for first request of partition to get total records in partition
|
||||
|
@ -2067,6 +2072,14 @@ const useLogs = () => {
|
|||
// ) {
|
||||
// delete queryReq.query.track_total_hits;
|
||||
// }
|
||||
|
||||
if (searchObj.data.isOperationCancelled) {
|
||||
notificationMsg.value = "Search operation is cancelled.";
|
||||
searchObj.loading = false;
|
||||
searchObj.data.isOperationCancelled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedSQL: any = fnParsedSQL();
|
||||
searchObj.meta.resultGrid.showPagination = true;
|
||||
if (searchObj.meta.sqlMode == true) {
|
||||
|
@ -2099,10 +2112,9 @@ const useLogs = () => {
|
|||
page_type: searchObj.data.stream.streamType,
|
||||
traceparent,
|
||||
},
|
||||
"UI"
|
||||
"UI",
|
||||
)
|
||||
.then(async (res) => {
|
||||
removeTraceId(traceId);
|
||||
if (
|
||||
res.data.hasOwnProperty("function_error") &&
|
||||
res.data.function_error != "" &&
|
||||
|
@ -2113,7 +2125,7 @@ const useLogs = () => {
|
|||
res.data.function_error,
|
||||
res.data.new_start_time,
|
||||
res.data.new_end_time,
|
||||
store.state.timezone
|
||||
store.state.timezone,
|
||||
);
|
||||
searchObj.data.datetime.startTime = res.data.new_start_time;
|
||||
searchObj.data.datetime.endTime = res.data.new_end_time;
|
||||
|
@ -2201,7 +2213,7 @@ const useLogs = () => {
|
|||
const lastRecordTimeStamp = parseInt(
|
||||
searchObj.data.queryResults.hits[0][
|
||||
store.state.zoConfig.timestamp_column
|
||||
]
|
||||
],
|
||||
);
|
||||
searchObj.data.queryResults.hits = res.data.hits;
|
||||
} else {
|
||||
|
@ -2274,7 +2286,7 @@ const useLogs = () => {
|
|||
`Paginated data time after response received from server took ${
|
||||
searchObjDebug["paginatedDataReceivedEndTime"] -
|
||||
searchObjDebug["paginatedDataReceivedStartTime"]
|
||||
} milliseconds to complete`
|
||||
} milliseconds to complete`,
|
||||
);
|
||||
|
||||
resolve(true);
|
||||
|
@ -2322,6 +2334,9 @@ const useLogs = () => {
|
|||
}
|
||||
|
||||
reject(false);
|
||||
})
|
||||
.finally(() => {
|
||||
removeTraceId(traceId);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -2350,6 +2365,19 @@ const useLogs = () => {
|
|||
|
||||
const getHistogramQueryData = (queryReq: any) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (searchObj.data.isOperationCancelled) {
|
||||
searchObj.loadingHistogram = false;
|
||||
searchObj.data.isOperationCancelled = false;
|
||||
|
||||
searchObj.data.histogram.errorCode = 429;
|
||||
notificationMsg.value = "Search operation was cancelled";
|
||||
searchObj.data.histogram.errorMsg =
|
||||
"Error while fetching histogram data.";
|
||||
searchObj.data.histogram.errorDetail = "Search operation was cancelled";
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const dismiss = () => {};
|
||||
try {
|
||||
const { traceparent, traceId } = generateTraceContext();
|
||||
|
@ -2364,7 +2392,7 @@ const useLogs = () => {
|
|||
page_type: searchObj.data.stream.streamType,
|
||||
traceparent,
|
||||
},
|
||||
"UI"
|
||||
"UI",
|
||||
)
|
||||
.then(async (res: any) => {
|
||||
removeTraceId(traceId);
|
||||
|
@ -2395,13 +2423,13 @@ const useLogs = () => {
|
|||
`Histogram processing after data received took ${
|
||||
searchObjDebug["histogramProcessingEndTime"] -
|
||||
searchObjDebug["histogramProcessingStartTime"]
|
||||
} milliseconds to complete`
|
||||
} milliseconds to complete`,
|
||||
);
|
||||
console.log(
|
||||
`Entire Histogram took ${
|
||||
searchObjDebug["histogramEndTime"] -
|
||||
searchObjDebug["histogramStartTime"]
|
||||
} milliseconds to complete`
|
||||
} milliseconds to complete`,
|
||||
);
|
||||
console.log("=================== End Debug ===================");
|
||||
dismiss();
|
||||
|
@ -2454,6 +2482,9 @@ const useLogs = () => {
|
|||
}
|
||||
|
||||
reject(false);
|
||||
})
|
||||
.finally(() => {
|
||||
removeTraceId(traceId);
|
||||
});
|
||||
} catch (e: any) {
|
||||
dismiss();
|
||||
|
@ -2540,15 +2571,15 @@ const useLogs = () => {
|
|||
selectedStreamValues.length > 1
|
||||
? searchObj.data.stream.expandGroupRows[stream]
|
||||
: selectedStreamValues.length > 1
|
||||
? false
|
||||
: true,
|
||||
])
|
||||
? false
|
||||
: true,
|
||||
]),
|
||||
),
|
||||
};
|
||||
searchObj.data.stream.expandGroupRowsFieldCount = {
|
||||
common: 0,
|
||||
...Object.fromEntries(
|
||||
selectedStreamValues.sort().map((stream: any) => [stream, 0])
|
||||
selectedStreamValues.sort().map((stream: any) => [stream, 0]),
|
||||
),
|
||||
};
|
||||
|
||||
|
@ -2610,7 +2641,7 @@ const useLogs = () => {
|
|||
" hours"
|
||||
: searchObj.data.datetime.queryRangeRestrictionInHour +
|
||||
" hour",
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -2629,13 +2660,13 @@ const useLogs = () => {
|
|||
searchObj.meta.hasUserDefinedSchemas = true;
|
||||
if (store.state.zoConfig.hasOwnProperty("timestamp_column")) {
|
||||
userDefineSchemaSettings.push(
|
||||
store.state.zoConfig?.timestamp_column
|
||||
store.state.zoConfig?.timestamp_column,
|
||||
);
|
||||
}
|
||||
|
||||
if (store.state.zoConfig.hasOwnProperty("all_fields_name")) {
|
||||
userDefineSchemaSettings.push(
|
||||
store.state.zoConfig?.all_fields_name
|
||||
store.state.zoConfig?.all_fields_name,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
@ -2656,15 +2687,15 @@ const useLogs = () => {
|
|||
searchObj.organizationIdetifier + "_" + stream.name
|
||||
]
|
||||
: environmentInterestingFields.length > 0
|
||||
? [...environmentInterestingFields]
|
||||
: [...schemaInterestingFields];
|
||||
? [...environmentInterestingFields]
|
||||
: [...schemaInterestingFields];
|
||||
|
||||
searchObj.data.stream.interestingFieldList.push(
|
||||
...streamInterestingFieldsLocal
|
||||
...streamInterestingFieldsLocal,
|
||||
);
|
||||
|
||||
const intField = new Set(
|
||||
searchObj.data.stream.interestingFieldList
|
||||
searchObj.data.stream.interestingFieldList,
|
||||
);
|
||||
searchObj.data.stream.interestingFieldList = [...intField];
|
||||
|
||||
|
@ -2709,7 +2740,7 @@ const useLogs = () => {
|
|||
schemaMaps[schemaFieldsIndex].streams.length > 0
|
||||
) {
|
||||
fieldObj.streams.push(
|
||||
...schemaMaps[schemaFieldsIndex].streams
|
||||
...schemaMaps[schemaFieldsIndex].streams,
|
||||
);
|
||||
searchObj.data.stream.expandGroupRowsFieldCount[
|
||||
schemaMaps[schemaFieldsIndex].streams[0]
|
||||
|
@ -2731,7 +2762,7 @@ const useLogs = () => {
|
|||
schemaMaps.splice(schemaFieldsIndex, 1);
|
||||
} else if (commonSchemaFieldsIndex > -1) {
|
||||
commonSchemaMaps[commonSchemaFieldsIndex].streams.push(
|
||||
stream.name
|
||||
stream.name,
|
||||
);
|
||||
// searchObj.data.stream.expandGroupRowsFieldCount["common"] =
|
||||
// searchObj.data.stream.expandGroupRowsFieldCount[
|
||||
|
@ -2768,7 +2799,7 @@ const useLogs = () => {
|
|||
schemaMaps[schemaFieldsIndex].streams.length > 0
|
||||
) {
|
||||
fieldObj.streams.push(
|
||||
...schemaMaps[schemaFieldsIndex].streams
|
||||
...schemaMaps[schemaFieldsIndex].streams,
|
||||
);
|
||||
searchObj.data.stream.expandGroupRowsFieldCount[
|
||||
schemaMaps[schemaFieldsIndex].streams[0]
|
||||
|
@ -2789,7 +2820,7 @@ const useLogs = () => {
|
|||
schemaMaps.splice(schemaFieldsIndex, 1);
|
||||
} else if (commonSchemaFieldsIndex > -1) {
|
||||
commonSchemaMaps[commonSchemaFieldsIndex].streams.push(
|
||||
stream.name
|
||||
stream.name,
|
||||
);
|
||||
// searchObj.data.stream.expandGroupRowsFieldCount["common"] =
|
||||
// searchObj.data.stream.expandGroupRowsFieldCount["common"] +
|
||||
|
@ -2841,17 +2872,17 @@ const useLogs = () => {
|
|||
maxIndex: string | number,
|
||||
obj: {},
|
||||
currentIndex: any,
|
||||
array: { [x: string]: {} }
|
||||
array: { [x: string]: {} },
|
||||
) => {
|
||||
const numAttributes = Object.keys(obj).length;
|
||||
const maxNumAttributes = Object.keys(
|
||||
array[maxIndex]
|
||||
array[maxIndex],
|
||||
).length;
|
||||
return numAttributes > maxNumAttributes
|
||||
? currentIndex
|
||||
: maxIndex;
|
||||
},
|
||||
0
|
||||
0,
|
||||
);
|
||||
const recordwithMaxAttribute =
|
||||
searchObj.data.queryResults.hits[maxAttributesIndex];
|
||||
|
@ -2906,7 +2937,7 @@ const useLogs = () => {
|
|||
} operation took ${
|
||||
searchObjDebug["extractFieldsEndTime"] -
|
||||
searchObjDebug["extractFieldsStartTime"]
|
||||
} milliseconds to complete`
|
||||
} milliseconds to complete`,
|
||||
);
|
||||
} catch (e: any) {
|
||||
searchObj.loadingStream = false;
|
||||
|
@ -2934,7 +2965,7 @@ const useLogs = () => {
|
|||
logFieldSelectedValue.push(
|
||||
...logFilterField[
|
||||
`${store.state.selectedOrganization.identifier}_${stream}`
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
const selectedFields = (logFilterField && logFieldSelectedValue) || [];
|
||||
|
@ -2953,11 +2984,11 @@ const useLogs = () => {
|
|||
(searchObj.meta.sqlMode == true &&
|
||||
parsedSQL.hasOwnProperty("columns") &&
|
||||
searchObj.data.queryResults?.hits[0].hasOwnProperty(
|
||||
store.state.zoConfig.timestamp_column
|
||||
store.state.zoConfig.timestamp_column,
|
||||
)) ||
|
||||
searchObj.meta.sqlMode == false ||
|
||||
searchObj.data.stream.selectedFields.includes(
|
||||
store.state.zoConfig.timestamp_column
|
||||
store.state.zoConfig.timestamp_column,
|
||||
)
|
||||
) {
|
||||
searchObj.data.resultGrid.columns.push({
|
||||
|
@ -2966,13 +2997,13 @@ const useLogs = () => {
|
|||
timestampToTimezoneDate(
|
||||
row[store.state.zoConfig.timestamp_column] / 1000,
|
||||
store.state.timezone,
|
||||
"yyyy-MM-dd HH:mm:ss.SSS"
|
||||
"yyyy-MM-dd HH:mm:ss.SSS",
|
||||
),
|
||||
prop: (row: any) =>
|
||||
timestampToTimezoneDate(
|
||||
row[store.state.zoConfig.timestamp_column] / 1000,
|
||||
store.state.timezone,
|
||||
"yyyy-MM-dd HH:mm:ss.SSS"
|
||||
"yyyy-MM-dd HH:mm:ss.SSS",
|
||||
),
|
||||
label: t("search.timestamp") + ` (${store.state.timezone})`,
|
||||
align: "left",
|
||||
|
@ -2999,13 +3030,13 @@ const useLogs = () => {
|
|||
timestampToTimezoneDate(
|
||||
row[store.state.zoConfig.timestamp_column] / 1000,
|
||||
store.state.timezone,
|
||||
"yyyy-MM-dd HH:mm:ss.SSS"
|
||||
"yyyy-MM-dd HH:mm:ss.SSS",
|
||||
),
|
||||
prop: (row: any) =>
|
||||
timestampToTimezoneDate(
|
||||
row[store.state.zoConfig.timestamp_column] / 1000,
|
||||
store.state.timezone,
|
||||
"yyyy-MM-dd HH:mm:ss.SSS"
|
||||
"yyyy-MM-dd HH:mm:ss.SSS",
|
||||
),
|
||||
label: t("search.timestamp") + ` (${store.state.timezone})`,
|
||||
align: "left",
|
||||
|
@ -3050,7 +3081,7 @@ const useLogs = () => {
|
|||
|
||||
let totalCount = Math.max(
|
||||
searchObj.data.queryResults.hits.length,
|
||||
searchObj.data.queryResults.total
|
||||
searchObj.data.queryResults.total,
|
||||
);
|
||||
if (searchObj.meta.resultGrid.showPagination == false) {
|
||||
endCount = searchObj.data.queryResults.hits.length;
|
||||
|
@ -3062,7 +3093,7 @@ const useLogs = () => {
|
|||
) {
|
||||
endCount = Math.min(
|
||||
startCount + searchObj.meta.resultGrid.rowsPerPage - 1,
|
||||
totalCount
|
||||
totalCount,
|
||||
);
|
||||
} else {
|
||||
endCount = searchObj.meta.resultGrid.rowsPerPage * (currentPage + 1);
|
||||
|
@ -3140,7 +3171,7 @@ const useLogs = () => {
|
|||
histogramResults.map((item: any) => [
|
||||
item.zo_sql_key,
|
||||
JSON.parse(JSON.stringify(item)),
|
||||
])
|
||||
]),
|
||||
);
|
||||
|
||||
searchObj.data.queryResults.aggs.forEach((item: any) => {
|
||||
|
@ -3163,11 +3194,11 @@ const useLogs = () => {
|
|||
unparsed_x_data.push(bucket.zo_sql_key);
|
||||
// const histDate = new Date(bucket.zo_sql_key);
|
||||
xData.push(
|
||||
histogramDateTimezone(bucket.zo_sql_key, store.state.timezone)
|
||||
histogramDateTimezone(bucket.zo_sql_key, store.state.timezone),
|
||||
);
|
||||
// xData.push(Math.floor(histDate.getTime()))
|
||||
yData.push(parseInt(bucket.zo_sql_num, 10));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
searchObj.data.queryResults.total = num_records;
|
||||
|
@ -3223,7 +3254,7 @@ const useLogs = () => {
|
|||
// }
|
||||
parsedSQL.where = null;
|
||||
sqlContext.push(
|
||||
b64EncodeUnicode(parser.sqlify(parsedSQL).replace(/`/g, '"'))
|
||||
b64EncodeUnicode(parser.sqlify(parsedSQL).replace(/`/g, '"')),
|
||||
);
|
||||
} else {
|
||||
const parseQuery = query.split("|");
|
||||
|
@ -3245,8 +3276,8 @@ const useLogs = () => {
|
|||
const streamsData: any = searchObj.data.stream.selectedStream.filter(
|
||||
(streams: any) =>
|
||||
!searchObj.data.stream.missingStreamMultiStreamFilter.includes(
|
||||
streams
|
||||
)
|
||||
streams,
|
||||
),
|
||||
);
|
||||
|
||||
let finalQuery: string = "";
|
||||
|
@ -3273,7 +3304,7 @@ const useLogs = () => {
|
|||
|
||||
finalQuery = finalQuery.replace(
|
||||
"[FIELD_LIST]",
|
||||
`'${item}' as _stream_name` + queryFieldList
|
||||
`'${item}' as _stream_name` + queryFieldList,
|
||||
);
|
||||
sqlContext.push(b64EncodeUnicode(finalQuery));
|
||||
});
|
||||
|
@ -3319,8 +3350,6 @@ const useLogs = () => {
|
|||
traceparent,
|
||||
})
|
||||
.then((res) => {
|
||||
removeTraceId(traceId);
|
||||
|
||||
searchObj.loading = false;
|
||||
searchObj.data.histogram.chartParams.title = "";
|
||||
if (res.data.from > 0) {
|
||||
|
@ -3399,7 +3428,10 @@ const useLogs = () => {
|
|||
trace_id = "";
|
||||
}
|
||||
})
|
||||
.finally(() => (searchObj.loading = false));
|
||||
.finally(() => {
|
||||
removeTraceId(traceId);
|
||||
searchObj.loading = false;
|
||||
});
|
||||
} catch (e: any) {
|
||||
searchObj.loading = false;
|
||||
showErrorNotification("Error while fetching data");
|
||||
|
@ -3529,8 +3561,8 @@ const useLogs = () => {
|
|||
}
|
||||
|
||||
const date = {
|
||||
startTime: queryParams.from,
|
||||
endTime: queryParams.to,
|
||||
startTime: Number(queryParams.from),
|
||||
endTime: Number(queryParams.to),
|
||||
relativeTimePeriod: queryParams.period || null,
|
||||
type: queryParams.period ? "relative" : "absolute",
|
||||
};
|
||||
|
@ -3583,7 +3615,7 @@ const useLogs = () => {
|
|||
|
||||
if (queryParams.stream) {
|
||||
searchObj.data.stream.selectedStream.push(
|
||||
...queryParams.stream.split(",")
|
||||
...queryParams.stream.split(","),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -3714,7 +3746,7 @@ const useLogs = () => {
|
|||
? queryStr != ""
|
||||
? queryStr
|
||||
: `SELECT [FIELD_LIST] FROM "${searchObj.data.stream.selectedStream.join(
|
||||
","
|
||||
",",
|
||||
)}"`
|
||||
: "";
|
||||
|
||||
|
@ -3723,7 +3755,7 @@ const useLogs = () => {
|
|||
const streamData: any = await getStream(
|
||||
stream,
|
||||
searchObj.data.stream.streamType || "logs",
|
||||
true
|
||||
true,
|
||||
);
|
||||
|
||||
if (streamData.schema != undefined) {
|
||||
|
@ -3764,7 +3796,7 @@ const useLogs = () => {
|
|||
) {
|
||||
query = query.replace(
|
||||
"[FIELD_LIST]",
|
||||
searchObj.data.stream.interestingFieldList.join(",")
|
||||
searchObj.data.stream.interestingFieldList.join(","),
|
||||
);
|
||||
} else {
|
||||
query = query.replace("[FIELD_LIST]", "*");
|
||||
|
@ -3813,7 +3845,7 @@ const useLogs = () => {
|
|||
sql: string,
|
||||
column: string,
|
||||
type: "ASC" | "DESC",
|
||||
streamName: string
|
||||
streamName: string,
|
||||
) => {
|
||||
// Parse the SQL query into an AST
|
||||
try {
|
||||
|
@ -3830,7 +3862,7 @@ const useLogs = () => {
|
|||
|
||||
// Check if _timestamp is in the SELECT clause if not SELECT *
|
||||
const includesTimestamp = !!parsedQuery.columns.find(
|
||||
(col: any) => col?.expr?.column === column || col?.expr?.column === "*"
|
||||
(col: any) => col?.expr?.column === column || col?.expr?.column === "*",
|
||||
);
|
||||
|
||||
// If ORDER BY is present and doesn't include _timestamp, append it
|
||||
|
@ -3851,7 +3883,7 @@ const useLogs = () => {
|
|||
// Convert the AST back to a SQL string, replacing backtics with empty strings and table name with double quotes
|
||||
return quoteTableNameDirectly(
|
||||
parser.sqlify(parsedQuery).replace(/`/g, ""),
|
||||
streamName
|
||||
streamName,
|
||||
);
|
||||
} catch (err) {
|
||||
return sql;
|
||||
|
@ -3901,26 +3933,29 @@ const useLogs = () => {
|
|||
const removeTraceId = (traceId: string) => {
|
||||
searchObj.data.searchRequestTraceIds =
|
||||
searchObj.data.searchRequestTraceIds.filter(
|
||||
(id: string) => id !== traceId
|
||||
(id: string) => id !== traceId,
|
||||
);
|
||||
};
|
||||
|
||||
const cancelQuery = () => {
|
||||
const tracesIds = [...searchObj.data.searchRequestTraceIds];
|
||||
searchObj.data.isOperationCancelled = true;
|
||||
searchService
|
||||
.delete_running_queries(
|
||||
store.state.selectedOrganization.identifier,
|
||||
searchObj.data.searchRequestTraceIds
|
||||
searchObj.data.searchRequestTraceIds,
|
||||
)
|
||||
.then((res) => {
|
||||
const isCancelled = res.data.some((item: any) => item.is_success);
|
||||
$q.notify({
|
||||
message: isCancelled
|
||||
? "Running query cancelled successfully"
|
||||
: "Query execution was completed before cancellation.",
|
||||
color: "positive",
|
||||
position: "bottom",
|
||||
timeout: 1500,
|
||||
});
|
||||
if (isCancelled) {
|
||||
searchObj.data.isOperationCancelled = false;
|
||||
$q.notify({
|
||||
message: "Running query cancelled successfully",
|
||||
color: "positive",
|
||||
position: "bottom",
|
||||
timeout: 4000,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error: any) => {
|
||||
$q.notify({
|
||||
|
@ -3932,8 +3967,10 @@ const useLogs = () => {
|
|||
});
|
||||
})
|
||||
.finally(() => {
|
||||
if (searchObj.loading) searchObj.loading = false;
|
||||
if (searchObj.loadingHistogram) searchObj.loadingHistogram = false;
|
||||
searchObj.data.searchRequestTraceIds =
|
||||
searchObj.data.searchRequestTraceIds.filter(
|
||||
(id: string) => !tracesIds.includes(id),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -14,7 +14,14 @@
|
|||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { reactive } from "vue";
|
||||
import { useLocalTraceFilterField } from "@/utils/zincutils";
|
||||
import {
|
||||
b64EncodeStandard,
|
||||
b64EncodeUnicode,
|
||||
useLocalTraceFilterField,
|
||||
} from "@/utils/zincutils";
|
||||
import { useStore } from "vuex";
|
||||
import { useRouter } from "vue-router";
|
||||
import { copyToClipboard, useQuasar } from "quasar";
|
||||
|
||||
const defaultObject = {
|
||||
organizationIdetifier: "",
|
||||
|
@ -69,6 +76,7 @@ const defaultObject = {
|
|||
},
|
||||
scrollInfo: {},
|
||||
serviceColors: {} as any,
|
||||
redirectedFromLogs: false,
|
||||
},
|
||||
data: {
|
||||
query: "",
|
||||
|
@ -121,13 +129,18 @@ const defaultObject = {
|
|||
histogramHide: false,
|
||||
},
|
||||
traceDetails: {
|
||||
selectedTrace: null,
|
||||
selectedTrace: null as {
|
||||
trace_id: string;
|
||||
trace_start_time: number;
|
||||
trace_end_time: number;
|
||||
} | null,
|
||||
traceId: "",
|
||||
spanList: [],
|
||||
loading: false,
|
||||
selectedSpanId: "" as String | null,
|
||||
expandedSpans: [] as String[],
|
||||
showSpanDetails: false,
|
||||
selectedLogStreams: [] as String[],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -135,6 +148,10 @@ const defaultObject = {
|
|||
const searchObj = reactive(Object.assign({}, defaultObject));
|
||||
|
||||
const useTraces = () => {
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
const $q = useQuasar();
|
||||
|
||||
const resetSearchObj = () => {
|
||||
// delete searchObj.data;
|
||||
searchObj.data.errorMsg = "No stream found in selected organization!";
|
||||
|
@ -169,7 +186,128 @@ const useTraces = () => {
|
|||
useLocalTraceFilterField(selectedFields);
|
||||
};
|
||||
|
||||
return { searchObj, resetSearchObj, updatedLocalLogFilterField };
|
||||
function getUrlQueryParams(getShareLink: boolean = false) {
|
||||
const date = searchObj.data.datetime;
|
||||
const query: any = {};
|
||||
|
||||
query["stream"] = searchObj.data.stream.selectedStream.value;
|
||||
|
||||
if (date.type === "relative" && !getShareLink) {
|
||||
query["period"] = date.relativeTimePeriod;
|
||||
} else {
|
||||
query["from"] = date.startTime;
|
||||
query["to"] = date.endTime;
|
||||
}
|
||||
|
||||
query["query"] = b64EncodeUnicode(searchObj.data.editorValue);
|
||||
|
||||
query["filter_type"] = searchObj.meta.filterType;
|
||||
|
||||
query["org_identifier"] = store.state.selectedOrganization.identifier;
|
||||
|
||||
query["trace_id"] = router.currentRoute.value.query.trace_id;
|
||||
|
||||
if (router.currentRoute.value.query.span_id)
|
||||
query["span_id"] = router.currentRoute.value.query.span_id;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
const copyTracesUrl = (
|
||||
customTimeRange: { from: string; to: string } | null = null,
|
||||
) => {
|
||||
const queryParams = getUrlQueryParams(true);
|
||||
|
||||
if (customTimeRange) {
|
||||
queryParams.from = customTimeRange.from;
|
||||
queryParams.to = customTimeRange.to;
|
||||
}
|
||||
|
||||
const queryString = Object.entries(queryParams)
|
||||
.map(
|
||||
([key, value]: any) =>
|
||||
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
|
||||
)
|
||||
.join("&");
|
||||
|
||||
let shareURL = window.location.origin + window.location.pathname;
|
||||
|
||||
if (queryString != "") {
|
||||
shareURL += "?" + queryString;
|
||||
}
|
||||
|
||||
copyToClipboard(shareURL)
|
||||
.then(() => {
|
||||
$q.notify({
|
||||
type: "positive",
|
||||
message: "Link Copied Successfully!",
|
||||
timeout: 5000,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
$q.notify({
|
||||
type: "negative",
|
||||
message: "Error while copy link.",
|
||||
timeout: 5000,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Function to build query details for navigation
|
||||
const buildQueryDetails = (span: any, isSpan: boolean = true) => {
|
||||
const spanIdField =
|
||||
store.state.organizationData?.organizationSettings?.span_id_field_name;
|
||||
const traceIdField =
|
||||
store.state.organizationData?.organizationSettings?.trace_id_field_name;
|
||||
const traceId = searchObj.data.traceDetails.selectedTrace?.trace_id;
|
||||
|
||||
let query: string = isSpan
|
||||
? `${spanIdField}='${span.spanId || span.span_id}' ${
|
||||
traceId ? `AND ${traceIdField}='${traceId}'` : ""
|
||||
}`
|
||||
: `${traceIdField}='${traceId}'`;
|
||||
|
||||
if (query) query = b64EncodeStandard(query) as string;
|
||||
|
||||
return {
|
||||
stream: searchObj.data.traceDetails.selectedLogStreams.join(","),
|
||||
from: span.startTimeMs * 1000 - 60000000,
|
||||
to: span.endTimeMs * 1000 + 60000000,
|
||||
refresh: 0,
|
||||
query,
|
||||
orgIdentifier: store.state.selectedOrganization.identifier,
|
||||
};
|
||||
};
|
||||
|
||||
// Function to navigate to logs with the provided query details
|
||||
const navigateToLogs = (queryDetails: any) => {
|
||||
router.push({
|
||||
path: "/logs",
|
||||
query: {
|
||||
stream_type: "logs",
|
||||
stream: queryDetails.stream,
|
||||
from: queryDetails.from,
|
||||
to: queryDetails.to,
|
||||
refresh: queryDetails.refresh,
|
||||
sql_mode: "false",
|
||||
query: queryDetails.query,
|
||||
org_identifier: queryDetails.orgIdentifier,
|
||||
show_histogram: "true",
|
||||
type: "trace_explorer",
|
||||
quick_mode: "false",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
searchObj,
|
||||
resetSearchObj,
|
||||
updatedLocalLogFilterField,
|
||||
getUrlQueryParams,
|
||||
copyTracesUrl,
|
||||
buildQueryDetails,
|
||||
navigateToLogs,
|
||||
};
|
||||
};
|
||||
|
||||
export default useTraces;
|
||||
|
|
|
@ -107,7 +107,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
>
|
||||
</div>
|
||||
<ThemeSwitcher></ThemeSwitcher>
|
||||
<template v-if="config.isCloud !== 'true' && !store.state.zoConfig?.custom_hide_menus?.split(',')?.includes('openapi')">
|
||||
<template
|
||||
v-if="
|
||||
config.isCloud !== 'true' &&
|
||||
!store.state.zoConfig?.custom_hide_menus
|
||||
?.split(',')
|
||||
?.includes('openapi')
|
||||
"
|
||||
>
|
||||
<q-btn
|
||||
class="q-ml-xs no-border"
|
||||
size="13px"
|
||||
|
@ -888,6 +895,10 @@ export default defineComponent({
|
|||
//scrape interval will be in number
|
||||
store.dispatch("setOrganizationSettings", {
|
||||
scrape_interval: orgSettings?.data?.data?.scrape_interval ?? 15,
|
||||
span_id_field_name:
|
||||
orgSettings?.data?.data?.span_id_field_name ?? "spanId",
|
||||
trace_id_field_name:
|
||||
orgSettings?.data?.data?.trace_id_field_name ?? "traceId",
|
||||
});
|
||||
} catch (error) {}
|
||||
return;
|
||||
|
|
|
@ -54,7 +54,8 @@
|
|||
"expand": "Expand",
|
||||
"date": "Date",
|
||||
"description": "Description",
|
||||
"cancelChanges": "Cancel Changes"
|
||||
"cancelChanges": "Cancel Changes",
|
||||
"logs": "Logs"
|
||||
},
|
||||
"search": {
|
||||
"selectIndex": "Select Stream First",
|
||||
|
@ -117,7 +118,8 @@
|
|||
"allFieldsWarningMsg": "Searches can be slow when the number of fields in the schema are extremely high, please choose the required fields appropriately.",
|
||||
"regionFilterMsg": "Search Region/Cluster",
|
||||
"queryRangeRestrictionMsg": "The query range is restricted to {range}. Please adjust the date range accordingly. Otherwise, the start date will be automatically adjusted to fit within the allowed limit.",
|
||||
"cancel": "Cancel query"
|
||||
"cancel": "Cancel query",
|
||||
"viewTrace": "View Trace"
|
||||
},
|
||||
"menu": {
|
||||
"home": "Home",
|
||||
|
@ -843,7 +845,7 @@
|
|||
"updateuserKey": "Update User Key",
|
||||
"id": "Id",
|
||||
"update": "Update",
|
||||
"generalLabel": "Settings",
|
||||
"generalLabel": "General Settings",
|
||||
"queryManagement": "Query Management",
|
||||
"apikeyLabel": "API Keys",
|
||||
"generalPageTitle": "General Settings",
|
||||
|
@ -855,7 +857,11 @@
|
|||
"deleteLogoMessage": "Are you sure you want to delete this logo image? Click Ok to delete.",
|
||||
"deleteLogo": "Delete Logo",
|
||||
"logo": "Logo",
|
||||
"customLogoText": "Custom Logo Text"
|
||||
"customLogoText": "Custom Logo Text",
|
||||
"orgLabel": "Organization Parameters",
|
||||
"logDetails": "Log Details",
|
||||
"traceIdFieldName": "Trace ID Field Name",
|
||||
"spanIdFieldName": "Span ID Field Name"
|
||||
},
|
||||
"reports": {
|
||||
"header": "Reports",
|
||||
|
@ -886,5 +892,9 @@
|
|||
"queryRange": "Query Range",
|
||||
"refreshQuery": "Refresh Query",
|
||||
"queryType": "Query Type"
|
||||
},
|
||||
"traces": {
|
||||
"viewLogs": "View Logs",
|
||||
"backToLogs": "Back to Logs"
|
||||
}
|
||||
}
|
|
@ -87,6 +87,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
@copy="copyContentToClipboard"
|
||||
@add-field-to-table="addFieldToTable"
|
||||
@add-search-term="toggleIncludeSearchTerm"
|
||||
@view-trace="viewTrace"
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-tab-panel>
|
||||
|
@ -286,7 +287,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
/>
|
||||
</div>
|
||||
<div
|
||||
v-show="streamType !== 'enrichment_tables' && searchObj.data.stream.selectedStream.length <= 1"
|
||||
v-show="
|
||||
streamType !== 'enrichment_tables' &&
|
||||
searchObj.data.stream.selectedStream.length <= 1
|
||||
"
|
||||
class="col-8 row justify-center align-center q-gutter-sm"
|
||||
>
|
||||
<div style="line-height: 40px; font-weight: bold">
|
||||
|
@ -359,6 +363,7 @@ export default defineComponent({
|
|||
"remove:searchterm",
|
||||
"search:timeboxed",
|
||||
"add:table",
|
||||
"view-trace",
|
||||
],
|
||||
props: {
|
||||
modelValue: {
|
||||
|
@ -467,6 +472,10 @@ export default defineComponent({
|
|||
emit("add:table", value);
|
||||
};
|
||||
|
||||
const viewTrace = () => {
|
||||
emit("view-trace");
|
||||
};
|
||||
|
||||
return {
|
||||
t,
|
||||
store,
|
||||
|
@ -483,6 +492,7 @@ export default defineComponent({
|
|||
addFieldToTable,
|
||||
searchObj,
|
||||
multiStreamFields,
|
||||
viewTrace,
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
|
|
|
@ -100,8 +100,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
class="q-mt-lg"
|
||||
>
|
||||
<h5 class="text-center">
|
||||
<q-icon name="warning"
|
||||
color="warning" size="10rem" /><br />
|
||||
<q-icon name="warning" color="warning" size="10rem" /><br />
|
||||
<div
|
||||
data-test="logs-search-filter-error-message"
|
||||
v-html="searchObj.data.filterErrMsg"
|
||||
|
@ -168,8 +167,7 @@ color="warning" size="10rem" /><br />
|
|||
data-test="logs-search-no-stream-selected-text"
|
||||
class="text-center col-10 q-mx-none"
|
||||
>
|
||||
<q-icon name="info"
|
||||
color="primary" size="md" /> Select a
|
||||
<q-icon name="info" color="primary" size="md" /> Select a
|
||||
stream and press 'Run query' to continue. Additionally, you
|
||||
can apply additional filters and adjust the date range to
|
||||
enhance search.
|
||||
|
@ -188,8 +186,7 @@ color="primary" size="md" /> Select a
|
|||
data-test="logs-search-error-message"
|
||||
class="text-center q-ma-none col-10"
|
||||
>
|
||||
<q-icon name="info"
|
||||
color="primary" size="md" />
|
||||
<q-icon name="info" color="primary" size="md" />
|
||||
{{ t("search.noRecordFound") }}
|
||||
<q-btn
|
||||
v-if="searchObj.data.errorMsg != ''"
|
||||
|
@ -213,8 +210,7 @@ color="primary" size="md" />
|
|||
data-test="logs-search-error-message"
|
||||
class="text-center q-ma-none col-10"
|
||||
>
|
||||
<q-icon name="info"
|
||||
color="primary" size="md" />
|
||||
<q-icon name="info" color="primary" size="md" />
|
||||
{{ t("search.applySearch") }}
|
||||
</h6>
|
||||
</div>
|
||||
|
@ -301,16 +297,13 @@ export default defineComponent({
|
|||
name: "PageSearch",
|
||||
components: {
|
||||
SearchBar: defineAsyncComponent(
|
||||
() => import("@/plugins/logs/SearchBar.vue")
|
||||
() => import("@/plugins/logs/SearchBar.vue"),
|
||||
),
|
||||
IndexList: defineAsyncComponent(
|
||||
() => import("@/plugins/logs/IndexList.vue")
|
||||
() => import("@/plugins/logs/IndexList.vue"),
|
||||
),
|
||||
SearchResult: defineAsyncComponent(
|
||||
() => import("@/plugins/logs/SearchResult.vue")
|
||||
),
|
||||
ConfirmDialog: defineAsyncComponent(
|
||||
() => import("@/components/ConfirmDialog.vue")
|
||||
() => import("@/plugins/logs/SearchResult.vue"),
|
||||
),
|
||||
SanitizedHtmlRenderer,
|
||||
VisualizeLogsQuery,
|
||||
|
@ -350,6 +343,11 @@ export default defineComponent({
|
|||
// this.searchObj.data.resultGrid.currentPage =
|
||||
// this.searchObj.data.resultGrid.currentPage + 1;
|
||||
this.searchObj.loading = true;
|
||||
|
||||
// As page count request was getting fired on chaning date records per page instead of histogram,
|
||||
// so added this condition to avoid that
|
||||
this.searchObj.meta.refreshHistogram = true;
|
||||
|
||||
await this.getQueryData(false);
|
||||
this.refreshHistogramChart();
|
||||
|
||||
|
@ -521,6 +519,18 @@ export default defineComponent({
|
|||
queryParams.stream_type !== searchObj.data.stream.streamType ||
|
||||
queryParams.stream !== searchObj.data.stream.selectedStream.join(",");
|
||||
|
||||
if (queryParams.type === "trace_explorer") {
|
||||
searchObj.organizationIdetifier = queryParams.org_identifier;
|
||||
searchObj.data.stream.selectedStream.value = queryParams.stream;
|
||||
searchObj.data.stream.streamType = queryParams.stream_type;
|
||||
resetSearchObj();
|
||||
resetStreamData();
|
||||
restoreUrlQueryParams();
|
||||
loadLogsData();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
isStreamChanged &&
|
||||
queryParams.type === "stream_explorer" &&
|
||||
|
@ -596,7 +606,7 @@ export default defineComponent({
|
|||
searchObj.meta.pageType = "logs";
|
||||
loadLogsData();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const importSqlParser = async () => {
|
||||
|
@ -687,14 +697,14 @@ export default defineComponent({
|
|||
const streamData: any = getStream(
|
||||
searchObj.data.stream.selectedStream[0],
|
||||
searchObj.data.stream.streamType || "logs",
|
||||
true
|
||||
true,
|
||||
);
|
||||
searchObj.data.stream.selectedStreamFields = streamData.schema;
|
||||
}
|
||||
|
||||
const streamFieldNames: any =
|
||||
searchObj.data.stream.selectedStreamFields.map(
|
||||
(item: any) => item.name
|
||||
(item: any) => item.name,
|
||||
);
|
||||
|
||||
for (
|
||||
|
@ -714,12 +724,12 @@ export default defineComponent({
|
|||
) {
|
||||
searchObj.data.query = searchObj.data.query.replace(
|
||||
"[FIELD_LIST]",
|
||||
searchObj.data.stream.interestingFieldList.join(",")
|
||||
searchObj.data.stream.interestingFieldList.join(","),
|
||||
);
|
||||
} else {
|
||||
searchObj.data.query = searchObj.data.query.replace(
|
||||
"[FIELD_LIST]",
|
||||
"*"
|
||||
"*",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -728,7 +738,7 @@ export default defineComponent({
|
|||
searchObj.data.query,
|
||||
store.state.zoConfig.timestamp_column,
|
||||
"DESC",
|
||||
searchObj.data.stream.selectedStream.join(",")
|
||||
searchObj.data.stream.selectedStream.join(","),
|
||||
);
|
||||
|
||||
searchObj.data.editorValue = searchObj.data.query;
|
||||
|
@ -795,7 +805,7 @@ export default defineComponent({
|
|||
|
||||
const setInterestingFieldInSQLQuery = (
|
||||
field: any,
|
||||
isFieldExistInSQL: boolean
|
||||
isFieldExistInSQL: boolean,
|
||||
) => {
|
||||
//implement setQuery function using node-sql-parser
|
||||
//isFieldExistInSQL is used to check if the field is already present in the query or not.
|
||||
|
@ -807,7 +817,7 @@ export default defineComponent({
|
|||
let filteredData = removeFieldByName(parsedSQL.columns, field.name);
|
||||
|
||||
const index = searchObj.data.stream.interestingFieldList.indexOf(
|
||||
field.name
|
||||
field.name,
|
||||
);
|
||||
if (index > -1) {
|
||||
searchObj.data.stream.interestingFieldList.splice(index, 1);
|
||||
|
@ -844,7 +854,7 @@ export default defineComponent({
|
|||
.replace(/`/g, "")
|
||||
.replace(
|
||||
searchObj.data.stream.selectedStream[0],
|
||||
`"${searchObj.data.stream.selectedStream[0]}"`
|
||||
`"${searchObj.data.stream.selectedStream[0]}"`,
|
||||
);
|
||||
searchObj.data.query = newQuery;
|
||||
searchObj.data.editorValue = newQuery;
|
||||
|
@ -866,7 +876,7 @@ export default defineComponent({
|
|||
/SELECT\s+(.*?)\s+FROM/i,
|
||||
(match, fields) => {
|
||||
return `SELECT ${field_list} FROM`;
|
||||
}
|
||||
},
|
||||
);
|
||||
setQuery(searchObj.meta.quickMode);
|
||||
updateUrlQueryParams();
|
||||
|
@ -895,7 +905,7 @@ export default defineComponent({
|
|||
|
||||
if (errors.length) {
|
||||
showErrorNotification(
|
||||
"There are some errors, please fix them and try again"
|
||||
"There are some errors, please fix them and try again",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
@ -934,13 +944,13 @@ export default defineComponent({
|
|||
searchObj.meta.quickMode
|
||||
? searchObj.data.stream.interestingFieldList
|
||||
: [],
|
||||
logsQuery
|
||||
logsQuery,
|
||||
);
|
||||
}
|
||||
|
||||
const { fields, conditions, streamName } = await getFieldsFromQuery(
|
||||
logsQuery ?? "",
|
||||
store.state.zoConfig.timestamp_column ?? "_timestamp"
|
||||
store.state.zoConfig.timestamp_column ?? "_timestamp",
|
||||
);
|
||||
|
||||
// set stream type and stream name
|
||||
|
@ -972,7 +982,7 @@ export default defineComponent({
|
|||
// if x axis fields length is 2, then add 2nd x axis field to breakdown fields
|
||||
if (dashboardPanelData.data.queries[0].fields.x.length == 2) {
|
||||
dashboardPanelData.data.queries[0].fields.breakdown.push(
|
||||
dashboardPanelData.data.queries[0].fields.x[1]
|
||||
dashboardPanelData.data.queries[0].fields.x[1],
|
||||
);
|
||||
// remove 2nd x axis field from x axis fields
|
||||
dashboardPanelData.data.queries[0].fields.x.splice(1, 1);
|
||||
|
@ -1009,7 +1019,7 @@ export default defineComponent({
|
|||
// set fields and conditions
|
||||
await setFieldsAndConditions();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
|
@ -1017,9 +1027,9 @@ export default defineComponent({
|
|||
async () => {
|
||||
// await nextTick();
|
||||
visualizeChartData.value = JSON.parse(
|
||||
JSON.stringify(dashboardPanelData.data)
|
||||
JSON.stringify(dashboardPanelData.data),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
|
@ -1027,7 +1037,7 @@ export default defineComponent({
|
|||
() => {
|
||||
// rerender chart
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
|
@ -1040,7 +1050,7 @@ export default defineComponent({
|
|||
const dateTime =
|
||||
searchObj.data.datetime.type === "relative"
|
||||
? getConsumableRelativeTime(
|
||||
searchObj.data.datetime.relativeTimePeriod
|
||||
searchObj.data.datetime.relativeTimePeriod,
|
||||
)
|
||||
: cloneDeep(searchObj.data.datetime);
|
||||
|
||||
|
@ -1049,7 +1059,7 @@ export default defineComponent({
|
|||
end_time: new Date(dateTime.endTime),
|
||||
};
|
||||
},
|
||||
{ deep: true }
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const handleRunQueryFn = () => {
|
||||
|
@ -1064,7 +1074,7 @@ export default defineComponent({
|
|||
searchBarRef.value.dateTimeRef.refresh();
|
||||
|
||||
visualizeChartData.value = JSON.parse(
|
||||
JSON.stringify(dashboardPanelData.data)
|
||||
JSON.stringify(dashboardPanelData.data),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -1205,7 +1215,7 @@ export default defineComponent({
|
|||
this.getHistogramQueryData(this.searchObj.data.histogramQuery).then(
|
||||
(res: any) => {
|
||||
this.searchResultRef.reDrawChart();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,95 @@
|
|||
<template>
|
||||
<div class="q-py-xs flex justify-start q-px-md copy-log-btn">
|
||||
<div class="q-py-xs flex justify-start q-px-md log-detail-actions">
|
||||
<q-btn
|
||||
:label="t('common.copyToClipboard')"
|
||||
dense
|
||||
size="sm"
|
||||
no-caps
|
||||
class="q-px-sm"
|
||||
class="q-px-sm copy-log-btn q-mr-sm"
|
||||
icon="content_copy"
|
||||
@click="copyLogToClipboard"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="filteredStreamOptions.length"
|
||||
class="o2-input flex items-center logs-trace-selector"
|
||||
>
|
||||
<q-select
|
||||
data-test="log-search-index-list-select-stream"
|
||||
v-model="searchObj.meta.selectedTraceStream"
|
||||
:label="
|
||||
searchObj.meta.selectedTraceStream ? '' : t('search.selectIndex')
|
||||
"
|
||||
:options="filteredStreamOptions"
|
||||
data-cy="stream-selection"
|
||||
input-debounce="0"
|
||||
behavior="menu"
|
||||
filled
|
||||
size="xs"
|
||||
borderless
|
||||
dense
|
||||
fill-input
|
||||
:title="searchObj.meta.selectedTraceStream"
|
||||
>
|
||||
<template #no-option>
|
||||
<div class="o2-input log-stream-search-input">
|
||||
<q-input
|
||||
data-test="alert-list-search-input"
|
||||
v-model="streamSearchValue"
|
||||
borderless
|
||||
filled
|
||||
debounce="500"
|
||||
autofocus
|
||||
dense
|
||||
size="xs"
|
||||
@update:model-value="filterStreamFn"
|
||||
class="q-ml-auto q-mb-xs no-border q-pa-xs"
|
||||
:placeholder="t('search.searchStream')"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="search" class="cursor-pointer" />
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
<q-item>
|
||||
<q-item-section> {{ t("search.noResult") }}</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
<template #before-options>
|
||||
<div class="o2-input log-stream-search-input">
|
||||
<q-input
|
||||
data-test="alert-list-search-input"
|
||||
v-model="streamSearchValue"
|
||||
borderless
|
||||
debounce="500"
|
||||
filled
|
||||
dense
|
||||
size="xs"
|
||||
autofocus
|
||||
@update:model-value="filterStreamFn"
|
||||
class="q-ml-auto q-mb-xs no-border q-pa-xs"
|
||||
:placeholder="t('search.searchStream')"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="search" class="cursor-pointer" />
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</template>
|
||||
</q-select>
|
||||
<q-btn
|
||||
data-test="trace-view-logs-btn"
|
||||
v-close-popup="true"
|
||||
class="text-bold traces-view-logs-btn q-px-sm view-trace-btn"
|
||||
:label="t('search.viewTrace')"
|
||||
padding="sm sm"
|
||||
size="xs"
|
||||
no-caps
|
||||
dense
|
||||
:icon="outlinedAccountTree"
|
||||
@click="redirectToTraces"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="q-pl-md">
|
||||
{
|
||||
|
@ -31,14 +112,11 @@
|
|||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
v-if="searchObj.data.stream.selectedStreamFields.some(
|
||||
(item: any) =>
|
||||
item.name === key
|
||||
? item.isSchemaField
|
||||
: ''
|
||||
)
|
||||
&& multiStreamFields.includes(key)
|
||||
"
|
||||
v-if="
|
||||
searchObj.data.stream.selectedStreamFields.some((item: any) =>
|
||||
item.name === key ? item.isSchemaField : '',
|
||||
) && multiStreamFields.includes(key)
|
||||
"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label
|
||||
|
@ -58,18 +136,15 @@
|
|||
>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
v-if="searchObj.data.stream.selectedStreamFields.some(
|
||||
(item: any) =>
|
||||
item.name === key
|
||||
? item.isSchemaField
|
||||
: ''
|
||||
)
|
||||
&& multiStreamFields.includes(key)
|
||||
"
|
||||
v-if="
|
||||
searchObj.data.stream.selectedStreamFields.some((item: any) =>
|
||||
item.name === key ? item.isSchemaField : '',
|
||||
) && multiStreamFields.includes(key)
|
||||
"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label
|
||||
|
@ -135,6 +210,9 @@ import EqualIcon from "@/components/icons/EqualIcon.vue";
|
|||
import NotEqualIcon from "@/components/icons/NotEqualIcon.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import useLogs from "../../composables/useLogs";
|
||||
import { outlinedAccountTree } from "@quasar/extras/material-icons-outlined";
|
||||
import { useRouter } from "vue-router";
|
||||
import useStreams from "@/composables/useStreams";
|
||||
|
||||
export default {
|
||||
name: "JsonPreview",
|
||||
|
@ -154,6 +232,15 @@ export default {
|
|||
setup(props: any, { emit }: any) {
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
|
||||
const streamSearchValue = ref<string>("");
|
||||
|
||||
const { getStreams } = useStreams();
|
||||
|
||||
const filteredStreamOptions = ref([]);
|
||||
|
||||
const tracesStreams = ref([]);
|
||||
|
||||
const copyLogToClipboard = () => {
|
||||
emit("copy", props.value);
|
||||
};
|
||||
|
@ -174,17 +261,52 @@ export default {
|
|||
multiStreamFields.value.push(item.name);
|
||||
}
|
||||
});
|
||||
getTracesStreams();
|
||||
});
|
||||
|
||||
const getTracesStreams = async () => {
|
||||
await getStreams("traces", false)
|
||||
.then((res: any) => {
|
||||
tracesStreams.value = res.list.map((option: any) => option.name);
|
||||
filteredStreamOptions.value = JSON.parse(
|
||||
JSON.stringify(tracesStreams.value),
|
||||
);
|
||||
|
||||
console.log("tracesStreams", tracesStreams.value);
|
||||
|
||||
if (!searchObj.meta.selectedTraceStream.length)
|
||||
searchObj.meta.selectedTraceStream = tracesStreams.value[0];
|
||||
})
|
||||
.catch(() => Promise.reject())
|
||||
.finally(() => {});
|
||||
};
|
||||
|
||||
const filterStreamFn = (val: any = "") => {
|
||||
filteredStreamOptions.value = tracesStreams.value.filter(
|
||||
(stream: any) => {
|
||||
return stream.toLowerCase().indexOf(val.toLowerCase()) > -1;
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const redirectToTraces = () => {
|
||||
emit("view-trace");
|
||||
};
|
||||
|
||||
return {
|
||||
t,
|
||||
copyLogToClipboard,
|
||||
getImageURL,
|
||||
addSearchTerm,
|
||||
addFieldToTable,
|
||||
outlinedAccountTree,
|
||||
store,
|
||||
searchObj,
|
||||
multiStreamFields,
|
||||
redirectToTraces,
|
||||
filteredStreamOptions,
|
||||
filterStreamFn,
|
||||
streamSearchValue,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
@ -197,3 +319,44 @@ export default {
|
|||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.logs-trace-selector {
|
||||
.q-select {
|
||||
.q-field__control {
|
||||
min-height: 30px !important;
|
||||
height: 30px !important;
|
||||
padding: 0px 8px !important;
|
||||
width: 220px !important;
|
||||
|
||||
.q-field,
|
||||
.q-field__native {
|
||||
span {
|
||||
display: inline-block;
|
||||
width: 180px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.q-field__append {
|
||||
height: 27px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.q-btn {
|
||||
height: 30px !important;
|
||||
padding: 2px 8px !important;
|
||||
margin-left: -1px;
|
||||
|
||||
.q-btn__content {
|
||||
span {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -473,6 +473,23 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</q-btn-group>
|
||||
|
||||
<q-btn
|
||||
v-if="
|
||||
config.isEnterprise == 'true' &&
|
||||
!!searchObj.data.searchRequestTraceIds.length &&
|
||||
(searchObj.loading == true ||
|
||||
searchObj.loadingHistogram == true)
|
||||
"
|
||||
data-test="logs-search-bar-refresh-btn"
|
||||
data-cy="search-bar-refresh-button"
|
||||
dense
|
||||
flat
|
||||
:title="t('search.cancel')"
|
||||
class="q-pa-none search-button cancel-search-button"
|
||||
@click="cancelQuery"
|
||||
>{{ t("search.cancel") }}</q-btn
|
||||
>
|
||||
<q-btn
|
||||
v-else
|
||||
data-test="logs-search-bar-refresh-btn"
|
||||
data-cy="search-bar-refresh-button"
|
||||
dense
|
||||
|
|
|
@ -47,7 +47,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
Math.max(
|
||||
1,
|
||||
searchObj.data.queryResults?.partitionDetail?.paginations
|
||||
?.length || 0
|
||||
?.length || 0,
|
||||
)
|
||||
"
|
||||
:input="false"
|
||||
|
@ -232,7 +232,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</tr>
|
||||
<tr
|
||||
v-if="
|
||||
searchObj.loading == false && searchObj.data.missingStreamMessage != ''
|
||||
searchObj.loading == false &&
|
||||
searchObj.data.missingStreamMessage != ''
|
||||
"
|
||||
>
|
||||
<td
|
||||
|
@ -365,7 +366,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
@click.prevent.stop="
|
||||
copyLogToClipboard(
|
||||
column.prop(row, column.name).toString(),
|
||||
false
|
||||
false,
|
||||
)
|
||||
"
|
||||
title="Copy"
|
||||
|
@ -410,6 +411,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
@copy="copyLogToClipboard"
|
||||
@add-field-to-table="addFieldToTable"
|
||||
@add-search-term="addSearchTerm"
|
||||
@view-trace="
|
||||
redirectToTraces(searchObj.data.queryResults.hits[index])
|
||||
"
|
||||
/>
|
||||
</td>
|
||||
</q-tr>
|
||||
|
@ -444,6 +448,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
@remove:searchterm="removeSearchTerm"
|
||||
@search:timeboxed="onTimeBoxed"
|
||||
@add:table="addFieldToTable"
|
||||
@view-trace="
|
||||
redirectToTraces(
|
||||
searchObj.data.queryResults.hits[
|
||||
searchObj.meta.resultGrid.navigation.currentRowIndex
|
||||
]
|
||||
)
|
||||
"
|
||||
/>
|
||||
</q-dialog>
|
||||
</div>
|
||||
|
@ -472,6 +483,7 @@ import NotEqualIcon from "../../components/icons/NotEqualIcon.vue";
|
|||
import useLogs from "../../composables/useLogs";
|
||||
import { convertLogData } from "@/utils/logs/convertLogData";
|
||||
import SanitizedHtmlRenderer from "@/components/SanitizedHtmlRenderer.vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
export default defineComponent({
|
||||
name: "SearchResult",
|
||||
|
@ -482,7 +494,7 @@ export default defineComponent({
|
|||
NotEqualIcon,
|
||||
JsonPreview: defineAsyncComponent(() => import("./JsonPreview.vue")),
|
||||
ChartRenderer: defineAsyncComponent(
|
||||
() => import("@/components/dashboards/panels/ChartRenderer.vue")
|
||||
() => import("@/components/dashboards/panels/ChartRenderer.vue"),
|
||||
),
|
||||
SanitizedHtmlRenderer,
|
||||
},
|
||||
|
@ -515,7 +527,7 @@ export default defineComponent({
|
|||
this.searchObj.data.resultGrid.currentPage <=
|
||||
Math.round(
|
||||
this.searchObj.data.queryResults.total /
|
||||
this.searchObj.meta.resultGrid.rowsPerPage
|
||||
this.searchObj.meta.resultGrid.rowsPerPage,
|
||||
)
|
||||
) {
|
||||
this.searchObj.data.resultGrid.currentPage =
|
||||
|
@ -534,7 +546,7 @@ export default defineComponent({
|
|||
if (
|
||||
this.pageNumberInput >
|
||||
Math.ceil(
|
||||
this.searchObj.data.queryResults.partitionDetail.paginations.length
|
||||
this.searchObj.data.queryResults.partitionDetail.paginations.length,
|
||||
)
|
||||
) {
|
||||
this.$q.notify({
|
||||
|
@ -557,7 +569,7 @@ export default defineComponent({
|
|||
this.searchObj.data.resultGrid.columns.splice(RGIndex, 1);
|
||||
|
||||
const SFIndex = this.searchObj.data.stream.selectedFields.indexOf(
|
||||
col.name
|
||||
col.name,
|
||||
);
|
||||
|
||||
this.searchObj.data.stream.selectedFields.splice(SFIndex, 1);
|
||||
|
@ -590,6 +602,7 @@ export default defineComponent({
|
|||
const scrollPosition = ref(0);
|
||||
const rowsPerPageOptions = [10, 25, 50, 100, 250, 500];
|
||||
const disableMoreErrorDetails = ref(false);
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
searchObj,
|
||||
|
@ -626,7 +639,7 @@ export default defineComponent({
|
|||
plotChart.value = convertLogData(
|
||||
searchObj.data.histogram.xData,
|
||||
searchObj.data.histogram.yData,
|
||||
searchObj.data.histogram.chartParams
|
||||
searchObj.data.histogram.chartParams,
|
||||
);
|
||||
// plotChart.value.forceReLayout();
|
||||
}
|
||||
|
@ -653,7 +666,7 @@ export default defineComponent({
|
|||
const newIndex = getRowIndex(
|
||||
isNext,
|
||||
isPrev,
|
||||
Number(searchObj.meta.resultGrid.navigation.currentRowIndex)
|
||||
Number(searchObj.meta.resultGrid.navigation.currentRowIndex),
|
||||
);
|
||||
searchObj.meta.resultGrid.navigation.currentRowIndex = newIndex;
|
||||
};
|
||||
|
@ -680,7 +693,7 @@ export default defineComponent({
|
|||
if (searchObj.data.stream.selectedFields.includes(fieldName)) {
|
||||
searchObj.data.stream.selectedFields =
|
||||
searchObj.data.stream.selectedFields.filter(
|
||||
(v: any) => v !== fieldName
|
||||
(v: any) => v !== fieldName,
|
||||
);
|
||||
} else {
|
||||
searchObj.data.stream.selectedFields.push(fieldName);
|
||||
|
@ -699,10 +712,41 @@ export default defineComponent({
|
|||
type: "positive",
|
||||
message: "Content Copied Successfully!",
|
||||
timeout: 1000,
|
||||
})
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const redirectToTraces = (log: any) => {
|
||||
// 15 mins +- from the log timestamp
|
||||
const from = log[store.state.zoConfig.timestamp_column] - 900000000;
|
||||
const to = log[store.state.zoConfig.timestamp_column] + 900000000;
|
||||
const refresh = 0;
|
||||
|
||||
const query: any = {
|
||||
name: "traceDetails",
|
||||
query: {
|
||||
stream: searchObj.meta.selectedTraceStream,
|
||||
from,
|
||||
to,
|
||||
refresh,
|
||||
org_identifier: store.state.selectedOrganization.identifier,
|
||||
trace_id:
|
||||
log[
|
||||
store.state.organizationData.organizationSettings
|
||||
.trace_id_field_name
|
||||
],
|
||||
reload: "true",
|
||||
},
|
||||
};
|
||||
|
||||
query["span_id"] =
|
||||
log[
|
||||
store.state.organizationData.organizationSettings.span_id_field_name
|
||||
];
|
||||
|
||||
router.push(query);
|
||||
};
|
||||
|
||||
return {
|
||||
t,
|
||||
store,
|
||||
|
@ -734,6 +778,7 @@ export default defineComponent({
|
|||
pageNumberInput,
|
||||
refreshPartitionPagination,
|
||||
disableMoreErrorDetails,
|
||||
redirectToTraces,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -998,11 +1043,17 @@ export default defineComponent({
|
|||
<style lang="scss">
|
||||
.search-list {
|
||||
.copy-log-btn {
|
||||
.q-btn .q-icon {
|
||||
.q-icon {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.view-trace-btn {
|
||||
.q-icon {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.q-pagination__content input {
|
||||
border: 1px solid lightgrey;
|
||||
top: 7px;
|
||||
|
|
|
@ -173,7 +173,6 @@ import { useRouter } from "vue-router";
|
|||
|
||||
import useTraces from "@/composables/useTraces";
|
||||
|
||||
import streamService from "@/services/stream";
|
||||
import searchService from "@/services/search";
|
||||
import TransformService from "@/services/jstransform";
|
||||
import {
|
||||
|
@ -260,7 +259,8 @@ export default defineComponent({
|
|||
const router = useRouter();
|
||||
const $q = useQuasar();
|
||||
const { t } = useI18n();
|
||||
const { searchObj, resetSearchObj } = useTraces();
|
||||
const { searchObj, resetSearchObj, getUrlQueryParams, copyTracesUrl } =
|
||||
useTraces();
|
||||
let refreshIntervalID = 0;
|
||||
const searchResultRef = ref(null);
|
||||
const searchBarRef = ref(null);
|
||||
|
@ -578,11 +578,15 @@ export default defineComponent({
|
|||
req.query.sql = b64EncodeUnicode(req.query.sql);
|
||||
|
||||
const queryParams = getUrlQueryParams();
|
||||
|
||||
router.push({ query: queryParams });
|
||||
return req;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
searchObj.loading = false;
|
||||
showErrorNotification("Invalid SQL Syntax");
|
||||
showErrorNotification(
|
||||
"An error occurred while constructing the search query."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -765,15 +769,17 @@ export default defineComponent({
|
|||
|
||||
let filter = searchObj.data.editorValue.trim();
|
||||
|
||||
let duration = "";
|
||||
if (searchObj.meta.filterType === "basic" && durationFilter.max) {
|
||||
duration += ` duration >= ${
|
||||
durationFilter.min * 1000
|
||||
} AND duration <= ${durationFilter.max * 1000}`;
|
||||
if (
|
||||
searchObj.meta.filterType === "basic" &&
|
||||
durationFilter.max !== undefined &&
|
||||
durationFilter.min !== undefined
|
||||
) {
|
||||
const minDuration = durationFilter.min * 1000;
|
||||
const maxDuration = durationFilter.max * 1000;
|
||||
|
||||
filter = filter
|
||||
? searchObj.data.editorValue + " AND" + duration
|
||||
: duration;
|
||||
const duration = `duration >= ${minDuration} AND duration <= ${maxDuration}`;
|
||||
|
||||
filter = filter ? `${filter} AND ${duration}` : duration;
|
||||
}
|
||||
|
||||
searchService
|
||||
|
@ -806,8 +812,6 @@ export default defineComponent({
|
|||
//update grid columns
|
||||
updateGridColumns();
|
||||
|
||||
if (router.currentRoute.value.query.trace_id) openTraceDetails();
|
||||
|
||||
// dismiss();
|
||||
})
|
||||
.catch((err) => {
|
||||
|
@ -1145,6 +1149,11 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
onActivated(() => {
|
||||
restoreUrlQueryParams();
|
||||
const params = router.currentRoute.value.query;
|
||||
if (params.reload === "true") {
|
||||
loadPageData();
|
||||
}
|
||||
if (
|
||||
searchObj.organizationIdetifier !=
|
||||
store.state.selectedOrganization.identifier
|
||||
|
@ -1204,71 +1213,6 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
const copyTracesUrl = (customTimeRange = null) => {
|
||||
const queryParams = getUrlQueryParams(true);
|
||||
|
||||
if (customTimeRange) {
|
||||
queryParams.from = customTimeRange.from;
|
||||
queryParams.to = customTimeRange.to;
|
||||
}
|
||||
|
||||
const queryString = Object.entries(queryParams)
|
||||
.map(
|
||||
([key, value]) =>
|
||||
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`
|
||||
)
|
||||
.join("&");
|
||||
|
||||
let shareURL = window.location.origin + window.location.pathname;
|
||||
|
||||
if (queryString != "") {
|
||||
shareURL += "?" + queryString;
|
||||
}
|
||||
|
||||
copyToClipboard(shareURL)
|
||||
.then(() => {
|
||||
$q.notify({
|
||||
type: "positive",
|
||||
message: "Link Copied Successfully!",
|
||||
timeout: 5000,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
$q.notify({
|
||||
type: "negative",
|
||||
message: "Error while copy link.",
|
||||
timeout: 5000,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function getUrlQueryParams(getShareLink: false) {
|
||||
const date = searchObj.data.datetime;
|
||||
const query = {};
|
||||
|
||||
query["stream"] = selectedStreamName.value;
|
||||
|
||||
if (date.type == "relative" && !getShareLink) {
|
||||
query["period"] = date.relativeTimePeriod;
|
||||
} else {
|
||||
query["from"] = date.startTime;
|
||||
query["to"] = date.endTime;
|
||||
}
|
||||
|
||||
query["query"] = b64EncodeUnicode(searchObj.data.editorValue);
|
||||
|
||||
query["filter_type"] = searchObj.meta.filterType;
|
||||
|
||||
query["org_identifier"] = store.state.selectedOrganization.identifier;
|
||||
|
||||
query["trace_id"] = router.currentRoute.value.query.trace_id;
|
||||
|
||||
if (router.currentRoute.value.query.span_id)
|
||||
query["span_id"] = router.currentRoute.value.query.span_id;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
const onSplitterUpdate = () => {
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
};
|
||||
|
@ -1327,7 +1271,6 @@ export default defineComponent({
|
|||
updateGridColumns,
|
||||
getConsumableDateTime,
|
||||
runQueryFn,
|
||||
getTraceDetails,
|
||||
verifyOrganizationStatus,
|
||||
fieldValues,
|
||||
onSplitterUpdate,
|
||||
|
|
|
@ -164,6 +164,7 @@ import {
|
|||
nextTick,
|
||||
defineAsyncComponent,
|
||||
onBeforeUnmount,
|
||||
onActivated,
|
||||
} from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
|
@ -247,6 +248,22 @@ export default defineComponent({
|
|||
await importSqlParser();
|
||||
});
|
||||
|
||||
onActivated(async () => {
|
||||
await nextTick();
|
||||
if (searchObj.data.datetime.type === "relative") {
|
||||
dateTimeRef.value.setRelativeTime(
|
||||
searchObj.data.datetime.relativeTimePeriod
|
||||
);
|
||||
|
||||
dateTimeRef.value.refresh();
|
||||
} else {
|
||||
dateTimeRef.value.setAbsoluteTime(
|
||||
searchObj.data.datetime.startTime,
|
||||
searchObj.data.datetime.endTime
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const refreshTimeChange = (item) => {
|
||||
searchObj.meta.refreshInterval = item.value;
|
||||
searchObj.meta.refreshIntervalLabel = item.label;
|
||||
|
@ -315,6 +332,8 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
const updateDateTime = async (value: object) => {
|
||||
if (router.currentRoute.value.name !== "traces") return;
|
||||
|
||||
searchObj.data.datetime = {
|
||||
startTime: value.startTime,
|
||||
endTime: value.endTime,
|
||||
|
|
|
@ -54,16 +54,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
/>
|
||||
</q-item>
|
||||
</q-virtual-scroll>
|
||||
<q-dialog
|
||||
v-model="searchObj.meta.showTraceDetails"
|
||||
position="right"
|
||||
full-height
|
||||
full-width
|
||||
maximized
|
||||
@hide="closeTraceDetails"
|
||||
>
|
||||
<trace-details @shareLink="shareLink" />
|
||||
</q-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -77,16 +67,13 @@ import { useI18n } from "vue-i18n";
|
|||
import { byString } from "../../utils/json";
|
||||
import useTraces from "../../composables/useTraces";
|
||||
import { getImageURL } from "../../utils/zincutils";
|
||||
import TraceDetails from "./TraceDetails.vue";
|
||||
import { convertTraceData } from "@/utils/traces/convertTraceData";
|
||||
import TraceBlock from "./TraceBlock.vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
|
||||
export default defineComponent({
|
||||
name: "SearchResult",
|
||||
components: {
|
||||
TraceDetails,
|
||||
ChartRenderer: defineAsyncComponent(
|
||||
() => import("@/components/dashboards/panels/ChartRenderer.vue")
|
||||
),
|
||||
|
@ -98,7 +85,6 @@ export default defineComponent({
|
|||
"remove:searchTerm",
|
||||
"search:timeboxed",
|
||||
"get:traceDetails",
|
||||
"shareLink",
|
||||
],
|
||||
methods: {
|
||||
closeColumn(col: any) {
|
||||
|
@ -151,7 +137,6 @@ export default defineComponent({
|
|||
const $q = useQuasar();
|
||||
const router = useRouter();
|
||||
|
||||
const showTraceDetails = ref(false);
|
||||
const { searchObj, updatedLocalLogFilterField } = useTraces();
|
||||
const totalHeight = ref(0);
|
||||
|
||||
|
@ -180,17 +165,16 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
const expandRowDetail = (props: any) => {
|
||||
searchObj.data.traceDetails.selectedTrace = props;
|
||||
router.push({
|
||||
name: "traces",
|
||||
name: "traceDetails",
|
||||
query: {
|
||||
...router.currentRoute.value.query,
|
||||
stream: router.currentRoute.value.query.stream,
|
||||
trace_id: props.trace_id,
|
||||
from: props.trace_start_time - 10000000,
|
||||
to: props.trace_end_time + 10000000,
|
||||
org_identifier: store.state.selectedOrganization.identifier,
|
||||
},
|
||||
});
|
||||
setTimeout(() => {
|
||||
searchObj.meta.showTraceDetails = true;
|
||||
}, 100);
|
||||
|
||||
emit("get:traceDetails", props);
|
||||
};
|
||||
|
@ -222,7 +206,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
const closeTraceDetails = () => {
|
||||
const query = cloneDeep(router.currentRoute.value.query);
|
||||
const query = JSON.parse(JSON.stringify(router.currentRoute.value.query));
|
||||
delete query.trace_id;
|
||||
if (query.span_id) delete query.span_id;
|
||||
|
||||
|
@ -242,15 +226,6 @@ export default defineComponent({
|
|||
expandRowDetail(searchObj.data.queryResults.hits[data.dataIndex]);
|
||||
};
|
||||
|
||||
const shareLink = () => {
|
||||
if (!searchObj.data.traceDetails.selectedTrace) return;
|
||||
const trace = searchObj.data.traceDetails.selectedTrace as any;
|
||||
emit("shareLink", {
|
||||
from: trace.trace_start_time - 60000000,
|
||||
to: trace.trace_end_time + 60000000,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
t,
|
||||
store,
|
||||
|
@ -267,10 +242,7 @@ export default defineComponent({
|
|||
totalHeight,
|
||||
reDrawChart,
|
||||
getImageURL,
|
||||
showTraceDetails,
|
||||
closeTraceDetails,
|
||||
onChartClick,
|
||||
shareLink,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -36,7 +36,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
paddingBottom: '6px',
|
||||
}"
|
||||
ref="spanBlock"
|
||||
@click="selectSpan"
|
||||
@click="selectSpan(span.spanId)"
|
||||
@mouseover="onSpanHover"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
|
@ -45,13 +46,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
}"
|
||||
class="cursor-pointer flex items-center no-wrap position-relative"
|
||||
:class="defocusSpan ? 'defocus' : ''"
|
||||
@click="selectSpan"
|
||||
@click="selectSpan(span.spanId)"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
height: spanDimensions.barHeight + 'px',
|
||||
width: getWidth + '%',
|
||||
left: getLeftPosition + '%',
|
||||
width: spanWidth + '%',
|
||||
left: leftPosition + '%',
|
||||
position: 'relative',
|
||||
}"
|
||||
class="flex justify-start items-center no-wrap"
|
||||
|
@ -81,13 +82,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<div
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
...getDurationStyle,
|
||||
...durationStyle,
|
||||
transition: 'all 0.5s ease',
|
||||
zIndex: 1,
|
||||
}"
|
||||
class="text-caption"
|
||||
class="text-caption flex items-center"
|
||||
>
|
||||
{{ formatTimeWithSuffix(span.durationUs) }}
|
||||
<div>
|
||||
{{ formatTimeWithSuffix(span.durationUs) }}
|
||||
</div>
|
||||
</div>
|
||||
<q-resize-observer debounce="300" @resize="onResize" />
|
||||
</div>
|
||||
|
@ -100,17 +103,30 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
:span="span"
|
||||
:spanData="spanData"
|
||||
:baseTracePosition="baseTracePosition"
|
||||
@view-logs="viewSpanLogs"
|
||||
@select-span="selectSpan"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref } from "vue";
|
||||
import {
|
||||
defineComponent,
|
||||
computed,
|
||||
ref,
|
||||
onMounted,
|
||||
nextTick,
|
||||
watch,
|
||||
onActivated,
|
||||
} from "vue";
|
||||
import useTraces from "@/composables/useTraces";
|
||||
import { getImageURL, formatTimeWithSuffix } from "@/utils/zincutils";
|
||||
import SpanDetails from "./SpanDetails.vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { b64EncodeStandard } from "@/utils/zincutils";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
export default defineComponent({
|
||||
name: "SpanBlock",
|
||||
|
@ -148,7 +164,7 @@ export default defineComponent({
|
|||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
emits: ["toggleCollapse", "selectSpan"],
|
||||
emits: ["toggleCollapse", "selectSpan", "hover", "view-logs"],
|
||||
components: { SpanDetails },
|
||||
setup(props, { emit }) {
|
||||
const store = useStore();
|
||||
|
@ -160,8 +176,16 @@ export default defineComponent({
|
|||
if (!searchObj.data.traceDetails.selectedSpanId) return false;
|
||||
return searchObj.data.traceDetails.selectedSpanId !== props.span.spanId;
|
||||
});
|
||||
const selectSpan = () => {
|
||||
emit("selectSpan", props.span.spanId);
|
||||
const durationStyle = ref({});
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
const leftPosition = ref(0);
|
||||
|
||||
const spanWidth = ref(0);
|
||||
|
||||
const selectSpan = (spanId: string) => {
|
||||
emit("selectSpan", spanId);
|
||||
};
|
||||
const toggleSpanCollapse = () => {
|
||||
emit("toggleCollapse", props.span.spanId);
|
||||
|
@ -175,59 +199,75 @@ export default defineComponent({
|
|||
|
||||
const spanMarkerRef = ref(null);
|
||||
|
||||
const getLeftPosition = computed(() => {
|
||||
const getLeftPosition = () => {
|
||||
const left =
|
||||
props.span.startTimeMs - props.baseTracePosition["startTimeMs"];
|
||||
|
||||
// if (props.span.startTimeMs < props.baseTracePosition["startTimeMs"]) {
|
||||
// const left =
|
||||
// props.baseTracePosition["startTimeMs"] - props.span.startTimeMs;
|
||||
// // props.baseTracePosition + props.baseTracePosition["durationMs"];
|
||||
// return -(left / props.baseTracePosition?.durationMs) * 100;
|
||||
// }
|
||||
// // console.log(
|
||||
// // props.span.startTimeMs,
|
||||
// // props.baseTracePosition["startTimeMs"],
|
||||
// // left,
|
||||
// // props.baseTracePosition?.durationMs
|
||||
// // );
|
||||
return (left / props.baseTracePosition?.durationMs) * 100;
|
||||
});
|
||||
const getWidth = computed(() => {
|
||||
};
|
||||
|
||||
const getSpanWidth = () => {
|
||||
return Number(
|
||||
(
|
||||
(props.span?.durationMs / props.baseTracePosition?.durationMs) *
|
||||
100
|
||||
).toFixed(2)
|
||||
);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
durationStyle.value = getDurationStyle();
|
||||
});
|
||||
const getDurationStyle = computed(() => {
|
||||
|
||||
watch(
|
||||
() => props.span.startTimeMs + props.baseTracePosition["startTimeMs"],
|
||||
() => {
|
||||
leftPosition.value = getLeftPosition();
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.span?.durationMs + props.baseTracePosition?.durationMs,
|
||||
() => {
|
||||
spanWidth.value = getSpanWidth();
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => spanBlockWidth.value + leftPosition.value + spanWidth.value,
|
||||
(val) => {
|
||||
durationStyle.value = getDurationStyle();
|
||||
}
|
||||
);
|
||||
|
||||
const getDurationStyle = () => {
|
||||
const style: any = {
|
||||
top: "10px",
|
||||
};
|
||||
|
||||
const onePercent = Number((spanBlockWidth.value / 100).toFixed(2));
|
||||
const labelWidth = 60;
|
||||
if (
|
||||
(getLeftPosition.value + getWidth.value) * onePercent + labelWidth >
|
||||
(leftPosition.value + spanWidth.value) * onePercent + labelWidth >
|
||||
spanBlockWidth.value
|
||||
) {
|
||||
style.right = 0;
|
||||
style.top = "0";
|
||||
} else if (getLeftPosition.value > 50) {
|
||||
style.left =
|
||||
getLeftPosition.value * onePercent - labelWidth + 10 + "px";
|
||||
style.top = "-5px";
|
||||
} else if (leftPosition.value > 50) {
|
||||
style.left = leftPosition.value * onePercent - labelWidth + 10 + "px";
|
||||
} else {
|
||||
const left =
|
||||
getLeftPosition.value +
|
||||
(Math.floor(getWidth.value) ? getWidth.value : 1);
|
||||
leftPosition.value +
|
||||
(Math.floor(spanWidth.value) ? spanWidth.value : 1);
|
||||
|
||||
style.left =
|
||||
(left * onePercent - getLeftPosition.value * onePercent < 19
|
||||
? getLeftPosition.value * onePercent + 19
|
||||
(left * onePercent - leftPosition.value * onePercent < 19
|
||||
? leftPosition.value * onePercent + 19
|
||||
: left * onePercent) + "px";
|
||||
}
|
||||
|
||||
return style;
|
||||
});
|
||||
};
|
||||
|
||||
const getSpanStartTime = computed(() => {
|
||||
return props.span.startTimeMs - props.baseTracePosition["startTimeMs"];
|
||||
|
@ -248,13 +288,22 @@ export default defineComponent({
|
|||
}
|
||||
};
|
||||
|
||||
const viewSpanLogs = () => {
|
||||
emit("view-logs");
|
||||
};
|
||||
|
||||
const onSpanHover = () => {
|
||||
emit("hover");
|
||||
};
|
||||
|
||||
return {
|
||||
t,
|
||||
formatTimeWithSuffix,
|
||||
selectSpan,
|
||||
toggleSpanCollapse,
|
||||
getImageURL,
|
||||
getLeftPosition,
|
||||
getWidth,
|
||||
leftPosition,
|
||||
spanWidth,
|
||||
getDurationStyle,
|
||||
spanBlock,
|
||||
onResize,
|
||||
|
@ -265,6 +314,9 @@ export default defineComponent({
|
|||
defocusSpan,
|
||||
isSpanSelected,
|
||||
store,
|
||||
viewSpanLogs,
|
||||
onSpanHover,
|
||||
durationStyle,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -286,4 +338,16 @@ export default defineComponent({
|
|||
.light-grey {
|
||||
background-color: #ececec;
|
||||
}
|
||||
|
||||
.view-span-logs {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.span-block-overlay {
|
||||
&:hover {
|
||||
.view-span-logs {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<div class="full-width q-mb-md q-px-md span-details-container">
|
||||
<div
|
||||
:class="store.state.theme === 'dark' ? 'dark-theme' : 'light-theme'"
|
||||
class="full-width q-mb-md q-px-md span-details-container"
|
||||
>
|
||||
<div
|
||||
class="flex justify-between items-center full-width"
|
||||
style="border-bottom: 1px solid #e9e9e9"
|
||||
|
@ -8,6 +11,20 @@
|
|||
{{ span.operationName }}
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div style="border-right: 1px solid #cccccc; font-size: 14px">
|
||||
<q-btn
|
||||
class="q-mx-sm view-span-logs-btn"
|
||||
size="10px"
|
||||
icon="search"
|
||||
dense
|
||||
padding="xs sm"
|
||||
no-caps
|
||||
:title="t('traces.viewLogs')"
|
||||
@click.stop="viewSpanLogs"
|
||||
>
|
||||
View Logs</q-btn
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="q-px-sm"
|
||||
style="border-right: 1px solid #cccccc; font-size: 14px"
|
||||
|
@ -357,6 +374,72 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="span.links.length">
|
||||
<div
|
||||
class="flex items-center no-wrap cursor-pointer"
|
||||
@click="toggleLinks"
|
||||
>
|
||||
<q-icon
|
||||
name="expand_more"
|
||||
:class="!isLinksExpanded ? 'rotate-270' : ''"
|
||||
size="14px"
|
||||
class="cursor-pointer text-grey-7"
|
||||
/>
|
||||
<div class="cursor-pointer text-bold">References</div>
|
||||
<div
|
||||
class="q-ml-sm text-grey-9"
|
||||
style="
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 12px;
|
||||
"
|
||||
>
|
||||
{{ span.links.length }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="isLinksExpanded" class="q-px-md q-my-sm">
|
||||
<q-separator />
|
||||
<template v-for="link in span.links" :key="link.context.spanId">
|
||||
<div
|
||||
class="flex row justify-between items-center q-pa-xs links-container"
|
||||
>
|
||||
<div
|
||||
class="ref-span-link cursor-pointer"
|
||||
@click="openReferenceTrace('span', link)"
|
||||
>
|
||||
Span in another trace
|
||||
</div>
|
||||
<div class="flex items-center link-id-container">
|
||||
<div class="q-mr-sm link-span-id ellipsis">
|
||||
<span class="text-grey-7">Span ID: </span>
|
||||
<span
|
||||
class="id-link cursor-pointer"
|
||||
@click="openReferenceTrace('span', link)"
|
||||
>{{ link.context.spanId }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="link-trace-id ellipsis">
|
||||
<span class="text-grey-7">Trace ID: </span>
|
||||
<span
|
||||
class="id-link cursor-pointer"
|
||||
@click="openReferenceTrace('trace', link)"
|
||||
>
|
||||
{{ link.context.traceId }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<q-separator />
|
||||
</template>
|
||||
<div
|
||||
class="full-width text-center q-pt-lg text-bold"
|
||||
v-if="!span.links.length"
|
||||
>
|
||||
No events present for this span
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right flex items-center justify-end">
|
||||
<span class="text-grey-7 q-mr-xs">Span Id: </span
|
||||
><span class="">{{ span.spanId }}</span>
|
||||
|
@ -381,6 +464,8 @@ import { useStore } from "vuex";
|
|||
import { formatTimeWithSuffix } from "@/utils/zincutils";
|
||||
import { date, useQuasar } from "quasar";
|
||||
import { copyToClipboard } from "quasar";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const props = defineProps({
|
||||
span: {
|
||||
|
@ -397,8 +482,35 @@ const props = defineProps({
|
|||
},
|
||||
});
|
||||
|
||||
const links = [
|
||||
{
|
||||
context: {
|
||||
traceId: "f6e08ab2a928aa393375f0d9b05a9054",
|
||||
spanId: "ecc59cb843104cf8",
|
||||
traceFlags: 1,
|
||||
traceState: undefined,
|
||||
},
|
||||
attributes: {},
|
||||
},
|
||||
{
|
||||
context: {
|
||||
traceId: "6d88ba59ea87ffffdbad56b9e8acc1b3",
|
||||
spanId: "39d6bc6878b73c60",
|
||||
traceFlags: 1,
|
||||
traceState: undefined,
|
||||
},
|
||||
attributes: {},
|
||||
},
|
||||
];
|
||||
|
||||
const emit = defineEmits(["view-logs", "select-span"]);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const getDuration = computed(() => formatTimeWithSuffix(props.span.durationUs));
|
||||
|
||||
const getStartTime = computed(() => {
|
||||
|
@ -522,6 +634,12 @@ const areEventsExpananded = ref(false);
|
|||
|
||||
const isExceptionExpanded = ref(false);
|
||||
|
||||
const isLinksExpanded = ref(false);
|
||||
|
||||
const toggleLinks = () => {
|
||||
isLinksExpanded.value = !isLinksExpanded.value;
|
||||
};
|
||||
|
||||
const toggleProcess = () => {
|
||||
areProcessExpananded.value = !areProcessExpananded.value;
|
||||
};
|
||||
|
@ -544,6 +662,10 @@ const expandEvent = (index: number) => {
|
|||
else expandedEvents.value[index.toString()] = true;
|
||||
};
|
||||
|
||||
const viewSpanLogs = () => {
|
||||
emit("view-logs", props.span.spanId);
|
||||
};
|
||||
|
||||
const copySpanId = () => {
|
||||
$q.notify({
|
||||
type: "positive",
|
||||
|
@ -552,6 +674,31 @@ const copySpanId = () => {
|
|||
});
|
||||
copyToClipboard(props.span.spanId);
|
||||
};
|
||||
|
||||
const openReferenceTrace = (type: string, link: any) => {
|
||||
const query = {
|
||||
stream: router.currentRoute.value.query.stream,
|
||||
trace_id: link.context.traceId,
|
||||
span_id: link.context.spanId,
|
||||
from: props.span.startTimeMs * 1000 - 3600000000,
|
||||
to: props.span.startTimeMs * 1000 + 3600000000,
|
||||
org_identifier: store.state.selectedOrganization.identifier,
|
||||
};
|
||||
|
||||
if (type !== "span") {
|
||||
delete query.span_id;
|
||||
}
|
||||
|
||||
if (query.trace_id === props.spanData.trace_id) {
|
||||
emit("select-span", link.context.spanId);
|
||||
return;
|
||||
}
|
||||
|
||||
router.push({
|
||||
name: "traceDetails",
|
||||
query,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
@ -727,7 +874,7 @@ const copySpanId = () => {
|
|||
}
|
||||
}
|
||||
.span_details_tab-panels {
|
||||
height: calc(100% - 102px);
|
||||
height: calc(100% - 104px);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
@ -736,6 +883,38 @@ const copySpanId = () => {
|
|||
border-top: 1px solid $border-color;
|
||||
background-color: color-mix(in srgb, currentColor 5%, transparent);
|
||||
}
|
||||
|
||||
.link-id-container {
|
||||
.link-trace-id {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.link-span-id {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.ref-span-link,
|
||||
.id-link {
|
||||
&:hover {
|
||||
opacity: 0.6;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.dark-theme {
|
||||
.links-container {
|
||||
border-left: 1px solid #ffffff47;
|
||||
border-right: 1px solid #ffffff47;
|
||||
}
|
||||
}
|
||||
|
||||
.light-theme {
|
||||
.links-container {
|
||||
border-left: 1px solid #0000001f;
|
||||
border-right: 1px solid #0000001f;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.tags-expander {
|
||||
|
|
|
@ -15,28 +15,45 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="trace-details"
|
||||
:style="{
|
||||
width: '97vw !important',
|
||||
background: store.state.theme === 'dark' ? '#181a1b' : '#ffffff',
|
||||
}"
|
||||
>
|
||||
<div class="trace-details full-width" :style="backgroundStyle">
|
||||
<div
|
||||
class="row q-px-sm"
|
||||
v-if="traceTree.length && !searchObj.data.traceDetails.loading"
|
||||
>
|
||||
<div
|
||||
class="q-py-sm q-px-sm flex items-end justify-between col-12 toolbar"
|
||||
>
|
||||
<div class="flex items-end justify-start">
|
||||
<div class="text-h6 q-mr-lg">
|
||||
<div class="full-width flex items-center toolbar flex justify-between">
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
data-test="add-alert-back-btn"
|
||||
class="flex justify-center items-center q-mr-sm cursor-pointer"
|
||||
style="
|
||||
border: 1.5px solid;
|
||||
border-radius: 50%;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
"
|
||||
title="Traces List"
|
||||
@click="routeToTracesList"
|
||||
>
|
||||
<q-icon name="arrow_back_ios_new" size="14px" />
|
||||
</div>
|
||||
<div
|
||||
class="text-subtitle1 q-mr-lg ellipsis toolbar-operation-name"
|
||||
:title="traceTree[0]['operationName']"
|
||||
>
|
||||
{{ traceTree[0]["operationName"] }}
|
||||
</div>
|
||||
<div class="q-pb-xs q-mr-lg flex items-center">
|
||||
<div>Trace ID: {{ spanList[0]["trace_id"] }}</div>
|
||||
<div class="q-mr-lg flex items-center text-body2">
|
||||
<div class="flex items-center">
|
||||
Trace ID:
|
||||
<div
|
||||
class="toolbar-trace-id ellipsis q-pl-xs"
|
||||
:title="spanList[0]['trace_id']"
|
||||
>
|
||||
{{ spanList[0]["trace_id"] }}
|
||||
</div>
|
||||
</div>
|
||||
<q-icon
|
||||
class="q-ml-xs text-grey-8 cursor-pointer trace-copy-icon"
|
||||
class="q-ml-xs cursor-pointer trace-copy-icon"
|
||||
size="12px"
|
||||
name="content_copy"
|
||||
title="Copy"
|
||||
|
@ -44,12 +61,97 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div class="q-pb-xs">Spans: {{ spanList.length }}</div>
|
||||
<div class="q-pb-xs q-mr-lg">Spans: {{ spanList.length }}</div>
|
||||
|
||||
<!-- TODO OK: Create component for this usecase multi select with button -->
|
||||
<div class="o2-input flex items-center trace-logs-selector">
|
||||
<q-select
|
||||
data-test="log-search-index-list-select-stream"
|
||||
v-model="searchObj.data.traceDetails.selectedLogStreams"
|
||||
:label="
|
||||
searchObj.data.traceDetails.selectedLogStreams.length
|
||||
? ''
|
||||
: t('search.selectIndex')
|
||||
"
|
||||
:options="filteredStreamOptions"
|
||||
data-cy="stream-selection"
|
||||
input-debounce="0"
|
||||
behavior="menu"
|
||||
filled
|
||||
multiple
|
||||
borderless
|
||||
dense
|
||||
fill-input
|
||||
:title="selectedStreamsString"
|
||||
>
|
||||
<template #no-option>
|
||||
<div class="o2-input log-stream-search-input">
|
||||
<q-input
|
||||
data-test="alert-list-search-input"
|
||||
v-model="streamSearchValue"
|
||||
borderless
|
||||
filled
|
||||
debounce="500"
|
||||
autofocus
|
||||
dense
|
||||
size="xs"
|
||||
@update:model-value="filterStreamFn"
|
||||
class="q-ml-auto q-mb-xs no-border q-pa-xs"
|
||||
:placeholder="t('search.searchStream')"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="search" class="cursor-pointer" />
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
<q-item>
|
||||
<q-item-section> {{ t("search.noResult") }}</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
<template #before-options>
|
||||
<div class="o2-input log-stream-search-input">
|
||||
<q-input
|
||||
data-test="alert-list-search-input"
|
||||
v-model="streamSearchValue"
|
||||
borderless
|
||||
debounce="500"
|
||||
filled
|
||||
dense
|
||||
autofocus
|
||||
@update:model-value="filterStreamFn"
|
||||
class="q-ml-auto q-mb-xs no-border q-pa-xs"
|
||||
:placeholder="t('search.searchStream')"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="search" class="cursor-pointer" />
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</template>
|
||||
</q-select>
|
||||
<q-btn
|
||||
data-test="trace-view-logs-btn"
|
||||
v-close-popup="true"
|
||||
class="text-bold traces-view-logs-btn"
|
||||
:label="
|
||||
searchObj.meta.redirectedFromLogs
|
||||
? t('traces.backToLogs')
|
||||
: t('traces.viewLogs')
|
||||
"
|
||||
text-color="light-text"
|
||||
padding="sm sm"
|
||||
size="sm"
|
||||
no-caps
|
||||
dense
|
||||
icon="search"
|
||||
@click="redirectToLogs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<q-btn
|
||||
data-test="logs-search-bar-share-link-btn"
|
||||
class="q-mr-sm download-logs-btn q-px-sm"
|
||||
class="q-mr-sm download-logs-btn"
|
||||
size="sm"
|
||||
icon="share"
|
||||
round
|
||||
|
@ -58,9 +160,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
:title="t('search.shareLink')"
|
||||
@click="shareLink"
|
||||
/>
|
||||
<q-btn v-close-popup="true" round flat icon="cancel" size="md" />
|
||||
<q-btn round flat icon="cancel" size="md" @click="router.back()" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-separator style="width: 100%" />
|
||||
<div class="col-12 flex justify-between items-end q-pr-sm q-pt-sm">
|
||||
<div
|
||||
|
@ -157,8 +260,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
:spanDimensions="spanDimensions"
|
||||
:spanMap="spanMap"
|
||||
:leftWidth="leftWidth"
|
||||
class="trace-tree"
|
||||
@toggle-collapse="toggleSpanCollapse"
|
||||
@select-span="updateSelectedSpan"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -166,12 +269,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</div>
|
||||
<q-separator vertical />
|
||||
<div
|
||||
v-if="isSidebarOpen && selectedSpanId"
|
||||
v-if="isSidebarOpen && (selectedSpanId || showTraceDetails)"
|
||||
class="histogram-sidebar"
|
||||
:class="isTimelineExpanded ? '' : 'full'"
|
||||
>
|
||||
<trace-details-sidebar
|
||||
:span="spanMap[selectedSpanId as string]"
|
||||
@view-logs="redirectToLogs"
|
||||
@close="closeSidebar"
|
||||
/>
|
||||
</div>
|
||||
|
@ -195,6 +299,9 @@ import {
|
|||
onMounted,
|
||||
watch,
|
||||
defineAsyncComponent,
|
||||
onBeforeMount,
|
||||
onActivated,
|
||||
onDeactivated,
|
||||
} from "vue";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import SpanRenderer from "./SpanRenderer.vue";
|
||||
|
@ -214,6 +321,12 @@ import {
|
|||
import { throttle } from "lodash-es";
|
||||
import { copyToClipboard, useQuasar } from "quasar";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { outlinedInfo } from "@quasar/extras/material-icons-outlined";
|
||||
import useStreams from "@/composables/useStreams";
|
||||
import { b64EncodeUnicode } from "@/utils/zincutils";
|
||||
import { useRouter } from "vue-router";
|
||||
import searchService from "@/services/search";
|
||||
import useNotifications from "@/composables/useNotifications";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TraceDetails",
|
||||
|
@ -231,14 +344,14 @@ export default defineComponent({
|
|||
TraceTimelineIcon,
|
||||
ServiceMapIcon,
|
||||
ChartRenderer: defineAsyncComponent(
|
||||
() => import("@/components/dashboards/panels/ChartRenderer.vue")
|
||||
() => import("@/components/dashboards/panels/ChartRenderer.vue"),
|
||||
),
|
||||
},
|
||||
emits: ["shareLink"],
|
||||
setup(props, { emit }) {
|
||||
const traceTree: any = ref([]);
|
||||
const spanMap: any = ref({});
|
||||
const { searchObj } = useTraces();
|
||||
const { searchObj, copyTracesUrl } = useTraces();
|
||||
const baseTracePosition: any = ref({});
|
||||
const collapseMapping: any = ref({});
|
||||
const traceRootSpan: any = ref(null);
|
||||
|
@ -247,6 +360,7 @@ export default defineComponent({
|
|||
const timeRange: any = ref({ start: 0, end: 0 });
|
||||
const store = useStore();
|
||||
const traceServiceMap: any = ref({});
|
||||
const { getStreams } = useStreams();
|
||||
const spanDimensions = {
|
||||
height: 30,
|
||||
barHeight: 8,
|
||||
|
@ -262,10 +376,22 @@ export default defineComponent({
|
|||
colors: ["#b7885e", "#1ab8be", "#ffcb99", "#f89570", "#839ae2"],
|
||||
};
|
||||
|
||||
const { showErrorNotification } = useNotifications();
|
||||
|
||||
const logStreams = ref([]);
|
||||
|
||||
const filteredStreamOptions = ref([]);
|
||||
|
||||
const streamSearchValue = ref<string>("");
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const $q = useQuasar();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const traceDetails = ref({});
|
||||
|
||||
const traceVisuals = [
|
||||
{ label: "Timeline", value: "timeline", icon: TraceTimelineIcon },
|
||||
{ label: "Service Map", value: "service_map", icon: ServiceMapIcon },
|
||||
|
@ -285,14 +411,112 @@ export default defineComponent({
|
|||
|
||||
const throttledResizing = ref<any>(null);
|
||||
|
||||
const serviceColorIndex = ref(0);
|
||||
const colors = ref(["#b7885e", "#1ab8be", "#ffcb99", "#f89570", "#839ae2"]);
|
||||
|
||||
const spanList: any = computed(() => {
|
||||
return searchObj.data.traceDetails.spanList;
|
||||
});
|
||||
|
||||
const isTimelineExpanded = ref(false);
|
||||
|
||||
const selectedStreamsString = computed(() =>
|
||||
searchObj.data.traceDetails.selectedLogStreams.join(", "),
|
||||
);
|
||||
|
||||
const showTraceDetails = ref(false);
|
||||
|
||||
onActivated(() => {
|
||||
const params = router.currentRoute.value.query;
|
||||
|
||||
if (
|
||||
searchObj.data.traceDetails.selectedTrace &&
|
||||
params.trace_id !== searchObj.data.traceDetails.selectedTrace?.trace_id
|
||||
) {
|
||||
resetTraceDetails();
|
||||
setupTraceDetails();
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeMount(async () => {
|
||||
setupTraceDetails();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => router.currentRoute.value.name,
|
||||
(curr, prev) => {
|
||||
if (prev === "logs" && curr === "traceDetails") {
|
||||
searchObj.meta.redirectedFromLogs = true;
|
||||
} else {
|
||||
searchObj.meta.redirectedFromLogs = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => router.currentRoute.value.query.trace_id,
|
||||
(_new, _old) => {
|
||||
// If trace_id changes, reset the trace details
|
||||
if (
|
||||
_new &&
|
||||
_new !== _old &&
|
||||
_new !== searchObj.data.traceDetails.selectedTrace?.trace_id
|
||||
) {
|
||||
resetTraceDetails();
|
||||
setupTraceDetails();
|
||||
const params = router.currentRoute.value.query;
|
||||
if (params.span_id) {
|
||||
updateSelectedSpan(params.span_id as string);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const backgroundStyle = computed(() => {
|
||||
return {
|
||||
background: store.state.theme === "dark" ? "#181a1b" : "#ffffff",
|
||||
};
|
||||
});
|
||||
|
||||
const resetTraceDetails = () => {
|
||||
searchObj.data.traceDetails.showSpanDetails = false;
|
||||
searchObj.data.traceDetails.selectedSpanId = "";
|
||||
searchObj.data.traceDetails.selectedTrace = {
|
||||
trace_id: "",
|
||||
trace_start_time: 0,
|
||||
trace_end_time: 0,
|
||||
};
|
||||
searchObj.data.traceDetails.spanList = [];
|
||||
searchObj.data.traceDetails.loading = true;
|
||||
};
|
||||
|
||||
const setupTraceDetails = async () => {
|
||||
showTraceDetails.value = false;
|
||||
searchObj.data.traceDetails.showSpanDetails = false;
|
||||
searchObj.data.traceDetails.selectedSpanId = "";
|
||||
|
||||
await getTraceMeta();
|
||||
await getStreams("logs", false)
|
||||
.then((res: any) => {
|
||||
logStreams.value = res.list.map((option: any) => option.name);
|
||||
filteredStreamOptions.value = JSON.parse(
|
||||
JSON.stringify(logStreams.value),
|
||||
);
|
||||
|
||||
if (!searchObj.data.traceDetails.selectedLogStreams.length)
|
||||
searchObj.data.traceDetails.selectedLogStreams.push(
|
||||
logStreams.value[0],
|
||||
);
|
||||
})
|
||||
.catch(() => Promise.reject())
|
||||
.finally(() => {});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
buildTracesTree();
|
||||
const params = router.currentRoute.value.query;
|
||||
if (params.span_id) {
|
||||
updateSelectedSpan(params.span_id as string);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
|
@ -302,7 +526,7 @@ export default defineComponent({
|
|||
buildTracesTree();
|
||||
} else traceTree.value = [];
|
||||
},
|
||||
{ immediate: true }
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const isSidebarOpen = computed(() => {
|
||||
|
@ -313,6 +537,156 @@ export default defineComponent({
|
|||
return searchObj.data.traceDetails.selectedSpanId;
|
||||
});
|
||||
|
||||
const getTraceMeta = () => {
|
||||
searchObj.loading = true;
|
||||
|
||||
let filter = (router.currentRoute.value.query.filter as string) || "";
|
||||
|
||||
if (filter?.length)
|
||||
filter += ` and trace_id='${router.currentRoute.value.query.trace_id}'`;
|
||||
else filter += `trace_id='${router.currentRoute.value.query.trace_id}'`;
|
||||
|
||||
searchService
|
||||
.get_traces({
|
||||
org_identifier: router.currentRoute.value.query
|
||||
.org_identifier as string,
|
||||
start_time: Number(router.currentRoute.value.query.from),
|
||||
end_time: Number(router.currentRoute.value.query.to),
|
||||
filter: filter || "",
|
||||
size: 1,
|
||||
from: 0,
|
||||
stream_name: router.currentRoute.value.query.stream as string,
|
||||
})
|
||||
.then(async (res: any) => {
|
||||
const trace = getTracesMetaData(res.data.hits)[0];
|
||||
if (!trace) {
|
||||
showTraceDetailsError();
|
||||
return;
|
||||
}
|
||||
searchObj.data.traceDetails.selectedTrace = trace;
|
||||
getTraceDetails();
|
||||
})
|
||||
.catch(() => {
|
||||
showTraceDetailsError();
|
||||
})
|
||||
.finally(() => {
|
||||
searchObj.loading = false;
|
||||
});
|
||||
};
|
||||
|
||||
const getDefaultRequest = () => {
|
||||
return {
|
||||
query: {
|
||||
sql: `select min(${store.state.zoConfig.timestamp_column}) as zo_sql_timestamp, min(start_time/1000) as trace_start_time, max(end_time/1000) as trace_end_time, min(service_name) as service_name, min(operation_name) as operation_name, count(trace_id) as spans, SUM(CASE WHEN span_status='ERROR' THEN 1 ELSE 0 END) as errors, max(duration) as duration, trace_id [QUERY_FUNCTIONS] from "[INDEX_NAME]" [WHERE_CLAUSE] group by trace_id order by zo_sql_timestamp DESC`,
|
||||
start_time: (new Date().getTime() - 900000) * 1000,
|
||||
end_time: new Date().getTime() * 1000,
|
||||
from: 0,
|
||||
size: 0,
|
||||
},
|
||||
encoding: "base64",
|
||||
};
|
||||
};
|
||||
|
||||
const buildTraceSearchQuery = (trace: any) => {
|
||||
const req = getDefaultRequest();
|
||||
req.query.from = 0;
|
||||
req.query.size = 1000;
|
||||
req.query.start_time =
|
||||
Math.ceil(
|
||||
Number(searchObj.data.traceDetails.selectedTrace?.trace_start_time),
|
||||
) - 30000000;
|
||||
req.query.end_time =
|
||||
Math.ceil(
|
||||
Number(searchObj.data.traceDetails.selectedTrace?.trace_end_time),
|
||||
) + 30000000;
|
||||
|
||||
req.query.sql = b64EncodeUnicode(
|
||||
`SELECT * FROM ${trace.stream} WHERE trace_id = '${trace.trace_id}' ORDER BY start_time`,
|
||||
) as string;
|
||||
|
||||
return req;
|
||||
};
|
||||
|
||||
const getTraceDetails = async () => {
|
||||
searchObj.data.traceDetails.loading = true;
|
||||
searchObj.data.traceDetails.spanList = [];
|
||||
const req = buildTraceSearchQuery(router.currentRoute.value.query);
|
||||
|
||||
searchService
|
||||
.search(
|
||||
{
|
||||
org_identifier: router.currentRoute.value.query
|
||||
?.org_identifier as string,
|
||||
query: req,
|
||||
page_type: "traces",
|
||||
},
|
||||
"UI",
|
||||
)
|
||||
.then((res: any) => {
|
||||
searchObj.data.traceDetails.spanList = res.data?.hits || [];
|
||||
buildTracesTree();
|
||||
})
|
||||
.finally(() => {
|
||||
searchObj.data.traceDetails.loading = false;
|
||||
});
|
||||
};
|
||||
|
||||
const getTracesMetaData = (traces: any[]) => {
|
||||
if (!traces.length) return [];
|
||||
|
||||
return traces.map((trace) => {
|
||||
const _trace = {
|
||||
trace_id: trace.trace_id,
|
||||
trace_start_time: Math.round(trace.start_time / 1000),
|
||||
trace_end_time: Math.round(trace.end_time / 1000),
|
||||
service_name: trace.service_name,
|
||||
operation_name: trace.operation_name,
|
||||
spans: trace.spans[0],
|
||||
errors: trace.spans[1],
|
||||
duration: trace.duration,
|
||||
services: {} as any,
|
||||
zo_sql_timestamp: new Date(trace.start_time / 1000).getTime(),
|
||||
};
|
||||
trace.service_name.forEach((service: any) => {
|
||||
if (!searchObj.meta.serviceColors[service.service_name]) {
|
||||
if (serviceColorIndex.value >= colors.value.length)
|
||||
generateNewColor();
|
||||
|
||||
searchObj.meta.serviceColors[service.service_name] =
|
||||
colors.value[serviceColorIndex.value];
|
||||
|
||||
serviceColorIndex.value++;
|
||||
}
|
||||
_trace.services[service.service_name] = service.count;
|
||||
});
|
||||
return _trace;
|
||||
});
|
||||
};
|
||||
|
||||
const showTraceDetailsError = () => {
|
||||
showErrorNotification(
|
||||
`Trace ${router.currentRoute.value.query.trace_id} not found`,
|
||||
);
|
||||
const query = cloneDeep(router.currentRoute.value.query);
|
||||
delete query.trace_id;
|
||||
router.push({
|
||||
name: "traces",
|
||||
query: {
|
||||
...query,
|
||||
},
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
function generateNewColor() {
|
||||
// Generate a color in HSL format
|
||||
const hue = (colors.value.length * 137.508) % 360; // Using golden angle approximation
|
||||
const saturation = 70 + (colors.value.length % 2) * 15;
|
||||
const lightness = 50;
|
||||
colors.value.push(`hsl(${hue}, ${saturation}%, ${lightness}%)`);
|
||||
return colors;
|
||||
}
|
||||
|
||||
const calculateTracePosition = () => {
|
||||
const tics = [];
|
||||
baseTracePosition.value["durationMs"] = timeRange.value.end;
|
||||
|
@ -415,7 +789,7 @@ export default defineComponent({
|
|||
},
|
||||
hasChildSpans: !!span.spans.length,
|
||||
currentIndex: index,
|
||||
})
|
||||
}),
|
||||
);
|
||||
if (collapseMapping.value[span.spanId]) {
|
||||
if (span.spans.length) {
|
||||
|
@ -426,7 +800,7 @@ export default defineComponent({
|
|||
span.totalSpans = span.spans.reduce(
|
||||
(acc: number, span: any) =>
|
||||
acc + ((span?.spans?.length || 0) + (span?.totalSpans || 0)),
|
||||
0
|
||||
0,
|
||||
);
|
||||
}
|
||||
return (span?.spans?.length || 0) + (span?.totalSpans || 0);
|
||||
|
@ -457,7 +831,7 @@ export default defineComponent({
|
|||
currentColumn: any[],
|
||||
serviceName: string,
|
||||
depth: number,
|
||||
height: number
|
||||
height: number,
|
||||
) => {
|
||||
maxHeight[depth] =
|
||||
maxHeight[depth] === undefined ? 1 : maxHeight[depth] + 1;
|
||||
|
@ -477,7 +851,7 @@ export default defineComponent({
|
|||
});
|
||||
if (span.spans && span.spans.length) {
|
||||
span.spans.forEach((_span: any) =>
|
||||
getService(_span, children, span.serviceName, depth + 1, height)
|
||||
getService(_span, children, span.serviceName, depth + 1, height),
|
||||
);
|
||||
} else {
|
||||
if (maxDepth < depth) maxDepth = depth;
|
||||
|
@ -486,7 +860,7 @@ export default defineComponent({
|
|||
}
|
||||
if (span.spans && span.spans.length) {
|
||||
span.spans.forEach((span: any) =>
|
||||
getService(span, currentColumn, serviceName, depth + 1, height)
|
||||
getService(span, currentColumn, serviceName, depth + 1, height),
|
||||
);
|
||||
} else {
|
||||
if (maxDepth < depth) maxDepth = depth;
|
||||
|
@ -497,7 +871,7 @@ export default defineComponent({
|
|||
});
|
||||
traceServiceMap.value = convertTraceServiceMapData(
|
||||
cloneDeep(serviceTree),
|
||||
maxDepth
|
||||
maxDepth,
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -524,6 +898,7 @@ export default defineComponent({
|
|||
style: {
|
||||
color: "",
|
||||
},
|
||||
links: JSON.parse(span.links || "[]"),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -572,8 +947,8 @@ export default defineComponent({
|
|||
x0: absoluteStartTime,
|
||||
x1: Number(
|
||||
(absoluteStartTime + spanPositionList.value[i].durationMs).toFixed(
|
||||
4
|
||||
)
|
||||
4,
|
||||
),
|
||||
),
|
||||
fillcolor: spanPositionList.value[i].style.color,
|
||||
});
|
||||
|
@ -586,100 +961,6 @@ export default defineComponent({
|
|||
timeRange.value.end = data.end || 0;
|
||||
calculateTracePosition();
|
||||
};
|
||||
const mockServiceMap = [
|
||||
{
|
||||
name: "Service A",
|
||||
color: "#000000",
|
||||
duration: "10",
|
||||
children: [
|
||||
{
|
||||
name: "Service B",
|
||||
color: "#000000",
|
||||
duration: "10",
|
||||
|
||||
children: [
|
||||
{
|
||||
name: "Service c",
|
||||
color: "#000000",
|
||||
duration: "10",
|
||||
|
||||
children: [
|
||||
{
|
||||
name: "Service F",
|
||||
color: "#000000",
|
||||
duration: "10",
|
||||
|
||||
children: [
|
||||
{
|
||||
name: "Service G",
|
||||
color: "#000000",
|
||||
duration: "10",
|
||||
|
||||
children: [
|
||||
{
|
||||
name: "Service H",
|
||||
color: "#000000",
|
||||
duration: "10",
|
||||
|
||||
children: [
|
||||
{
|
||||
name: "Service H",
|
||||
color: "#000000",
|
||||
duration: "10",
|
||||
|
||||
children: [
|
||||
{
|
||||
name: "Service H",
|
||||
color: "#000000",
|
||||
duration: "10",
|
||||
|
||||
children: [
|
||||
{
|
||||
name: "Service H H H H H H H H H H H H H H H H H",
|
||||
color: "#000000",
|
||||
duration: "10",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Service E",
|
||||
color: "#000000",
|
||||
duration: "10",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ name: "Service D", color: "#000000", duration: "10" },
|
||||
{ name: "Service D", color: "#000000", duration: "10" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Service X",
|
||||
color: "#000000",
|
||||
duration: "10",
|
||||
},
|
||||
{
|
||||
name: "Service Y",
|
||||
color: "#000000",
|
||||
duration: "10",
|
||||
children: [
|
||||
{
|
||||
name: "Service YA",
|
||||
color: "#000000",
|
||||
duration: "10",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
onMounted(() => {
|
||||
throttledResizing.value = throttle(resizing, 50);
|
||||
|
@ -719,10 +1000,84 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
const shareLink = () => {
|
||||
emit("shareLink");
|
||||
copyTracesUrl({
|
||||
from: router.currentRoute.value.query.from as string,
|
||||
to: router.currentRoute.value.query.to as string,
|
||||
});
|
||||
};
|
||||
|
||||
const redirectToLogs = () => {
|
||||
if (!searchObj.data.traceDetails.selectedTrace) {
|
||||
return;
|
||||
}
|
||||
const stream: string =
|
||||
searchObj.data.traceDetails.selectedLogStreams.join(",");
|
||||
const from =
|
||||
searchObj.data.traceDetails.selectedTrace?.trace_start_time - 60000000;
|
||||
const to =
|
||||
searchObj.data.traceDetails.selectedTrace?.trace_end_time + 60000000;
|
||||
const refresh = 0;
|
||||
|
||||
const query = b64EncodeUnicode(
|
||||
`${store.state.organizationData?.organizationSettings?.trace_id_field_name}='${spanList.value[0]["trace_id"]}'`,
|
||||
);
|
||||
|
||||
router.push({
|
||||
path: "/logs",
|
||||
query: {
|
||||
stream_type: "logs",
|
||||
stream,
|
||||
from,
|
||||
to,
|
||||
refresh,
|
||||
sql_mode: "false",
|
||||
query,
|
||||
org_identifier: store.state.selectedOrganization.identifier,
|
||||
show_histogram: "true",
|
||||
type: "trace_explorer",
|
||||
quick_mode: "false",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const filterStreamFn = (val: any = "") => {
|
||||
filteredStreamOptions.value = logStreams.value.filter((stream: any) => {
|
||||
return stream.toLowerCase().indexOf(val.toLowerCase()) > -1;
|
||||
});
|
||||
};
|
||||
|
||||
const openTraceDetails = () => {
|
||||
searchObj.data.traceDetails.showSpanDetails = true;
|
||||
showTraceDetails.value = true;
|
||||
};
|
||||
|
||||
const updateSelectedSpan = (spanId: string) => {
|
||||
showTraceDetails.value = false;
|
||||
searchObj.data.traceDetails.showSpanDetails = true;
|
||||
searchObj.data.traceDetails.selectedSpanId = spanId;
|
||||
};
|
||||
|
||||
const routeToTracesList = () => {
|
||||
const query = cloneDeep(router.currentRoute.value.query);
|
||||
delete query.trace_id;
|
||||
|
||||
if (searchObj.data.datetime.type === "relative") {
|
||||
query.period = searchObj.data.datetime.relativeTimePeriod;
|
||||
} else {
|
||||
query.from = searchObj.data.datetime.startTime.toString();
|
||||
query.to = searchObj.data.datetime.endTime.toString();
|
||||
}
|
||||
|
||||
router.push({
|
||||
name: "traces",
|
||||
query: {
|
||||
...query,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
router,
|
||||
t,
|
||||
traceTree,
|
||||
collapseMapping,
|
||||
|
@ -742,7 +1097,6 @@ export default defineComponent({
|
|||
traceChart,
|
||||
updateChart,
|
||||
traceServiceMap,
|
||||
mockServiceMap,
|
||||
activeVisual,
|
||||
traceVisuals,
|
||||
getImageURL,
|
||||
|
@ -754,6 +1108,18 @@ export default defineComponent({
|
|||
copyToClipboard,
|
||||
copyTraceId,
|
||||
shareLink,
|
||||
outlinedInfo,
|
||||
redirectToLogs,
|
||||
filteredStreamOptions,
|
||||
filterStreamFn,
|
||||
streamSearchValue,
|
||||
selectedStreamsString,
|
||||
openTraceDetails,
|
||||
showTraceDetails,
|
||||
traceDetails,
|
||||
updateSelectedSpan,
|
||||
backgroundStyle,
|
||||
routeToTracesList,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -765,6 +1131,7 @@ $seperatorWidth: 2px;
|
|||
$toolbarHeight: 50px;
|
||||
$traceHeaderHeight: 30px;
|
||||
$traceChartHeight: 210px;
|
||||
$appNavbarHeight: 57px;
|
||||
|
||||
$traceChartCollapseHeight: 42px;
|
||||
|
||||
|
@ -772,7 +1139,6 @@ $traceChartCollapseHeight: 42px;
|
|||
height: $toolbarHeight;
|
||||
}
|
||||
.trace-details {
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
.histogram-container-full {
|
||||
|
@ -784,23 +1150,27 @@ $traceChartCollapseHeight: 42px;
|
|||
|
||||
.histogram-sidebar {
|
||||
width: $sidebarWidth;
|
||||
height: calc(100vh - $toolbarHeight - $traceChartHeight - 44px);
|
||||
height: calc(
|
||||
100vh - $toolbarHeight - $traceChartHeight - 44px - $appNavbarHeight
|
||||
);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
&.full {
|
||||
height: calc(100vh - $toolbarHeight - 8px - 44px);
|
||||
height: calc(100vh - $toolbarHeight - 8px - 44px - $appNavbarHeight);
|
||||
}
|
||||
}
|
||||
|
||||
.histogram-spans-container {
|
||||
height: calc(100vh - $toolbarHeight - $traceChartHeight - 44px);
|
||||
height: calc(
|
||||
100vh - $toolbarHeight - $traceChartHeight - 44px - $appNavbarHeight
|
||||
);
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
|
||||
&.full {
|
||||
height: calc(100vh - $toolbarHeight - 8px - 44px);
|
||||
height: calc(100vh - $toolbarHeight - 8px - 44px - $appNavbarHeight);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -824,6 +1194,22 @@ $traceChartCollapseHeight: 42px;
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.log-stream-search-input {
|
||||
width: 226px;
|
||||
|
||||
.q-field .q-field__control {
|
||||
padding: 0px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-trace-id {
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.toolbar-operation-name {
|
||||
max-width: 225px;
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.trace-details {
|
||||
|
@ -869,4 +1255,36 @@ $traceChartCollapseHeight: 42px;
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.trace-logs-selector {
|
||||
.q-field {
|
||||
span {
|
||||
display: inline-block;
|
||||
width: 180px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.log-stream-search-input {
|
||||
.q-field .q-field__control {
|
||||
padding: 0px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.traces-view-logs-btn {
|
||||
height: 36px;
|
||||
margin-left: -1px;
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
|
||||
.q-btn__content {
|
||||
span {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -30,7 +30,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
@click="closeSidebar"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="q-pb-sm q-pt-xs flex flex-wrap">
|
||||
<div class="q-pb-sm q-pt-xs flex flex-wrap trace-details-toolbar-container">
|
||||
<div
|
||||
:title="span.operation_name"
|
||||
class="q-px-sm q-pb-none ellipsis non-selectable"
|
||||
|
@ -53,6 +53,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<span class="text-grey-7">Duration: </span>
|
||||
<span>{{ getDuration }}</span>
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
class="q-mx-xs view-span-logs-btn"
|
||||
size="10px"
|
||||
icon="search"
|
||||
dense
|
||||
padding="xs sm"
|
||||
no-caps
|
||||
:title="t('traces.viewLogs')"
|
||||
@click.stop="viewSpanLogs"
|
||||
>
|
||||
View Logs</q-btn
|
||||
>
|
||||
</div>
|
||||
<q-tabs
|
||||
v-model="activeTab"
|
||||
|
@ -319,6 +332,7 @@ import { useStore } from "vuex";
|
|||
import { useI18n } from "vue-i18n";
|
||||
import { computed } from "vue";
|
||||
import { formatTimeWithSuffix } from "@/utils/zincutils";
|
||||
import useTraces from "@/composables/useTraces";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TraceDetailsSidebar",
|
||||
|
@ -328,7 +342,7 @@ export default defineComponent({
|
|||
default: () => null,
|
||||
},
|
||||
},
|
||||
emits: ["close"],
|
||||
emits: ["close", "view-logs"],
|
||||
setup(props, { emit }) {
|
||||
const { t } = useI18n();
|
||||
const activeTab = ref("tags");
|
||||
|
@ -344,6 +358,7 @@ export default defineComponent({
|
|||
const pagination: any = ref({
|
||||
rowsPerPage: 0,
|
||||
});
|
||||
const { buildQueryDetails, navigateToLogs } = useTraces();
|
||||
|
||||
watch(
|
||||
() => props.span,
|
||||
|
@ -351,11 +366,14 @@ export default defineComponent({
|
|||
tags.value = {};
|
||||
processes.value = {};
|
||||
spanDetails.value = getFormattedSpanDetails();
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
|
||||
const getDuration = computed(() =>
|
||||
formatTimeWithSuffix(props.span.duration)
|
||||
formatTimeWithSuffix(props.span.duration),
|
||||
);
|
||||
|
||||
onBeforeMount(() => {
|
||||
|
@ -370,12 +388,12 @@ export default defineComponent({
|
|||
field: (row: any) =>
|
||||
date.formatDate(
|
||||
Math.floor(row[store.state.zoConfig.timestamp_column] / 1000000),
|
||||
"MMM DD, YYYY HH:mm:ss.SSS Z"
|
||||
"MMM DD, YYYY HH:mm:ss.SSS Z",
|
||||
),
|
||||
prop: (row: any) =>
|
||||
date.formatDate(
|
||||
Math.floor(row[store.state.zoConfig.timestamp_column] / 1000000),
|
||||
"MMM DD, YYYY HH:mm:ss.SSS Z"
|
||||
"MMM DD, YYYY HH:mm:ss.SSS Z",
|
||||
),
|
||||
label: "Timestamp",
|
||||
align: "left",
|
||||
|
@ -397,12 +415,12 @@ export default defineComponent({
|
|||
field: (row: any) =>
|
||||
date.formatDate(
|
||||
Math.floor(row[store.state.zoConfig.timestamp_column] / 1000000),
|
||||
"MMM DD, YYYY HH:mm:ss.SSS Z"
|
||||
"MMM DD, YYYY HH:mm:ss.SSS Z",
|
||||
),
|
||||
prop: (row: any) =>
|
||||
date.formatDate(
|
||||
Math.floor(row[store.state.zoConfig.timestamp_column] / 1000000),
|
||||
"MMM DD, YYYY HH:mm:ss.SSS Z"
|
||||
"MMM DD, YYYY HH:mm:ss.SSS Z",
|
||||
),
|
||||
label: "Timestamp",
|
||||
align: "left",
|
||||
|
@ -420,7 +438,7 @@ export default defineComponent({
|
|||
|
||||
const getExceptionEvents = computed(() => {
|
||||
return spanDetails.value.events.filter(
|
||||
(event: any) => event.name === "exception"
|
||||
(event: any) => event.name === "exception",
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -455,14 +473,14 @@ export default defineComponent({
|
|||
spanDetails.attrs[store.state.zoConfig.timestamp_column] =
|
||||
date.formatDate(
|
||||
Math.floor(
|
||||
spanDetails.attrs[store.state.zoConfig.timestamp_column] / 1000
|
||||
spanDetails.attrs[store.state.zoConfig.timestamp_column] / 1000,
|
||||
),
|
||||
"MMM DD, YYYY HH:mm:ss.SSS Z"
|
||||
"MMM DD, YYYY HH:mm:ss.SSS Z",
|
||||
);
|
||||
spanDetails.attrs.span_kind = getSpanKind(spanDetails.attrs.span_kind);
|
||||
|
||||
spanDetails.events = JSON.parse(props.span.events).map(
|
||||
(event: any) => event
|
||||
spanDetails.events = JSON.parse(props.span.events || "[]").map(
|
||||
(event: any) => event,
|
||||
);
|
||||
|
||||
return spanDetails;
|
||||
|
@ -484,6 +502,8 @@ export default defineComponent({
|
|||
watch(
|
||||
() => props.span,
|
||||
() => {
|
||||
tags.value = {};
|
||||
processes.value = {};
|
||||
Object.keys(props.span).forEach((key: string) => {
|
||||
if (!span_details.has(key)) {
|
||||
tags.value[key] = props.span[key];
|
||||
|
@ -499,7 +519,7 @@ export default defineComponent({
|
|||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
}
|
||||
},
|
||||
);
|
||||
function formatStackTrace(trace: any) {
|
||||
// Split the trace into lines
|
||||
|
@ -519,6 +539,11 @@ export default defineComponent({
|
|||
return formattedLines.join("\n");
|
||||
}
|
||||
|
||||
const viewSpanLogs = () => {
|
||||
const queryDetails = buildQueryDetails(props.span);
|
||||
navigateToLogs(queryDetails);
|
||||
};
|
||||
|
||||
return {
|
||||
t,
|
||||
activeTab,
|
||||
|
@ -535,6 +560,7 @@ export default defineComponent({
|
|||
getExceptionEvents,
|
||||
exceptionEventColumns,
|
||||
getDuration,
|
||||
viewSpanLogs,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -712,7 +738,7 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
.span_details_tab-panels {
|
||||
height: calc(100% - 102px);
|
||||
height: calc(100% - 104px);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
@ -738,4 +764,18 @@ export default defineComponent({
|
|||
padding: 8px 0 8px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.view-span-logs-btn {
|
||||
.q-btn__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
|
||||
.q-icon {
|
||||
margin-right: 2px !important;
|
||||
font-size: 14px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -15,7 +15,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
-->
|
||||
|
||||
<template>
|
||||
<template v-for="span in spans as any[]" :key="span.spanId">
|
||||
<template v-for="(span, index) in spans as any[]" :key="span.spanId">
|
||||
<div
|
||||
:style="{
|
||||
position: 'relative',
|
||||
|
@ -41,10 +41,29 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
:title="span.operationName"
|
||||
>
|
||||
<div
|
||||
class="flex no-wrap q-pt-sm full-width"
|
||||
class="flex no-wrap q-pt-sm full-width relative-position operation-name-container"
|
||||
:class="store.state.theme === 'dark' ? 'bg-dark' : 'bg-white'"
|
||||
:style="{ height: '30px' }"
|
||||
@mouseover="() => (spanHoveredIndex = index)"
|
||||
@mouseout="() => (spanHoveredIndex = -1)"
|
||||
>
|
||||
<div
|
||||
class="absolute view-logs-container"
|
||||
:class="spanHoveredIndex === index ? 'show' : ''"
|
||||
>
|
||||
<q-btn
|
||||
class="q-mx-xs view-span-logs"
|
||||
:class="store.state.theme === 'dark' ? 'bg-dark' : 'bg-white'"
|
||||
size="10px"
|
||||
icon="search"
|
||||
dense
|
||||
no-caps
|
||||
:title="t('traces.viewLogs')"
|
||||
@click.stop="viewSpanLogs(span)"
|
||||
>
|
||||
<!-- <span class="text view-logs-btn-text">View Logs</span> -->
|
||||
</q-btn>
|
||||
</div>
|
||||
<div
|
||||
v-if="span.hasChildSpans"
|
||||
:style="{
|
||||
|
@ -123,17 +142,24 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
:spanData="spanMap[span.spanId]"
|
||||
@toggle-collapse="toggleSpanCollapse"
|
||||
@select-span="selectSpan"
|
||||
@mouseover="() => (spanHoveredIndex = index)"
|
||||
@mouseout="() => (spanHoveredIndex = -1)"
|
||||
@view-logs="viewSpanLogs(span)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from "vue";
|
||||
import { defineComponent, onBeforeMount, ref } from "vue";
|
||||
import { getImageURL } from "@/utils/zincutils";
|
||||
import useTraces from "@/composables/useTraces";
|
||||
import { useStore } from "vuex";
|
||||
import SpanBlock from "./SpanBlock.vue";
|
||||
import type { Ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { b64EncodeStandard } from "@/utils/zincutils";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TraceTree",
|
||||
|
@ -171,17 +197,28 @@ export default defineComponent({
|
|||
default: 0,
|
||||
},
|
||||
},
|
||||
emits: ["toggleCollapse"],
|
||||
emits: ["toggleCollapse", "selectSpan"],
|
||||
setup(props, { emit }) {
|
||||
const { searchObj } = useTraces();
|
||||
const { searchObj, buildQueryDetails, navigateToLogs } = useTraces();
|
||||
const store = useStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const spanHoveredIndex = ref(-1);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function toggleSpanCollapse(spanId: number | string) {
|
||||
emit("toggleCollapse", spanId);
|
||||
}
|
||||
const selectSpan = (spanId: string) => {
|
||||
searchObj.data.traceDetails.showSpanDetails = true;
|
||||
searchObj.data.traceDetails.selectedSpanId = spanId;
|
||||
emit("selectSpan", spanId);
|
||||
};
|
||||
|
||||
// Main function to view span logs
|
||||
const viewSpanLogs = (span: any) => {
|
||||
const queryDetails = buildQueryDetails(span);
|
||||
navigateToLogs(queryDetails);
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -189,6 +226,9 @@ export default defineComponent({
|
|||
getImageURL,
|
||||
selectSpan,
|
||||
store,
|
||||
viewSpanLogs,
|
||||
t,
|
||||
spanHoveredIndex,
|
||||
};
|
||||
},
|
||||
components: { SpanBlock },
|
||||
|
@ -196,6 +236,10 @@ export default defineComponent({
|
|||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.view-logs-container {
|
||||
top: 7px;
|
||||
right: 0;
|
||||
}
|
||||
.spans-container {
|
||||
position: relative;
|
||||
}
|
||||
|
@ -205,4 +249,32 @@ export default defineComponent({
|
|||
height: auto;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.operation-name-container {
|
||||
.view-logs-container {
|
||||
visibility: hidden;
|
||||
}
|
||||
.view-logs-container {
|
||||
&.show {
|
||||
visibility: visible !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.view-logs-btn-text {
|
||||
visibility: visible;
|
||||
}
|
||||
.view-span-logs {
|
||||
background-color: inherit;
|
||||
.view-logs-btn-text {
|
||||
visibility: hidden;
|
||||
width: 0px;
|
||||
transition: width 0.3s ease-in;
|
||||
}
|
||||
&:hover .view-logs-btn-text {
|
||||
visibility: visible;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -44,6 +44,8 @@ const organizationObj = {
|
|||
folders: [],
|
||||
organizationSettings: {
|
||||
scrape_interval: 15,
|
||||
trace_id_field_name: "traceId",
|
||||
span_id_field_name: "spanId",
|
||||
},
|
||||
isDataIngested: false,
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue