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
21 changes: 9 additions & 12 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
members = [
"foundations",
"foundations-macros",
"foundations-sentry",
"examples",
"tools/gen-syscall-enum",
]
Expand All @@ -15,10 +16,10 @@ authors = ["Cloudflare"]
license = "BSD-3-Clause"

[workspace.metadata.release]
pre-release-commit-message = "Release {{version}}"
shared-version = true
tag-prefix = ""
push = false
publish = false
pre-release-commit-message = "chore: Release"
tag-message = "Release {{crate_name}} version {{version}}"

[profile.release]
debug = 1
Expand All @@ -38,7 +39,7 @@ check-cfg = [
[workspace.dependencies]
anyhow = "1.0.102"
backtrace = "0.3.76"
foundations = { version = "5.6.5", path = "./foundations" }
foundations = { version = "5.6.5", path = "./foundations", default-features = false }
foundations-macros = { version = "=5.6.5", path = "./foundations-macros", default-features = false }
bindgen = { version = "0.72.1", default-features = false }
cc = "1.2.61"
Expand Down Expand Up @@ -95,14 +96,6 @@ tikv-jemalloc-ctl = "0.6.1"
tower-service = "0.3.3"
tracing-slog = "0.4.0"
tracing-subscriber = "0.3.23"
sentry-core = { version = "0.36.0", default-features = false }
sentry = { version = "0.36.0", default-features = false, features = [
"backtrace",
"contexts",
"panic",
"ureq",
"rustls",
] }
zeroize = "1.8.2"

# needed for minver
Expand All @@ -121,3 +114,7 @@ neli-proc-macros = "0.2.2"
parking_lot_core = "0.9.12"
thiserror = "2.0.18"
tower = "0.5.3"

# Only used in deprecated `foundations` code
# TODO: remove before next major release
sentry-core = { version = "0.36.0", default-features = false }
2 changes: 1 addition & 1 deletion cliff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ commit_parsers = [
{ message = "^Pull request", skip = true }
]
filter_commits = false
tag_pattern = "[v0-9]*"
tag_pattern = "v[0-9]*"
sort_commits = "newest"
5 changes: 4 additions & 1 deletion examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ edition = { workspace = true }
repository = { workspace = true }
publish = false

[package.metadata.release]
release = false

[dev-dependencies]
anyhow = { workspace = true }
foundations = { workspace = true }
foundations = { workspace = true, features = ["default"] }
futures-util = { workspace = true }
http-body-util = { workspace = true }
hyper = { workspace = true }
Expand Down
5 changes: 5 additions & 0 deletions foundations-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ authors = { workspace = true }
license = { workspace = true }
readme = "../README.md"

[package.metadata.release]
shared-version = "foundations"
tag-prefix = ""
tag-message = "Release foundations version {{version}}"

[features]
default = ["settings_deny_unknown_fields_by_default"]
settings_deny_unknown_fields_by_default = []
Expand Down
38 changes: 38 additions & 0 deletions foundations-sentry/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[package]
name = "foundations-sentry"
description = "Foundations companion crate with Sentry integrations"
version = "1.0.0"
edition = { workspace = true }
repository = { workspace = true }
authors = { workspace = true }
license = { workspace = true }
readme = "../README.md"
categories = ["development-tools"]
keywords = ["foundations", "sentry", "telemetry", "metrics"]

# Generic for any separately-versioned crate at repo root
[package.metadata.release]
pre-release-hook = [
"git-cliff",
"-w",
"..",
"--include-path",
"{{crate_name}}/**",
"--tag-pattern",
"{{crate_name}}-v[0-9]*",
"-o",
"RELEASE_NOTES.md",
"--tag",
"{{version}}",
]

[lints]
workspace = true

[dependencies]
foundations = { workspace = true, features = ["settings", "ratelimit", "metrics"] }
governor = { workspace = true }
sentry-core = "0.47.0"

[dev-dependencies]
sentry-core = { version = "0.47.0", features = ["test"] }
6 changes: 6 additions & 0 deletions foundations-sentry/RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

1.0.0
- 2026-04-27 Configure cargo-release for separately-versioned crates
- 2026-04-27 Extract sentry hook into separate foundations-sentry crate


106 changes: 106 additions & 0 deletions foundations-sentry/src/hook.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//! Sentry hook implementation for tracking sentry events and rate-limiting them.

use crate::SentrySettings;
use foundations::ratelimit::StaticQuantaClock;
use governor::{Quota, RateLimiter};
use std::borrow::Cow;
use std::num::NonZeroU32;
use std::sync::Arc;

type Fingerprint = Cow<'static, str>;

/// Clean up keys in the sentry rate limiter once every 20 minutes
/// (3 times per hour), with no burst allowed.
const SENTRY_LIMITER_CLEANUP_QUOTA: Quota =
Quota::per_hour(NonZeroU32::new(3).unwrap()).allow_burst(NonZeroU32::new(1).unwrap());

/// Install the sentry hook on the provided client options.
///
/// This installs a `before_send` hook that increments `sentry_events_total`
/// and performs rate limiting, if configured. If a previous `before_send`
/// hook exists, it will be called after rate limiting has been applied.
/// Only unfiltered events are counted.
///
/// See the [`foundations-sentry`](crate) crate-level docs for more information.
pub fn install_hook_with_settings(
options: &mut sentry_core::ClientOptions,
settings: &SentrySettings,
) {
let rate_limiter = settings.max_events_per_second.map(|rl| {
RateLimiter::dashmap_with_clock(Quota::per_second(rl), StaticQuantaClock::default())
});

let previous = options.before_send.take();

options.before_send = Some(Arc::new(move |mut event| {
if let Some(limiter) = &rate_limiter {
foundations::ratelimit!(SENTRY_LIMITER_CLEANUP_QUOTA; limiter.retain_recent());

let fp = extract_fingerprint(&event);
if limiter.check_key(&fp).is_err() {
return None;
}
}

if let Some(prev) = &previous {
event = prev(event)?;
}

super::metrics::sentry::events_total(event.level).inc();

Some(event)
}));
}

/// Derive a fingerprint for a sentry event to perform rate limiting.
///
/// We check for the following event attributes, in order:
/// 1. Explicit fingerprint (if set and not defaulted)
/// 2. Event message
/// 3. First exception value/type
/// 4. Fallback: event level name
fn extract_fingerprint(event: &sentry_core::protocol::Event<'static>) -> Fingerprint {
use sentry_core::Level;

// Try the explicitly-specified fingerprint first, but only if its not defaulted
let explicit_fp = &event.fingerprint;
if !explicit_fp.is_empty() && !is_sentry_default_fingerprint(explicit_fp) {
if let [fp] = explicit_fp.as_ref() {
// Just clone if the explicit fingerprint is a single element
return fp.clone();
}
return explicit_fp.join("::").into();
}

// Try the event message, if there is one
if let Some(msg) = &event.message {
return msg.clone().into();
}

// Try the first attached exception, if there is one
if let Some(exc) = event.exception.first() {
if let Some(val) = &exc.value {
return val.clone().into();
}
if !exc.ty.is_empty() {
return exc.ty.clone().into();
}
}

// Finally, fall back to the event level
Cow::Borrowed(match event.level {
Level::Debug => "level::debug",
Level::Info => "level::info",
Level::Warning => "level::warning",
Level::Error => "level::error",
Level::Fatal => "level::fatal",
})
}

// Adapted from https://github.com/getsentry/sentry-rust/blob/0.47.0/sentry-types/src/protocol/v7.rs#L1619
fn is_sentry_default_fingerprint(fp: &[Cow<'_, str>]) -> bool {
if let [fp] = fp {
return matches!(fp.as_ref(), "{{ default }}" | "{{default}}");
}
false
}
43 changes: 43 additions & 0 deletions foundations-sentry/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#![allow(clippy::needless_doctest_main, reason = "for illustration")]
//! [`foundations`] companion crate with Sentry integrations. Includes rate limiting
//! of sentry events and tracking them with [`foundations`] metrics.
//!
//! This crate provides a sentry hook that increments the
//! `sentry_events_total{level=<...>}` metric for each sentry event. If a
//! previous `before_send` hook exists, it will be executed after rate limiting
//! and before the metric is incremented. Only unfiltered events are counted.
//!
//! For rate-limiting, we group events by fingerprint. Each group has a separate
//! rate limiter. The fingerprint of a sentry event is the first out of the following
//! attributes that is present and not defaulted:
//!
//! 1. Explicit `event.fingerprint`
//! 2. Event message
//! 3. First exception value, or exception type if no value is set
//! 4. Fallback: event level name (e.g., `error`)
//!
//! **note**: a clone of a client's [`sentry_core::ClientOptions`] will have the
//! hook installed. This means "child" sentry clients will inherit the hook. A
//! reinstall is only required if the [`sentry_core::ClientOptions::before_send`]
//! field is overwritten.
//!
//! # Usage
//!
//! To install the hook:
//!
//! ```rust
//! fn main() {
//! let mut client_opts = sentry_core::ClientOptions::default();
//! let sentry_settings = foundations_sentry::SentrySettings::default();
//! foundations_sentry::install_hook_with_settings(&mut client_opts, &sentry_settings);
//! // sentry::init(client_opts);
//! }
//! ```

pub mod metrics;

mod hook;
mod settings;

pub use self::hook::install_hook_with_settings;
pub use self::settings::SentrySettings;
11 changes: 11 additions & 0 deletions foundations-sentry/src/metrics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//! Sentry event related metrics.

use foundations::telemetry::metrics::{Counter, metrics};
use sentry_core::Level;

/// Sentry metrics.
#[metrics(unprefixed)]
pub mod sentry {
/// Total number of sentry events observed.
pub fn events_total(level: Level) -> Counter;
}
12 changes: 12 additions & 0 deletions foundations-sentry/src/settings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use foundations::settings::settings;
use std::num::NonZeroU32;

/// Sentry hook settings.
#[settings]
pub struct SentrySettings {
/// Maximum number of events that can be emitted per second, per fingerprint.
pub max_events_per_second: Option<NonZeroU32>,
// In the future, we may offer different fingerprinting modes here.
// For example, we could offer a "dummy" mode that assigns the same fingerprint
// to all events so they all share a single rate limit.
}
Loading
Loading