diff --git a/CHANGELOG.md b/CHANGELOG.md index f75ddd9f03..6a249cf322 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,10 @@ ## Unreleased -- Allow log ingestion behind a flag, only for internal use currently. ([#4471](https://github.com/getsentry/relay/pull/4471)) - **Features**: +- Preliminary breadcrumb to log conversion. ([#4479](https://github.com/getsentry/relay/pull/4479)) +- Allow log ingestion behind a flag, only for internal use currently. ([#4471](https://github.com/getsentry/relay/pull/4471)) - Add configuration option to limit the amount of concurrent http connections. ([#4453](https://github.com/getsentry/relay/pull/4453)) - Add flags context to event schema. ([#4458](https://github.com/getsentry/relay/pull/4458)) - Add support for view hierarchy attachment scrubbing. ([#4452](https://github.com/getsentry/relay/pull/4452)) diff --git a/Cargo.lock b/Cargo.lock index a46174d682..b92d4e295d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3768,6 +3768,7 @@ dependencies = [ "once_cell", "opentelemetry-proto", "relay-event-schema", + "relay-log", "relay-protocol", "serde_json", ] diff --git a/relay-dynamic-config/src/feature.rs b/relay-dynamic-config/src/feature.rs index 2210a1b1e7..dcd0146b0f 100644 --- a/relay-dynamic-config/src/feature.rs +++ b/relay-dynamic-config/src/feature.rs @@ -107,6 +107,11 @@ pub enum Feature { /// Serialized as `organizations:ourlogs-ingestion`. #[serde(rename = "organizations:ourlogs-ingestion")] OurLogsIngestion, + + /// Enables extracting logs from breadcrumbs for our log product. + #[serde(rename = "projects:ourlogs-breadcrumb-extraction")] + OurLogsBreadcrumbExtraction, + /// This feature has graduated and is hard-coded for external Relays. #[doc(hidden)] #[serde(rename = "projects:profiling-ingest-unsampled-profiles")] diff --git a/relay-dynamic-config/src/global.rs b/relay-dynamic-config/src/global.rs index 204ec880b2..1200c631e5 100644 --- a/relay-dynamic-config/src/global.rs +++ b/relay-dynamic-config/src/global.rs @@ -174,6 +174,29 @@ pub struct Options { )] pub span_extraction_sample_rate: Option, + /// Extract logs from breadrumbs for a fraction of sent breadcrumbs. + /// + /// `None` is the default and interpreted as extract nothing. + /// + /// Note: Any value below 1.0 will cause the product to break, so use with caution. + #[serde( + rename = "relay.ourlogs-breadcrumb-extraction.sample-rate", + deserialize_with = "default_on_error", + skip_serializing_if = "is_default" + )] + pub ourlogs_breadcrumb_extraction_sample_rate: Option, + + /// The maximum number of breadcrumbs to convert to OurLogs. + /// + /// When converting breadcrumbs to OurLogs, only up to this many breadcrumbs will be converted. + /// Defaults to 100. + #[serde( + rename = "relay.ourlogs-breadcrumb-extraction.max-breadcrumbs-converted", + deserialize_with = "default_on_error", + skip_serializing_if = "is_default" + )] + pub ourlogs_breadcrumb_extraction_max_breadcrumbs_converted: usize, + /// List of values on span description that are allowed to be sent to Sentry without being scrubbed. /// /// At this point, it doesn't accept IP addresses in CIDR format.. yet. diff --git a/relay-event-schema/src/protocol/contexts/mod.rs b/relay-event-schema/src/protocol/contexts/mod.rs index 2306b4a060..6dd48f4ae2 100644 --- a/relay-event-schema/src/protocol/contexts/mod.rs +++ b/relay-event-schema/src/protocol/contexts/mod.rs @@ -8,6 +8,7 @@ mod monitor; mod nel; mod os; mod otel; +mod our_logs; mod performance_score; mod profile; mod replay; @@ -25,6 +26,7 @@ pub use monitor::*; pub use nel::*; pub use os::*; pub use otel::*; +pub use our_logs::*; pub use performance_score::*; pub use profile::*; pub use replay::*; @@ -90,6 +92,9 @@ pub enum Context { Nel(Box), /// Performance score information. PerformanceScore(Box), + /// Ourlogs (logs product) information. + #[metastructure(tag = "sentry_logs")] + OurLogs(Box), /// Additional arbitrary fields for forwards compatibility. #[metastructure(fallback_variant)] Other(#[metastructure(pii = "true")] Object), @@ -109,7 +114,7 @@ impl From for ContextInner { /// name is `contexts`. /// /// The `contexts` type can be used to define arbitrary contextual data on the event. It accepts an -/// object of key/value pairs. The key is the “alias” of the context and can be freely chosen. +/// object of key/value pairs. The key is the "alias" of the context and can be freely chosen. /// However, as per policy, it should match the type of the context unless there are two values for /// a type. You can omit `type` if the key name is the type. /// diff --git a/relay-event-schema/src/protocol/contexts/our_logs.rs b/relay-event-schema/src/protocol/contexts/our_logs.rs new file mode 100644 index 0000000000..8c20a35f2b --- /dev/null +++ b/relay-event-schema/src/protocol/contexts/our_logs.rs @@ -0,0 +1,68 @@ +use relay_protocol::{Annotated, Empty, FromValue, IntoValue, Object, Value}; + +use crate::processor::ProcessValue; + +/// Our Logs context. +/// +/// The Sentry Logs context contains information about our logging product (ourlogs) for an event. +#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)] +pub struct OurLogsContext { + /// Whether breadcrumbs are being deduplicated. + pub deduplicated_breadcrumbs: Annotated, + + /// Additional arbitrary fields for forwards compatibility. + #[metastructure(additional_properties, retain = true)] + pub other: Object, +} + +impl super::DefaultContext for OurLogsContext { + fn default_key() -> &'static str { + "sentry_logs" // Ourlogs is an internal name, and 'logs' likely has conflicts with user contexts. + } + + fn from_context(context: super::Context) -> Option { + match context { + super::Context::OurLogs(c) => Some(*c), + _ => None, + } + } + + fn cast(context: &super::Context) -> Option<&Self> { + match context { + super::Context::OurLogs(c) => Some(c), + _ => None, + } + } + + fn cast_mut(context: &mut super::Context) -> Option<&mut Self> { + match context { + super::Context::OurLogs(c) => Some(c), + _ => None, + } + } + + fn into_context(self) -> super::Context { + super::Context::OurLogs(Box::new(self)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::protocol::Context; + + #[test] + fn test_our_logs_context() { + let json = r#"{ + "deduplicated_breadcrumbs": true, + "type": "sentry_logs" +}"#; + let context = Annotated::new(Context::OurLogs(Box::new(OurLogsContext { + deduplicated_breadcrumbs: Annotated::new(true), + ..OurLogsContext::default() + }))); + + assert_eq!(context, Annotated::from_json(json).unwrap()); + assert_eq!(json, context.to_json_pretty().unwrap()); + } +} diff --git a/relay-ourlogs/Cargo.toml b/relay-ourlogs/Cargo.toml index c132967b92..299a56bf9f 100644 --- a/relay-ourlogs/Cargo.toml +++ b/relay-ourlogs/Cargo.toml @@ -21,6 +21,7 @@ opentelemetry-proto = { workspace = true, features = [ "with-serde", "logs", ] } +relay-log = { workspace = true } relay-event-schema = { workspace = true } relay-protocol = { workspace = true } serde_json = { workspace = true } diff --git a/relay-ourlogs/src/lib.rs b/relay-ourlogs/src/lib.rs index 38130a4a4c..92f3047ca0 100644 --- a/relay-ourlogs/src/lib.rs +++ b/relay-ourlogs/src/lib.rs @@ -6,6 +6,7 @@ html_favicon_url = "https://raw.githubusercontent.com/getsentry/relay/master/artwork/relay-icon.png" )] +pub use crate::ourlog::breadcrumbs_to_ourlogs; pub use crate::ourlog::otel_to_sentry_log; pub use opentelemetry_proto::tonic::logs::v1::LogRecord as OtelLog; diff --git a/relay-ourlogs/src/ourlog.rs b/relay-ourlogs/src/ourlog.rs index 189f664965..3a88e223a7 100644 --- a/relay-ourlogs/src/ourlog.rs +++ b/relay-ourlogs/src/ourlog.rs @@ -1,8 +1,9 @@ -use opentelemetry_proto::tonic::common::v1::any_value::Value as OtelValue; - use crate::OtelLog; -use relay_event_schema::protocol::{AttributeValue, OurLog, SpanId, TraceId}; -use relay_protocol::{Annotated, Object}; +use opentelemetry_proto::tonic::common::v1::any_value::Value as OtelValue; +use relay_event_schema::protocol::{ + AttributeValue, Breadcrumb, Event, Level, OurLog, OurLogsContext, SpanId, TraceContext, TraceId, +}; +use relay_protocol::{Annotated, Object, Value}; /// Transform an OtelLog to a Sentry log. pub fn otel_to_sentry_log(otel_log: OtelLog) -> OurLog { @@ -70,9 +71,148 @@ pub fn otel_to_sentry_log(otel_log: OtelLog) -> OurLog { } } +/// Transform event breadcrumbs to OurLogs. +/// +/// Only converts up to `max_breadcrumbs` breadcrumbs. +pub fn breadcrumbs_to_ourlogs(event: &Event, max_breadcrumbs: usize) -> Option> { + let deduplicated_breadcrumbs = event + .context::() + .and_then(|ctx| ctx.deduplicated_breadcrumbs.value())?; + if !deduplicated_breadcrumbs { + // Only deduplicated breadcrumbs are supported. + return None; + } + + let event_trace_id = event + .context::() + .and_then(|trace_ctx| trace_ctx.trace_id.value()); + + let breadcrumbs = event.breadcrumbs.value()?; + let values = breadcrumbs.values.value()?; + + Some( + values + .iter() + .take(max_breadcrumbs) + .filter_map(|breadcrumb| { + let breadcrumb = breadcrumb.value()?; + + // Convert to nanoseconds + let timestamp_nanos = breadcrumb + .timestamp + .value()? + .into_inner() + .timestamp_nanos_opt() + .map(|t| t as u64)?; + + let mut attribute_data = Object::new(); + + if let Some(category) = breadcrumb.category.value() { + // Add category as sentry.category attribute if present, since the protocol doesn't have an equivalent field. + attribute_data.insert( + "sentry.category".to_string(), + Annotated::new(AttributeValue::StringValue(category.to_string())), + ); + } + + // Get span_id from data field if it exists and we have a trace_id from context, otherwise ignore it. + let span_id = if event_trace_id.is_some() { + breadcrumb + .data + .value() + .and_then(|data| data.get("__span").and_then(|span| span.value())) + .and_then(|span| match span { + Value::String(s) => Some(Annotated::new(SpanId(s.clone()))), + _ => None, + }) + .unwrap_or_else(Annotated::empty) + } else { + Annotated::empty() + }; + + // Convert breadcrumb data fields to primitive attributes + if let Some(data) = breadcrumb.data.value() { + for (key, value) in data.iter() { + if let Some(value) = value.value() { + let attribute = match value { + Value::String(s) => Some(AttributeValue::StringValue(s.clone())), + Value::Bool(b) => Some(AttributeValue::BoolValue(*b)), + Value::I64(i) => Some(AttributeValue::IntValue(*i)), + Value::F64(f) => Some(AttributeValue::DoubleValue(*f)), + _ => None, // Complex types will be supported once consumers are updated to ingest them. + }; + + if let Some(attr) = attribute { + attribute_data.insert(key.clone(), Annotated::new(attr)); + } + } + } + } + + let (body, level) = match breadcrumb.ty.value().map(|ty| ty.as_str()) { + Some("http") => { + let (body, level) = format_http_breadcrumb(breadcrumb)?; + (Annotated::new(body), Annotated::new(level)) + } + Some(_) | None => format_default_breadcrumb(breadcrumb)?, + }; + + Some(OurLog { + timestamp_nanos: Annotated::new(timestamp_nanos), + observed_timestamp_nanos: Annotated::new(timestamp_nanos), + trace_id: event_trace_id + .cloned() + .map(Annotated::new) + .unwrap_or_else(Annotated::empty), + span_id, + trace_flags: Annotated::new(0), + severity_text: level, + severity_number: Annotated::empty(), + body, + attributes: Annotated::new(attribute_data), + ..Default::default() + }) + }) + .collect(), + ) +} + +fn format_http_breadcrumb(breadcrumb: &Breadcrumb) -> Option<(String, String)> { + let data = breadcrumb.data.value().cloned().unwrap_or_default(); + let method = data.get("method").and_then(|v| v.value())?; + let url = data.get("url").and_then(|v| v.value())?; + let status_code = data + .get("status_code") + .and_then(|v| v.value()) + .and_then(|v| match v { + Value::I64(i) => Some(i), + _ => None, + })?; + + Some(( + format!("[{}] - {} {}", status_code, method.as_str()?, url.as_str()?), + Level::Info.to_string(), + )) +} + +fn format_default_breadcrumb( + breadcrumb: &Breadcrumb, +) -> Option<(Annotated, Annotated)> { + breadcrumb.message.value()?; // Log must have a message. + Some(( + breadcrumb.message.clone(), + breadcrumb + .level + .clone() + .map_value(|level| level.to_string()), + )) +} + #[cfg(test)] mod tests { use super::*; + use chrono::{TimeZone, Utc}; + use relay_event_schema::protocol::{Breadcrumb, Contexts, Level, Values}; use relay_protocol::{get_path, get_value}; #[test] @@ -201,4 +341,222 @@ mod tests { Some(&"SELECT \"table\".\"col\" FROM \"table\" WHERE \"table\".\"col\" = %s".into()) ); } + + #[test] + fn test_breadcrumbs_to_ourlogs() { + let json = r#"{ + "timestamp_nanos": 1577836800000000000, + "observed_timestamp_nanos": 1577836800000000000, + "trace_flags": 0, + "severity_text": "info", + "body": "test message", + "attributes": { + "bool_key": { + "bool_value": true + }, + "float_key": { + "double_value": 42.5 + }, + "int_key": { + "int_value": 42 + }, + "sentry.category": { + "string_value": "test category" + }, + "string_key": { + "string_value": "string value" + } + } +}"#; + + let timestamp = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap(); + let mut data = Object::new(); + data.insert( + "string_key".to_string(), + Annotated::new(Value::String("string value".to_string())), + ); + data.insert("bool_key".to_string(), Annotated::new(Value::Bool(true))); + data.insert("int_key".to_string(), Annotated::new(Value::I64(42))); + data.insert("float_key".to_string(), Annotated::new(Value::F64(42.5))); + + let breadcrumb = Breadcrumb { + message: Annotated::new("test message".to_string()), + category: Annotated::new("test category".to_string()), + timestamp: Annotated::new(timestamp.into()), + level: Annotated::new(Level::Info), + data: Annotated::new(data), + ..Default::default() + }; + + let mut contexts = Contexts::new(); + contexts.add(OurLogsContext { + deduplicated_breadcrumbs: Annotated::new(true), + other: Object::default(), + }); + + let event = Event { + breadcrumbs: Annotated::new(Values { + values: Annotated::new(vec![Annotated::new(breadcrumb)]), + other: Object::default(), + }), + contexts: Annotated::new(contexts), + ..Default::default() + }; + + let ourlogs = breadcrumbs_to_ourlogs(&event, 100).unwrap(); + assert_eq!(ourlogs.len(), 1); + + let annotated_log = Annotated::new(ourlogs[0].clone()); + assert_eq!(json, annotated_log.to_json_pretty().unwrap()); + } + + #[test] + fn test_breadcrumbs_limit() { + let mut breadcrumbs = Vec::new(); + for i in 0..5 { + let timestamp = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, i).unwrap(); + let breadcrumb = Breadcrumb { + message: Annotated::new(format!("message {}", i)), + timestamp: Annotated::new(timestamp.into()), + ..Default::default() + }; + breadcrumbs.push(Annotated::new(breadcrumb)); + } + + let mut contexts = Contexts::new(); + contexts.add(OurLogsContext { + deduplicated_breadcrumbs: Annotated::new(true), + other: Object::default(), + }); + + let event = Event { + breadcrumbs: Annotated::new(Values { + values: Annotated::new(breadcrumbs), + other: Object::default(), + }), + contexts: Annotated::new(contexts), + ..Default::default() + }; + + let ourlogs = breadcrumbs_to_ourlogs(&event, 3).unwrap(); + assert_eq!(ourlogs.len(), 3, "Limited to 3 breadcrumbs"); + assert_eq!(ourlogs[2].body.value().unwrap(), "message 2"); + + let ourlogs = breadcrumbs_to_ourlogs(&event, 10).unwrap(); + assert_eq!(ourlogs.len(), 5, "No limit"); + assert_eq!(ourlogs[4].body.value().unwrap(), "message 4"); + } + + #[test] + fn test_http_breadcrumb_conversion() { + let json = r#"{ + "timestamp_nanos": 1738209657000000000, + "observed_timestamp_nanos": 1738209657000000000, + "trace_flags": 0, + "severity_text": "info", + "body": "[200] - GET /api/0/organizations/sentry/issues/", + "attributes": { + "__span": { + "string_value": "bd61ce905c5f1bbd" + }, + "method": { + "string_value": "GET" + }, + "sentry.category": { + "string_value": "fetch" + }, + "status_code": { + "int_value": 200 + }, + "url": { + "string_value": "/api/0/organizations/sentry/issues/" + } + } +}"#; + + let timestamp = Utc.with_ymd_and_hms(2025, 1, 30, 4, 0, 57).unwrap(); + let mut data = Object::new(); + data.insert( + "__span".to_string(), + Annotated::new(Value::String("bd61ce905c5f1bbd".to_string())), + ); + data.insert( + "method".to_string(), + Annotated::new(Value::String("GET".to_string())), + ); + data.insert("status_code".to_string(), Annotated::new(Value::I64(200))); + data.insert( + "url".to_string(), + Annotated::new(Value::String( + "/api/0/organizations/sentry/issues/".to_string(), + )), + ); + + let breadcrumb = Breadcrumb { + ty: Annotated::new("http".to_string()), + category: Annotated::new("fetch".to_string()), + timestamp: Annotated::new(timestamp.into()), + data: Annotated::new(data), + ..Default::default() + }; + + let mut contexts = Contexts::new(); + contexts.add(OurLogsContext { + deduplicated_breadcrumbs: Annotated::new(true), + other: Object::default(), + }); + + let event = Event { + breadcrumbs: Annotated::new(Values { + values: Annotated::new(vec![Annotated::new(breadcrumb)]), + other: Object::default(), + }), + contexts: Annotated::new(contexts), + ..Default::default() + }; + + let ourlogs = breadcrumbs_to_ourlogs(&event, 100).unwrap(); + assert_eq!(ourlogs.len(), 1); + + let annotated_log = Annotated::new(ourlogs[0].clone()); + assert_eq!(json, annotated_log.to_json_pretty().unwrap()); + } + + #[test] + fn test_no_breadcrumbs_without_deduplicated_flag() { + let timestamp = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap(); + let breadcrumb = Breadcrumb { + message: Annotated::new("test message".to_string()), + timestamp: Annotated::new(timestamp.into()), + ..Default::default() + }; + + let mut contexts = Contexts::new(); + contexts.add(OurLogsContext { + deduplicated_breadcrumbs: Annotated::new(false), + other: Object::default(), + }); + + let event = Event { + breadcrumbs: Annotated::new(Values { + values: Annotated::new(vec![Annotated::new(breadcrumb.clone())]), + other: Object::default(), + }), + contexts: Annotated::new(contexts), + ..Default::default() + }; + + assert!(breadcrumbs_to_ourlogs(&event, 100).is_none()); + + // Check unset + let event = Event { + breadcrumbs: Annotated::new(Values { + values: Annotated::new(vec![Annotated::new(breadcrumb)]), + other: Object::default(), + }), + ..Default::default() + }; + + assert!(breadcrumbs_to_ourlogs(&event, 100).is_none()); + } } diff --git a/relay-server/src/envelope.rs b/relay-server/src/envelope.rs index 1561082be0..135cdf54fa 100644 --- a/relay-server/src/envelope.rs +++ b/relay-server/src/envelope.rs @@ -553,7 +553,7 @@ pub struct ItemHeaders { /// /// In order to only extract metrics once from an item while through a /// chain of Relays, a Relay that extracts metrics from an item (typically - /// the first Relay) MUST set this flat to true so that upstream Relays do + /// the first Relay) MUST set this flag to true so that upstream Relays do /// not extract the metric again causing double counting of the metric. #[serde(default, skip_serializing_if = "is_false")] metrics_extracted: bool, diff --git a/relay-server/src/services/processor.rs b/relay-server/src/services/processor.rs index 88ecbb4750..8f89ea4055 100644 --- a/relay-server/src/services/processor.rs +++ b/relay-server/src/services/processor.rs @@ -1613,7 +1613,17 @@ impl EnvelopeProcessorService { event::emit_feedback_metrics(managed_envelope.envelope()); } - attachment::scrub(managed_envelope, project_info); + attachment::scrub(managed_envelope, project_info.clone()); + + if_processing!(self.inner.config, { + if project_info.has_feature(Feature::OurLogsBreadcrumbExtraction) { + ourlog::extract_from_event( + managed_envelope, + &event, + &self.inner.global_config.current(), + ); + } + }); if self.inner.config.processing_enabled() && !event_fully_normalized.0 { relay_log::error!( @@ -1818,6 +1828,10 @@ impl EnvelopeProcessorService { ); } + if project_info.has_feature(Feature::OurLogsBreadcrumbExtraction) { + ourlog::extract_from_event(managed_envelope, &event, &global_config); + } + event = self.enforce_quotas( managed_envelope, event, @@ -1902,7 +1916,7 @@ impl EnvelopeProcessorService { }); report::process_user_reports(managed_envelope); - attachment::scrub(managed_envelope, project_info); + attachment::scrub(managed_envelope, project_info.clone()); Ok(Some(extracted_metrics)) } diff --git a/relay-server/src/services/processor/ourlog.rs b/relay-server/src/services/processor/ourlog.rs index d4a6ad8bbf..147e3b6f6c 100644 --- a/relay-server/src/services/processor/ourlog.rs +++ b/relay-server/src/services/processor/ourlog.rs @@ -14,11 +14,12 @@ use { crate::envelope::ContentType, crate::envelope::{Item, ItemType}, crate::services::outcome::{DiscardReason, Outcome}, - crate::services::processor::ProcessingError, - relay_dynamic_config::ProjectConfig, + crate::services::processor::{EventProcessing, ProcessingError}, + crate::utils::{self}, + relay_dynamic_config::{GlobalConfig, ProjectConfig}, relay_event_schema::processor::{process_value, ProcessingState}, - relay_event_schema::protocol::OurLog, - relay_ourlogs::OtelLog, + relay_event_schema::protocol::{Event, OurLog}, + relay_ourlogs::{breadcrumbs_to_ourlogs, OtelLog}, relay_pii::PiiProcessor, relay_protocol::Annotated, }; @@ -103,3 +104,43 @@ fn scrub( Ok(()) } + +/// Extract breadcrumbs from an event and convert them to logs. +#[cfg(feature = "processing")] +pub fn extract_from_event( + managed_envelope: &mut TypedEnvelope, + event: &Annotated, + global_config: &GlobalConfig, +) { + let Some(event) = event.value() else { + return; + }; + + let convert_breadcrumbs_to_logs = utils::sample( + global_config + .options + .ourlogs_breadcrumb_extraction_sample_rate + .unwrap_or(0.0), + ); + + if convert_breadcrumbs_to_logs { + relay_log::trace!("extracting breadcrumbs to logs"); + let ourlogs = breadcrumbs_to_ourlogs( + event, + global_config + .options + .ourlogs_breadcrumb_extraction_max_breadcrumbs_converted, + ); + + if let Some(ourlogs) = ourlogs { + for ourlog in ourlogs { + let mut log_item = Item::new(ItemType::Log); + if let Ok(payload) = Annotated::new(ourlog).to_json() { + log_item.set_payload(ContentType::Json, payload); + relay_log::trace!("Adding log to envelope"); + managed_envelope.envelope_mut().add_item(log_item); + } + } + } + } +} diff --git a/tests/integration/test_ourlogs.py b/tests/integration/test_ourlogs.py index e373898f64..7f21cb9843 100644 --- a/tests/integration/test_ourlogs.py +++ b/tests/integration/test_ourlogs.py @@ -1,7 +1,9 @@ import json from datetime import datetime, timedelta, timezone +import pytest from sentry_sdk.envelope import Envelope, Item, PayloadRef +from .test_store import make_transaction TEST_CONFIG = { @@ -121,3 +123,56 @@ def test_ourlog_extraction_is_disabled_without_feature( assert len(ourlogs) == 0 ourlogs_consumer.assert_empty() + + +@pytest.mark.parametrize( + "sample_rate,expected_ourlogs", + [ + (None, 0), + (1.0, 1), + (0.0, 0), + ], +) +def test_ourlog_breadcrumb_extraction_sample_rate( + mini_sentry, + relay_with_processing, + ourlogs_consumer, + sample_rate, + expected_ourlogs, +): + ourlogs_consumer = ourlogs_consumer() + project_id = 42 + + project_config = mini_sentry.add_full_project_config(project_id) + project_config["config"]["features"] = [ + "projects:ourlogs-breadcrumb-extraction", + "organizations:ourlogs-ingestion", + ] + + mini_sentry.global_config["options"] = { + "relay.ourlogs-breadcrumb-extraction.sample-rate": sample_rate, + "relay.ourlogs-breadcrumb-extraction.max-breadcrumbs-converted": 100, + } + + def send_transaction_with_breadcrumb(upstream): + transaction = make_transaction({"event_id": "cbf6960622e14a45abc1f03b2055b186"}) + transaction["breadcrumbs"] = [ + { + "timestamp": datetime.now(timezone.utc).isoformat(), + "message": "Test breadcrumb", + "category": "test", + "level": "info", + } + ] + transaction["contexts"]["sentry_logs"] = { + "deduplicated_breadcrumbs": True, + "type": "sentry_logs", + } + envelope = Envelope() + envelope.add_transaction(transaction) + upstream.send_envelope(project_id, envelope) + + relay = relay_with_processing(options=TEST_CONFIG) + send_transaction_with_breadcrumb(relay) + assert len(ourlogs_consumer.get_ourlogs()) == expected_ourlogs + ourlogs_consumer.assert_empty()