···22all: check
3344test:
55- cargo test
55+ cargo test --all-features
6677fmt:
88 cargo fmt --package links --package constellation --package ufos
+5
jetstream/Cargo.toml
···2020 "url",
2121] }
2222futures-util = "0.3.31"
2323+metrics = { version = "0.24.2", optional = true }
2324url = "2.5.4"
2425serde = { version = "1.0.215", features = ["derive"] }
2526serde_json = { version = "1.0.140", features = ["raw_value"] }
···3132[dev-dependencies]
3233anyhow = "1.0.93"
3334clap = { version = "4.5.20", features = ["derive"] }
3535+3636+[features]
3737+default = []
3838+metrics = ["dep:metrics"]
+1-6
jetstream/src/error.rs
···3636/// See [websocket_task](crate::websocket_task).
3737#[derive(Error, Debug)]
3838pub enum JetstreamEventError {
3939- #[error("received websocket message that could not be deserialized as JSON: {0}")]
4040- ReceivedMalformedJSON(#[from] serde_json::Error),
4139 #[error("failed to load built-in zstd dictionary for decoding: {0}")]
4240 CompressionDictionaryError(io::Error),
4343- #[error("failed to decode zstd-compressed message: {0}")]
4444- CompressionDecoderError(io::Error),
4545- #[error("all receivers were dropped but the websocket connection failed to close cleanly")]
4646- WebSocketCloseFailure,
4141+4742 #[error("failed to send ping or pong: {0}")]
4843 PingPongError(#[from] tokio_tungstenite::tungstenite::Error),
4944 #[error("jetstream event receiver closed")]
+99-4
jetstream/src/lib.rs
···1414 stream::StreamExt,
1515 SinkExt,
1616};
1717+#[cfg(feature = "metrics")]
1818+use metrics::{
1919+ counter,
2020+ describe_counter,
2121+ Unit,
2222+};
1723use tokio::{
1824 net::TcpStream,
1925 sync::mpsc::{
···299305 }
300306}
301307308308+#[cfg(feature = "metrics")]
309309+fn describe_metrics() {
310310+ describe_counter!(
311311+ "jetstream_connects",
312312+ Unit::Count,
313313+ "how many times we've tried to connect"
314314+ );
315315+ describe_counter!(
316316+ "jetstream_disconnects",
317317+ Unit::Count,
318318+ "how many times we've been disconnected"
319319+ );
320320+ describe_counter!(
321321+ "jetstream_total_events_received",
322322+ Unit::Count,
323323+ "total number of events received"
324324+ );
325325+ describe_counter!(
326326+ "jetstream_total_bytes_received",
327327+ Unit::Count,
328328+ "total uncompressed bytes received, not including websocket overhead"
329329+ );
330330+ describe_counter!(
331331+ "jetstream_total_event_errors",
332332+ Unit::Count,
333333+ "total errors when handling events"
334334+ );
335335+ describe_counter!(
336336+ "jetstream_total_events_sent",
337337+ Unit::Count,
338338+ "total events sent to the consumer"
339339+ );
340340+}
341341+302342impl JetstreamConnector {
303343 /// Create a Jetstream connector with a valid [JetstreamConfig].
304344 ///
305345 /// After creation, you can call [connect] to connect to the provided Jetstream instance.
306346 pub fn new(config: JetstreamConfig) -> Result<Self, ConfigValidationError> {
347347+ #[cfg(feature = "metrics")]
348348+ describe_metrics();
349349+307350 // We validate the configuration here so any issues are caught early.
308351 config.validate()?;
309352 Ok(JetstreamConnector { config })
···359402 }
360403 };
361404405405+ #[cfg(feature = "metrics")]
406406+ if let Some(host) = req.uri().host() {
407407+ let retry = if retry_attempt > 0 { "yes" } else { "no" };
408408+ counter!("jetstream_connects", "host" => host.to_string(), "retry" => retry)
409409+ .increment(1);
410410+ }
411411+362412 let mut last_cursor = connect_cursor;
363413 retry_attempt += 1;
364414 if let Ok((ws_stream, _)) = connect_async(req).await {
···368418 websocket_task(dict, ws_stream, send_channel.clone(), &mut last_cursor)
369419 .await
370420 {
371371- if let JetstreamEventError::ReceiverClosedError = e {
372372- log::error!("Jetstream receiver channel closed. Exiting consumer.");
373373- return;
421421+ match e {
422422+ JetstreamEventError::ReceiverClosedError => {
423423+ #[cfg(feature="metrics")]
424424+ counter!("jetstream_disconnects", "reason" => "channel", "fatal" => "yes").increment(1);
425425+ log::error!("Jetstream receiver channel closed. Exiting consumer.");
426426+ return;
427427+ }
428428+ JetstreamEventError::CompressionDictionaryError(_) => {
429429+ #[cfg(feature="metrics")]
430430+ counter!("jetstream_disconnects", "reason" => "zstd", "fatal" => "no").increment(1);
431431+ }
432432+ JetstreamEventError::PingPongError(_) => {
433433+ #[cfg(feature="metrics")]
434434+ counter!("jetstream_disconnects", "reason" => "pingpong", "fatal" => "no").increment(1);
435435+ }
374436 }
375375- log::error!("Jetstream closed after encountering error: {e:?}");
437437+ log::warn!("Jetstream closed after encountering error: {e:?}");
376438 } else {
439439+ #[cfg(feature = "metrics")]
440440+ counter!("jetstream_disconnects", "reason" => "close", "fatal" => "no")
441441+ .increment(1);
377442 log::warn!("Jetstream connection closed cleanly");
378443 }
379444 if t_connected.elapsed() > Duration::from_secs(success_threshold_s) {
···425490 match socket_read.next().await {
426491 Some(Ok(message)) => match message {
427492 Message::Text(json) => {
493493+ #[cfg(feature = "metrics")]
494494+ {
495495+ counter!("jetstream_total_events_received", "compressed" => "false")
496496+ .increment(1);
497497+ counter!("jetstream_total_bytes_received", "compressed" => "false")
498498+ .increment(json.len() as u64);
499499+ }
428500 let event: JetstreamEvent = match serde_json::from_str(&json) {
429501 Ok(ev) => ev,
430502 Err(e) => {
503503+ #[cfg(feature = "metrics")]
504504+ counter!("jetstream_total_event_errors", "reason" => "deserialize")
505505+ .increment(1);
431506 log::warn!(
432507 "failed to parse json: {e:?} (from {})",
433508 json.get(..24).unwrap_or(&json)
···439514440515 if let Some(last) = last_cursor {
441516 if event_cursor <= *last {
517517+ #[cfg(feature = "metrics")]
518518+ counter!("jetstream_total_event_errors", "reason" => "old")
519519+ .increment(1);
442520 log::warn!("event cursor {event_cursor:?} was not newer than the last one: {last:?}. dropping event.");
443521 continue;
444522 }
···453531 } else if let Some(last) = last_cursor.as_mut() {
454532 *last = event_cursor;
455533 }
534534+ #[cfg(feature = "metrics")]
535535+ counter!("jetstream_total_events_sent").increment(1);
456536 }
457537 Message::Binary(zstd_json) => {
538538+ #[cfg(feature = "metrics")]
539539+ {
540540+ counter!("jetstream_total_events_received", "compressed" => "true")
541541+ .increment(1);
542542+ counter!("jetstream_total_bytes_received", "compressed" => "true")
543543+ .increment(zstd_json.len() as u64);
544544+ }
458545 let mut cursor = IoCursor::new(zstd_json);
459546 let decoder =
460547 zstd::stream::Decoder::with_prepared_dictionary(&mut cursor, &dictionary)
···463550 let event: JetstreamEvent = match serde_json::from_reader(decoder) {
464551 Ok(ev) => ev,
465552 Err(e) => {
553553+ #[cfg(feature = "metrics")]
554554+ counter!("jetstream_total_event_errors", "reason" => "deserialize")
555555+ .increment(1);
466556 log::warn!("failed to parse json: {e:?}");
467557 continue;
468558 }
···471561472562 if let Some(last) = last_cursor {
473563 if event_cursor <= *last {
564564+ #[cfg(feature = "metrics")]
565565+ counter!("jetstream_total_event_errors", "reason" => "old")
566566+ .increment(1);
474567 log::warn!("event cursor {event_cursor:?} was not newer than the last one: {last:?}. dropping event.");
475568 continue;
476569 }
···485578 } else if let Some(last) = last_cursor.as_mut() {
486579 *last = event_cursor;
487580 }
581581+ #[cfg(feature = "metrics")]
582582+ counter!("jetstream_total_events_sent").increment(1);
488583 }
489584 Message::Ping(vec) => {
490585 log::trace!("Ping recieved, responding");