Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions sentry_streams/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion sentry_streams/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ edition = "2021"
[dependencies]
pyo3 = { version = "0.29.0" }
serde = { version = "1.0", features = ["derive"] }
sentry_arroyo = { version = ">=2.40.0, <2.44.0", features = ["ssl"] }
sentry_arroyo = { version = "2.47.0", features = ["ssl"] }
chrono = "0.4.40"
tracing = "0.1.40"
tracing-subscriber = "0.3.20"
Expand Down
2 changes: 1 addition & 1 deletion sentry_streams/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ classifiers = [

dependencies = [
"requests>=2.32.3",
"sentry-arroyo>=2.39.2,<2.44.0",
"sentry-arroyo>=2.47.0",
"pyyaml>=6.0.2",
"jsonschema>=4.23.0",
"sentry-kafka-schemas>=2.2.0",
Expand Down
3 changes: 2 additions & 1 deletion sentry_streams/src/consumer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,8 @@ pub fn build_dlq_policy(

// Create Kafka producer for DLQ
let producer_config = dlq_config.producer_config.clone().into();
let kafka_producer = KafkaProducer::new(producer_config);
let kafka_producer =
KafkaProducer::new(producer_config).expect("Failed to create DLQ Kafka producer");
let dlq_producer = KafkaDlqProducer::new(kafka_producer, Topic::new(&dlq_config.topic));

// Use default DLQ limits (no limits) and no max buffered messages
Expand Down
145 changes: 103 additions & 42 deletions sentry_streams/src/metrics.rs
Original file line number Diff line number Diff line change
@@ -1,43 +1,73 @@
use crate::metrics_config::PyMetricConfig;
use metrics::Label;
use metrics::{
Counter, Gauge, Histogram, Key, KeyName, Label, Metadata, Recorder, SharedString, Unit,
};
use metrics_exporter_dogstatsd::DogStatsDBuilder;
use sentry_arroyo::metrics::{init as arroyo_init, Metric, MetricType, Recorder};
use std::time::Duration;
use tracing::{error, info, warn};

struct MetricsFacadeRecorder;

impl Recorder for MetricsFacadeRecorder {
fn record_metric(&self, metric: Metric<'_>) {
let key = format!("streams.pipeline.{}", metric.key);
let value_f64 = match metric.value {
sentry_arroyo::metrics::MetricValue::I64(v) => v as f64,
sentry_arroyo::metrics::MetricValue::U64(v) => v as f64,
sentry_arroyo::metrics::MetricValue::F64(v) => v,
sentry_arroyo::metrics::MetricValue::Duration(d) => d.as_millis() as f64,
_ => return,
};
use tracing::{error, info};

let labels: Vec<(String, String)> = metric
.tags
.iter()
.map(|(key, value_dyn)| (key.to_string(), format!("{}", value_dyn)))
.collect();
const ARROYO_METRIC_PREFIX: &str = "arroyo.";
const STREAMS_METRIC_PREFIX: &str = "streams.pipeline.";

match metric.ty {
MetricType::Counter => {
let counter = metrics::counter!(key, &labels);
counter.increment(value_f64 as u64);
}
MetricType::Gauge => {
let gauge = metrics::gauge!(key, &labels);
gauge.set(value_f64);
}
MetricType::Timer => {
let histogram = metrics::histogram!(key, &labels);
histogram.record(value_f64);
}
_ => {}
/// Arroyo emits its own metrics (`arroyo.*`) through the global metrics-rs
/// recorder. This wrapper namespaces them under `streams.pipeline.` so they
/// keep the same names as before and match the Python side, while leaving
/// streams' own metrics (already `streams.pipeline.*`) untouched.
struct ArroyoPrefixRecorder<R> {
inner: R,
}

fn prefixed_name(name: &str) -> Option<String> {
name.starts_with(ARROYO_METRIC_PREFIX)
.then(|| format!("{STREAMS_METRIC_PREFIX}{name}"))
Comment on lines +20 to +22

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should arroyo metrics be prefixed with streams.pipeline.? We could add a global application metric label for arroyo metrics using configure_scope: https://github.com/getsentry/relay/blob/46f3659920eefc75b61e03fc5723a603d98b9dc9/relay/src/setup.rs#L110-L112

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to do that, but it's a naming decision, so I'd like a call before changing it. Tradeoffs:

  • Keep the prefix (current PR): Rust arroyo metrics keep the names they had before the bump (streams.pipeline.arroyo.*; the old MetricsFacadeRecorder added the same prefix). They also match the Python side, where DatadogMetricsBackend uses the streams.pipeline namespace for arroyo metrics too. No dashboard or monitor changes.
  • Drop the prefix and use configure_scope: this is cleaner, and arroyo metrics would match other arroyo users like relay. But the Rust series get renamed to arroyo.*, which breaks existing dashboards and monitors. Rust and Python would also diverge unless we change the Python namespace in the same PR. Pipeline tags already reach arroyo metrics through DogStatsD with_global_labels, so configure_scope would only be needed for an extra label like application.

Should I switch to the unprefixed names plus configure_scope, or keep this PR name-preserving and do the rename as a follow-up?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as part of this PR i think we should just retain compat, if you want to unprefix then i would do it in a separate pr

i'm mainly trying to bump arroyo now across the board so that we're unblocked across the various workstreams

}

fn prefix_key_name(key: KeyName) -> KeyName {
match prefixed_name(key.as_str()) {
Some(name) => KeyName::from(name),
None => key,
}
}

fn prefix_key(key: &Key) -> Option<Key> {
prefixed_name(key.name())
.map(|name| Key::from_parts(name, key.labels().cloned().collect::<Vec<_>>()))
}

impl<R: Recorder> Recorder for ArroyoPrefixRecorder<R> {
fn describe_counter(&self, key: KeyName, unit: Option<Unit>, description: SharedString) {
self.inner
.describe_counter(prefix_key_name(key), unit, description)
}

fn describe_gauge(&self, key: KeyName, unit: Option<Unit>, description: SharedString) {
self.inner
.describe_gauge(prefix_key_name(key), unit, description)
}

fn describe_histogram(&self, key: KeyName, unit: Option<Unit>, description: SharedString) {
self.inner
.describe_histogram(prefix_key_name(key), unit, description)
}

fn register_counter(&self, key: &Key, metadata: &Metadata<'_>) -> Counter {
match prefix_key(key) {
Some(key) => self.inner.register_counter(&key, metadata),
None => self.inner.register_counter(key, metadata),
}
}

fn register_gauge(&self, key: &Key, metadata: &Metadata<'_>) -> Gauge {
match prefix_key(key) {
Some(key) => self.inner.register_gauge(&key, metadata),
None => self.inner.register_gauge(key, metadata),
}
}

fn register_histogram(&self, key: &Key, metadata: &Metadata<'_>) -> Histogram {
match prefix_key(key) {
Some(key) => self.inner.register_histogram(&key, metadata),
None => self.inner.register_histogram(key, metadata),
Comment thread
sentry-junior[bot] marked this conversation as resolved.
}
}
}
Expand Down Expand Up @@ -77,21 +107,52 @@ pub fn configure_metrics(metric_config: Option<PyMetricConfig>) {
builder = builder.with_flush_interval(duration);
}

match builder.install() {
let recorder = match builder.build() {
Ok(recorder) => recorder,
Err(e) => {
error!("Failed to build DogStatsD recorder: {}", e);
return;
}
};

match metrics::set_global_recorder(ArroyoPrefixRecorder { inner: recorder }) {
Ok(_) => {
info!("Successfully initialized DogStatsD metrics exporter");
}
Err(e) => {
error!("Failed to install DogStatsD recorder: {}", e);
return;
}
}
}
}

if arroyo_init(MetricsFacadeRecorder).is_err() {
warn!("Arroyo metrics recorder already initialized, skipping");
return;
}
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_prefixed_name() {
assert_eq!(
prefixed_name("arroyo.consumer.run.count").as_deref(),
Some("streams.pipeline.arroyo.consumer.run.count")
);
assert_eq!(prefixed_name("streams.pipeline.input.messages"), None);
}

info!("Successfully initialized arroyo metrics");
#[test]
fn test_prefix_key_keeps_labels() {
let key = Key::from_parts(
"arroyo.producer.produce_status",
vec![Label::new("status", "success")],
);
let prefixed = prefix_key(&key).unwrap();
assert_eq!(
prefixed.name(),
"streams.pipeline.arroyo.producer.produce_status"
);
assert_eq!(
prefixed.labels().collect::<Vec<_>>(),
key.labels().collect::<Vec<_>>()
);
}
}
3 changes: 2 additions & 1 deletion sentry_streams/src/operators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,8 @@ pub fn build(
topic_name,
kafka_config,
} => {
let producer = KafkaProducer::new(kafka_config.clone().into());
let producer = KafkaProducer::new(kafka_config.clone().into())
.expect("Failed to create StreamSink Kafka producer");
Box::new(StreamSink::new(
route.clone(),
producer,
Expand Down
10 changes: 5 additions & 5 deletions sentry_streams/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading