Skip to main content

relay_cogs/
lib.rs

1//! Break down the cost of Relay by its components and individual features it handles.
2//!
3//! Relay is a one stop shop for all different kinds of events Sentry supports, Errors,
4//! Performance, Metrics, Replays, Crons and more. A single shared resource.
5//!
6//! This module intends to make it possible to give insights how much time and resources
7//! Relay spends processing individual features. The measurements collected can be later used
8//! for increased observability and accounting purposes.
9//!
10//! `relay-cogs` provides a way to give an answer to the questions:
11//!  - What portion of Relay's costs can be attributed to feature X?
12//!  - How much does feature X cost?
13//!
14//! ## Collecting COGs Measurements
15//!
16//! Measurements are collected through [`Cogs`] which attributes the measurement to either a single
17//! or to multiple different [app features](AppFeature) belonging to a [resource](ResourceId).
18//!
19//! Collected and [attributed measurements](CogsMeasurement) then are recorded by a [`CogsRecorder`].
20//!
21//! ```
22//! use relay_cogs::{AppFeature, Cogs, FeatureWeights, ResourceId};
23//!
24//! enum Message {
25//!     Span,
26//!     Transaction,
27//!     TransactionWithSpans { num_spans: usize },
28//! }
29//!
30//! struct Processor {
31//!     cogs: Cogs
32//! }
33//!
34//! impl From<&Message> for FeatureWeights {
35//!     fn from(value: &Message) -> Self {
36//!         match value {
37//!             Message::Span => FeatureWeights::new(AppFeature::Spans),
38//!             Message::Transaction => FeatureWeights::new(AppFeature::Transactions),
39//!             Message::TransactionWithSpans { num_spans } => FeatureWeights::builder()
40//!                 .weight(AppFeature::Spans, *num_spans)
41//!                 .weight(AppFeature::Transactions, 1)
42//!                 .build(),
43//!         }
44//!     }
45//! }
46//!
47//! impl Processor {
48//!     fn handle_message(&self, mut message: Message) {
49//!         let _cogs = self.cogs.timed(ResourceId::Relay, &message);
50//!
51//!         self.step1(&mut message);
52//!         self.step2(&mut message);
53//!
54//!         // Measurement automatically recorded here.
55//!     }
56//! #   fn step1(&self, _: &mut Message) {}
57//! #   fn step2(&self, _: &mut Message) {}
58//! }
59//! ```
60#![warn(missing_docs)]
61#![doc(
62    html_logo_url = "https://raw.githubusercontent.com/getsentry/relay/master/artwork/relay-icon.png",
63    html_favicon_url = "https://raw.githubusercontent.com/getsentry/relay/master/artwork/relay-icon.png"
64)]
65
66mod cogs;
67mod measurement;
68mod recorder;
69#[cfg(test)]
70mod test;
71
72pub mod time;
73
74use std::fmt;
75
76pub use self::cogs::*;
77pub use self::recorder::*;
78#[cfg(test)]
79pub use self::test::*;
80
81pub(crate) use self::measurement::*;
82
83/// Records a categorized measurement of the passed `body`, in `category` on `token`.
84///
85/// # Example:
86///
87/// ```
88/// # use relay_cogs::{AppFeature, Cogs, ResourceId};
89/// # struct Item;
90/// # fn do_something(_: &Item) -> bool { true };
91/// # fn do_something_else(_: &Item) -> bool { true };
92///
93/// fn process(cogs: &Cogs, item: &Item) {
94///     let mut token = cogs.timed(ResourceId::Relay, AppFeature::Transactions);
95///
96///     // The entire body is categorized as `processing`.
97///     relay_cogs::with!(token, "processing", {
98///         let success = do_something(&item);
99///     });
100///
101///     // Not categorized.
102///     if success {
103///         do_something_else(&item);
104///     }
105/// }
106/// ```
107#[macro_export]
108macro_rules! with {
109    ($token:expr, $category:expr, { $($body:tt)* }) => {
110        let token = $token.start_category($category);
111        $($body)*
112        drop(token);
113    };
114}
115
116/// Resource ID as tracked in COGS.
117///
118/// Infrastructure costs are labeled with a resource id,
119/// these costs need to be broken down further by the application
120/// by [app features](AppFeature).
121#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
122pub enum ResourceId {
123    /// The Relay resource.
124    ///
125    /// This includes all computational costs required for running Relay.
126    Relay,
127}
128
129/// App feature a COGS measurement is related to.
130///
131/// App features break down the cost of a [`ResourceId`], the
132/// app features do no need to directly match a Sentry product.
133/// Multiple app features are later grouped and aggregated to determine
134/// the cost of a product.
135#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
136pub enum AppFeature {
137    /// A placeholder which should not be emitted but can be emitted in rare cases,
138    /// for example error scenarios.
139    ///
140    /// It can be useful to start a COGS measurement before it is known
141    /// what the measurement should be attributed to.
142    /// For example when parsing data, the measurement should be started
143    /// before parsing, but only after parsing it is known what to attribute
144    /// the measurement to.
145    Unattributed,
146
147    /// Metrics are attributed by their namespace, whenever this is not possible
148    /// or feasible, this app feature is emitted instead.
149    UnattributedMetrics,
150    /// When processing an envelope cannot be attributed or is not feasible to be attributed
151    /// to a more specific category, this app feature is emitted instead.
152    UnattributedEnvelope,
153    /// All COGS data collected from HTTP request handlers which can't be attributed to
154    /// a specific product feature.
155    UnattributedRequest,
156
157    /// Transactions.
158    Transactions,
159    /// Errors.
160    Errors,
161    /// Logs.
162    Logs,
163    /// Trace metrics.
164    TraceMetrics,
165    /// Spans.
166    Spans,
167    /// Sessions.
168    Sessions,
169    /// Client reports.
170    ClientReports,
171    /// Crons check ins.
172    CheckIns,
173    /// Replays.
174    Replays,
175    /// Profiles.
176    ///
177    /// This app feature is for continuous profiling.
178    Profiles,
179    /// User Reports
180    UserReports,
181    /// Event attachments not associated with an event.
182    StandaloneAttachments,
183
184    /// Outcomes.
185    Outcomes,
186
187    /// Metrics in the spans namespace.
188    MetricsSpans,
189    /// Metrics in the transactions namespace.
190    MetricsTransactions,
191    /// Metrics in the sessions namespace.
192    MetricsSessions,
193    /// Metrics in the unsupported namespace.
194    ///
195    /// This is usually not emitted, since metrics in the unsupported
196    /// namespace should be dropped before any processing occurs.
197    MetricsUnsupported,
198
199    /// V2 attachments that cannot be attributed to spans or logs.
200    TraceAttachments,
201}
202
203impl AppFeature {
204    /// Returns the string representation for this app feature.
205    pub fn as_str(&self) -> &'static str {
206        match self {
207            Self::Unattributed => "unattributed",
208            Self::UnattributedMetrics => "unattributed_metrics",
209            Self::UnattributedEnvelope => "unattributed_envelope",
210            Self::UnattributedRequest => "unattributed_request",
211            Self::Transactions => "transactions",
212            Self::Errors => "errors",
213            Self::Spans => "spans",
214            Self::Logs => "our_logs",
215            Self::Sessions => "sessions",
216            Self::ClientReports => "client_reports",
217            Self::CheckIns => "check_ins",
218            Self::Replays => "replays",
219            Self::UserReports => "user_reports",
220            Self::StandaloneAttachments => "standalone_attachments",
221            Self::Outcomes => "outcomes",
222            Self::MetricsSpans => "metrics_spans",
223            Self::MetricsTransactions => "metrics_transactions",
224            Self::MetricsSessions => "metrics_sessions",
225            Self::MetricsUnsupported => "metrics_unsupported",
226            Self::Profiles => "profiles",
227            Self::TraceMetrics => "trace_metrics",
228            Self::TraceAttachments => "trace_attachments",
229        }
230    }
231}
232
233/// A COGS measurement.
234///
235/// The measurement has already been attributed to a specific feature.
236#[derive(Debug, Clone, Copy)]
237pub struct CogsMeasurement {
238    /// The measured resource.
239    pub resource: ResourceId,
240    /// The measured app feature.
241    pub feature: AppFeature,
242    /// Optional category for this measurement.
243    ///
244    /// A category further subdivides a measurement for a specific feature.
245    pub category: Option<&'static str>,
246    /// The measurement value.
247    pub value: Value,
248}
249
250impl fmt::Display for CogsMeasurement {
251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252        write!(f, "{:?}@{}", self.resource, self.feature.as_str())?;
253        if let Some(category) = self.category {
254            write!(f, "[{category}]")?;
255        }
256        write!(f, "={}", self.value)
257    }
258}
259
260/// A COGS measurement value.
261#[derive(Debug, Clone, Copy, PartialEq, Eq)]
262pub enum Value {
263    /// A time measurement.
264    Time(std::time::Duration),
265}
266
267impl fmt::Display for Value {
268    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
269        match self {
270            Self::Time(duration) => write!(f, "{duration:?}"),
271        }
272    }
273}