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:
Omkar Kesarkhane 2024-08-07 20:07:40 +05:30 committed by GitHub
parent 337e973a70
commit cfdc09fcba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 2040 additions and 523 deletions

View File

@ -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(),
}
}
}

View File

@ -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.

View File

@ -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();

View File

@ -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();

View File

@ -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 });

View File

@ -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();

View File

@ -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,
};
},
});

View File

@ -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>

View File

@ -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,
};

View File

@ -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",

View File

@ -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);
},
},
],
},
];

View File

@ -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),
);
});
};

View File

@ -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;

View File

@ -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;

View File

@ -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"
}
}

View File

@ -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() {

View File

@ -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();
}
},
);
}
}

View File

@ -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>

View File

@ -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

View File

@ -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;

View File

@ -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,

View File

@ -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,

View File

@ -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,
};
},
});

View File

@ -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>

View File

@ -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 {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -44,6 +44,8 @@ const organizationObj = {
folders: [],
organizationSettings: {
scrape_interval: 15,
trace_id_field_name: "traceId",
span_id_field_name: "spanId",
},
isDataIngested: false,
};