Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm

jetstream metrics

Changed files
+111 -13
.github
workflows
jetstream
ufos
+3 -1
.github/workflows/checks.yml
···
- uses: actions/checkout@v4
- name: Build lib
run: cargo build --verbose
+
- name: Check (default features)
+
run: cargo check
- name: Run tests
-
run: cargo test --verbose
+
run: cargo test --all-features --verbose
style:
runs-on: ubuntu-24.04
+1
Cargo.lock
···
"clap",
"futures-util",
"log",
+
"metrics",
"serde",
"serde_json",
"thiserror 2.0.12",
+1 -1
Makefile
···
all: check
test:
-
cargo test
+
cargo test --all-features
fmt:
cargo fmt --package links --package constellation --package ufos
+5
jetstream/Cargo.toml
···
"url",
] }
futures-util = "0.3.31"
+
metrics = { version = "0.24.2", optional = true }
url = "2.5.4"
serde = { version = "1.0.215", features = ["derive"] }
serde_json = { version = "1.0.140", features = ["raw_value"] }
···
[dev-dependencies]
anyhow = "1.0.93"
clap = { version = "4.5.20", features = ["derive"] }
+
+
[features]
+
default = []
+
metrics = ["dep:metrics"]
+1 -6
jetstream/src/error.rs
···
/// See [websocket_task](crate::websocket_task).
#[derive(Error, Debug)]
pub enum JetstreamEventError {
-
#[error("received websocket message that could not be deserialized as JSON: {0}")]
-
ReceivedMalformedJSON(#[from] serde_json::Error),
#[error("failed to load built-in zstd dictionary for decoding: {0}")]
CompressionDictionaryError(io::Error),
-
#[error("failed to decode zstd-compressed message: {0}")]
-
CompressionDecoderError(io::Error),
-
#[error("all receivers were dropped but the websocket connection failed to close cleanly")]
-
WebSocketCloseFailure,
+
#[error("failed to send ping or pong: {0}")]
PingPongError(#[from] tokio_tungstenite::tungstenite::Error),
#[error("jetstream event receiver closed")]
+99 -4
jetstream/src/lib.rs
···
stream::StreamExt,
SinkExt,
};
+
#[cfg(feature = "metrics")]
+
use metrics::{
+
counter,
+
describe_counter,
+
Unit,
+
};
use tokio::{
net::TcpStream,
sync::mpsc::{
···
}
}
+
#[cfg(feature = "metrics")]
+
fn describe_metrics() {
+
describe_counter!(
+
"jetstream_connects",
+
Unit::Count,
+
"how many times we've tried to connect"
+
);
+
describe_counter!(
+
"jetstream_disconnects",
+
Unit::Count,
+
"how many times we've been disconnected"
+
);
+
describe_counter!(
+
"jetstream_total_events_received",
+
Unit::Count,
+
"total number of events received"
+
);
+
describe_counter!(
+
"jetstream_total_bytes_received",
+
Unit::Count,
+
"total uncompressed bytes received, not including websocket overhead"
+
);
+
describe_counter!(
+
"jetstream_total_event_errors",
+
Unit::Count,
+
"total errors when handling events"
+
);
+
describe_counter!(
+
"jetstream_total_events_sent",
+
Unit::Count,
+
"total events sent to the consumer"
+
);
+
}
+
impl JetstreamConnector {
/// Create a Jetstream connector with a valid [JetstreamConfig].
///
/// After creation, you can call [connect] to connect to the provided Jetstream instance.
pub fn new(config: JetstreamConfig) -> Result<Self, ConfigValidationError> {
+
#[cfg(feature = "metrics")]
+
describe_metrics();
+
// We validate the configuration here so any issues are caught early.
config.validate()?;
Ok(JetstreamConnector { config })
···
}
};
+
#[cfg(feature = "metrics")]
+
if let Some(host) = req.uri().host() {
+
let retry = if retry_attempt > 0 { "yes" } else { "no" };
+
counter!("jetstream_connects", "host" => host.to_string(), "retry" => retry)
+
.increment(1);
+
}
+
let mut last_cursor = connect_cursor;
retry_attempt += 1;
if let Ok((ws_stream, _)) = connect_async(req).await {
···
websocket_task(dict, ws_stream, send_channel.clone(), &mut last_cursor)
.await
{
-
if let JetstreamEventError::ReceiverClosedError = e {
-
log::error!("Jetstream receiver channel closed. Exiting consumer.");
-
return;
+
match e {
+
JetstreamEventError::ReceiverClosedError => {
+
#[cfg(feature="metrics")]
+
counter!("jetstream_disconnects", "reason" => "channel", "fatal" => "yes").increment(1);
+
log::error!("Jetstream receiver channel closed. Exiting consumer.");
+
return;
+
}
+
JetstreamEventError::CompressionDictionaryError(_) => {
+
#[cfg(feature="metrics")]
+
counter!("jetstream_disconnects", "reason" => "zstd", "fatal" => "no").increment(1);
+
}
+
JetstreamEventError::PingPongError(_) => {
+
#[cfg(feature="metrics")]
+
counter!("jetstream_disconnects", "reason" => "pingpong", "fatal" => "no").increment(1);
+
}
}
-
log::error!("Jetstream closed after encountering error: {e:?}");
+
log::warn!("Jetstream closed after encountering error: {e:?}");
} else {
+
#[cfg(feature = "metrics")]
+
counter!("jetstream_disconnects", "reason" => "close", "fatal" => "no")
+
.increment(1);
log::warn!("Jetstream connection closed cleanly");
}
if t_connected.elapsed() > Duration::from_secs(success_threshold_s) {
···
match socket_read.next().await {
Some(Ok(message)) => match message {
Message::Text(json) => {
+
#[cfg(feature = "metrics")]
+
{
+
counter!("jetstream_total_events_received", "compressed" => "false")
+
.increment(1);
+
counter!("jetstream_total_bytes_received", "compressed" => "false")
+
.increment(json.len() as u64);
+
}
let event: JetstreamEvent = match serde_json::from_str(&json) {
Ok(ev) => ev,
Err(e) => {
+
#[cfg(feature = "metrics")]
+
counter!("jetstream_total_event_errors", "reason" => "deserialize")
+
.increment(1);
log::warn!(
"failed to parse json: {e:?} (from {})",
json.get(..24).unwrap_or(&json)
···
if let Some(last) = last_cursor {
if event_cursor <= *last {
+
#[cfg(feature = "metrics")]
+
counter!("jetstream_total_event_errors", "reason" => "old")
+
.increment(1);
log::warn!("event cursor {event_cursor:?} was not newer than the last one: {last:?}. dropping event.");
continue;
}
···
} else if let Some(last) = last_cursor.as_mut() {
*last = event_cursor;
}
+
#[cfg(feature = "metrics")]
+
counter!("jetstream_total_events_sent").increment(1);
}
Message::Binary(zstd_json) => {
+
#[cfg(feature = "metrics")]
+
{
+
counter!("jetstream_total_events_received", "compressed" => "true")
+
.increment(1);
+
counter!("jetstream_total_bytes_received", "compressed" => "true")
+
.increment(zstd_json.len() as u64);
+
}
let mut cursor = IoCursor::new(zstd_json);
let decoder =
zstd::stream::Decoder::with_prepared_dictionary(&mut cursor, &dictionary)
···
let event: JetstreamEvent = match serde_json::from_reader(decoder) {
Ok(ev) => ev,
Err(e) => {
+
#[cfg(feature = "metrics")]
+
counter!("jetstream_total_event_errors", "reason" => "deserialize")
+
.increment(1);
log::warn!("failed to parse json: {e:?}");
continue;
}
···
if let Some(last) = last_cursor {
if event_cursor <= *last {
+
#[cfg(feature = "metrics")]
+
counter!("jetstream_total_event_errors", "reason" => "old")
+
.increment(1);
log::warn!("event cursor {event_cursor:?} was not newer than the last one: {last:?}. dropping event.");
continue;
}
···
} else if let Some(last) = last_cursor.as_mut() {
*last = event_cursor;
}
+
#[cfg(feature = "metrics")]
+
counter!("jetstream_total_events_sent").increment(1);
}
Message::Ping(vec) => {
log::trace!("Ping recieved, responding");
+1 -1
ufos/Cargo.toml
···
fjall = { version = "2.8.0", features = ["lz4"] }
getrandom = "0.3.3"
http = "1.3.1"
-
jetstream = { path = "../jetstream" }
+
jetstream = { path = "../jetstream", features = ["metrics"] }
log = "0.4.26"
lsm-tree = "2.6.6"
metrics = "0.24.2"