diff --git a/src/common/meta/organization.rs b/src/common/meta/organization.rs index 656157f8c5..70b993847e 100644 --- a/src/common/meta/organization.rs +++ b/src/common/meta/organization.rs @@ -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(), } } } diff --git a/src/common/meta/traces.rs b/src/common/meta/traces.rs index 227d6e0362..f292c1f642 100644 --- a/src/common/meta/traces.rs +++ b/src/common/meta/traces.rs @@ -41,6 +41,7 @@ pub struct Span { #[serde(skip_serializing_if = "HashMap::is_empty")] pub service: HashMap, 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, } +#[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, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub trace_state: Option, +} + +#[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, + #[serde(default)] + pub dropped_attributes_count: u32, +} + #[derive(Debug, Default, Serialize, Deserialize, ToSchema)] pub struct ExportTraceServiceResponse { // The details of a partially successful export request. diff --git a/src/service/traces/mod.rs b/src/service/traces/mod.rs index 9406114b96..7473783a92 100644 --- a/src/service/traces/mod.rs +++ b/src/service/traces/mod.rs @@ -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 = 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(); diff --git a/src/service/traces/otlp_http.rs b/src/service/traces/otlp_http.rs index 6cebba68c0..94db9b213c 100644 --- a/src/service/traces/otlp_http.rs +++ b/src/service/traces/otlp_http.rs @@ -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 = 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 = HashMap::new(); + let mut link_att_map: HashMap = 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(); diff --git a/tests/ui-testing/playwright-tests/multiselect-stream.spec.js b/tests/ui-testing/playwright-tests/multiselect-stream.spec.js index 20e70b5d6b..1a0441b0e9 100644 --- a/tests/ui-testing/playwright-tests/multiselect-stream.spec.js +++ b/tests/ui-testing/playwright-tests/multiselect-stream.spec.js @@ -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 }); diff --git a/tests/ui-testing/playwright-tests/sanity.spec.js b/tests/ui-testing/playwright-tests/sanity.spec.js index cef91dcc9e..8e0755c824 100644 --- a/tests/ui-testing/playwright-tests/sanity.spec.js +++ b/tests/ui-testing/playwright-tests/sanity.spec.js @@ -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(); diff --git a/web/src/components/DateTime.vue b/web/src/components/DateTime.vue index 864df96963..3f918c24f5 100644 --- a/web/src/components/DateTime.vue +++ b/web/src/components/DateTime.vue @@ -23,7 +23,7 @@ along with this program. If not, see . 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, }; }, }); diff --git a/web/src/components/settings/OrganizationSettings.vue b/web/src/components/settings/OrganizationSettings.vue new file mode 100644 index 0000000000..a4f55f1d33 --- /dev/null +++ b/web/src/components/settings/OrganizationSettings.vue @@ -0,0 +1,172 @@ + + + + + diff --git a/web/src/components/settings/index.vue b/web/src/components/settings/index.vue index 244e651c19..017cb31872 100644 --- a/web/src/components/settings/index.vue +++ b/web/src/components/settings/index.vue @@ -16,15 +16,15 @@ along with this program. If not, see .