objectstore_service/backend/
counting.rs1use std::num::NonZeroU64;
18
19use objectstore_types::metadata::Metadata;
20use objectstore_types::range::ByteRange;
21use objectstore_types::resumable::UploadProgress;
22use objectstore_types::time::Timestamp;
23use objectstore_types::usecase::Usecase;
24
25use crate::backend::common::{
26 Backend, DeleteResponse, ExpiryUpdate, GetResponse, MetadataResponse, PutResponse,
27 SetExpiryResponse,
28};
29use crate::error::Result;
30use crate::id::ObjectId;
31use crate::resumable::{BackendToken, Session};
32use crate::stream::ClientStream;
33
34fn count(usecase: &Usecase) {
39 objectstore_metrics::count!("cogs.usage" += 1, app_feature = usecase.to_string());
40}
41
42#[derive(Debug)]
46pub struct CountingBackend {
47 inner: Box<dyn Backend>,
48}
49
50impl CountingBackend {
51 pub fn new(inner: Box<dyn Backend>) -> Self {
54 Self { inner }
55 }
56}
57
58#[async_trait::async_trait]
59impl Backend for CountingBackend {
60 fn name(&self) -> &'static str {
61 self.inner.name()
62 }
63
64 fn upload_granularity(&self) -> u64 {
65 self.inner.upload_granularity()
66 }
67
68 async fn put_object(
69 &self,
70 id: &ObjectId,
71 metadata: &Metadata,
72 stream: ClientStream,
73 access_time: Timestamp,
74 ) -> Result<PutResponse> {
75 count(id.usecase());
76 self.inner
77 .put_object(id, metadata, stream, access_time)
78 .await
79 }
80
81 async fn get_object(
82 &self,
83 id: &ObjectId,
84 access_time: Timestamp,
85 range: Option<ByteRange>,
86 ) -> Result<GetResponse> {
87 count(id.usecase());
88 self.inner.get_object(id, access_time, range).await
89 }
90
91 async fn get_metadata(
92 &self,
93 id: &ObjectId,
94 access_time: Timestamp,
95 ) -> Result<MetadataResponse> {
96 count(id.usecase());
97 self.inner.get_metadata(id, access_time).await
98 }
99
100 async fn set_expiry(
101 &self,
102 id: &ObjectId,
103 target: ExpiryUpdate,
104 access_time: Timestamp,
105 ) -> Result<SetExpiryResponse> {
106 count(id.usecase());
107 self.inner.set_expiry(id, target, access_time).await
108 }
109
110 async fn delete_object(&self, id: &ObjectId, access_time: Timestamp) -> Result<DeleteResponse> {
111 count(id.usecase());
112 self.inner.delete_object(id, access_time).await
113 }
114
115 async fn join(&self) {
116 self.inner.join().await;
117 }
118
119 async fn create_upload_session(
120 &self,
121 id: &ObjectId,
122 metadata: &Metadata,
123 upload_length: NonZeroU64,
124 ) -> Result<Option<BackendToken>> {
125 count(id.usecase());
126 self.inner
127 .create_upload_session(id, metadata, upload_length)
128 .await
129 }
130
131 async fn put_chunk(
132 &self,
133 session: &Session,
134 offset: u64,
135 content_length: u64,
136 stream: ClientStream,
137 ) -> Result<UploadProgress> {
138 count(session.object_id.usecase());
139 self.inner
140 .put_chunk(session, offset, content_length, stream)
141 .await
142 }
143
144 async fn upload_offset(&self, session: &Session) -> Result<UploadProgress> {
145 count(session.object_id.usecase());
146 self.inner.upload_offset(session).await
147 }
148
149 async fn cancel_upload(&self, session: &Session) -> Result<()> {
150 count(session.object_id.usecase());
151 self.inner.cancel_upload(session).await
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use objectstore_types::scope::{Scope, Scopes};
158
159 use super::*;
160 use crate::backend::in_memory::InMemoryBackend;
161 use crate::id::ObjectContext;
162 use crate::stream;
163
164 fn object_id(usecase: &str) -> ObjectId {
165 ObjectId::new(
166 ObjectContext {
167 usecase: usecase.parse().unwrap(),
168 scopes: Scopes::from_iter([Scope::create("org", "1").unwrap()]),
169 },
170 "key".into(),
171 )
172 }
173
174 fn capture(f: impl std::future::Future<Output = ()>) -> Vec<String> {
179 objectstore_metrics::with_capturing_test_client(|| {
180 tokio::runtime::Builder::new_current_thread()
181 .enable_all()
182 .build()
183 .unwrap()
184 .block_on(f);
185 })
186 }
187
188 #[test]
189 fn counts_each_core_operation_once() {
190 let captured = capture(async {
191 let backend = CountingBackend::new(Box::new(InMemoryBackend::new("in-memory")));
192 let id = object_id("attachments");
193
194 backend
195 .put_object(
196 &id,
197 &Metadata::default(),
198 stream::single("hi"),
199 Timestamp::now(),
200 )
201 .await
202 .unwrap();
203 backend
204 .get_object(&id, Timestamp::now(), None)
205 .await
206 .unwrap();
207 backend.get_metadata(&id, Timestamp::now()).await.unwrap();
208 backend.delete_object(&id, Timestamp::now()).await.unwrap();
209 });
210
211 let cogs = captured
212 .iter()
213 .filter(|m| m.starts_with("cogs.usage"))
214 .count();
215 assert_eq!(
216 cogs, 4,
217 "expected one count per operation, captured: {captured:?}"
218 );
219 assert!(
220 captured
221 .iter()
222 .all(|m| !m.starts_with("cogs.usage")
223 || m == "cogs.usage:+1|c|#app_feature:attachments"),
224 "captured: {captured:?}"
225 );
226 }
227
228 #[test]
229 fn counts_missing_reads_on_dispatch() {
230 let captured = capture(async {
231 let backend = CountingBackend::new(Box::new(InMemoryBackend::new("in-memory")));
232 let result = backend
234 .get_object(&object_id("attachments"), Timestamp::now(), None)
235 .await
236 .unwrap();
237 assert!(result.is_none());
238 });
239
240 assert_eq!(
241 captured
242 .iter()
243 .filter(|m| m.starts_with("cogs.usage"))
244 .count(),
245 1,
246 "captured: {captured:?}"
247 );
248 }
249
250 #[test]
251 fn new_usecase_is_app_feature() {
252 let captured = capture(async {
253 let backend = CountingBackend::new(Box::new(InMemoryBackend::new("in-memory")));
254 backend
255 .get_object(&object_id("new_usecase"), Timestamp::now(), None)
256 .await
257 .unwrap();
258 });
259
260 assert!(
261 captured
262 .iter()
263 .any(|m| m == "cogs.usage:+1|c|#app_feature:new_usecase"),
264 "captured: {captured:?}"
265 );
266 }
267}