1use std::{
19 cmp, fmt,
20 ops::AddAssign,
21 str::FromStr,
22 sync::{
23 Arc,
24 atomic::{AtomicU8, AtomicU64, Ordering},
25 },
26 time::Duration,
27};
28
29use arrow::array::{ArrayRef, RecordBatch, StringArray};
30use arrow::util::pretty::pretty_format_batches;
31use async_trait::async_trait;
32use chrono::Utc;
33use datafusion::{
34 common::{HashMap, instant::Instant},
35 error::DataFusionError,
36 execution::object_store::{DefaultObjectStoreRegistry, ObjectStoreRegistry},
37};
38use futures::stream::{BoxStream, Stream};
39use futures::{StreamExt, TryStreamExt};
40use object_store::{
41 CopyOptions, GetOptions, GetRange, GetResult, ListResult, MultipartUpload,
42 ObjectMeta, ObjectStore, ObjectStoreExt, PutMultipartOptions, PutOptions, PutPayload,
43 PutResult, Result, path::Path,
44};
45use parking_lot::{Mutex, RwLock};
46use url::Url;
47
48struct TimeToFirstItemStream<S> {
54 inner: S,
55 start: Option<Instant>,
56 request_duration: Arc<AtomicU64>,
57 duration_recorded: bool,
58}
59
60impl<S> TimeToFirstItemStream<S> {
61 fn new(inner: S, request_duration: Arc<AtomicU64>) -> Self {
62 Self {
63 inner,
64 start: None,
65 request_duration,
66 duration_recorded: false,
67 }
68 }
69}
70
71impl<S> Stream for TimeToFirstItemStream<S>
72where
73 S: Stream<Item = Result<ObjectMeta>> + Unpin,
74{
75 type Item = Result<ObjectMeta>;
76
77 fn poll_next(
78 mut self: std::pin::Pin<&mut Self>,
79 cx: &mut std::task::Context<'_>,
80 ) -> std::task::Poll<Option<Self::Item>> {
81 let start = *self.start.get_or_insert_with(Instant::now);
82
83 let poll_result = std::pin::Pin::new(&mut self.inner).poll_next(cx);
84
85 if !self.duration_recorded && poll_result.is_ready() {
86 self.duration_recorded = true;
87 let nanos = start.elapsed().as_nanos() as u64;
88 self.request_duration.store(nanos, Ordering::Release);
89 }
90
91 poll_result
92 }
93}
94
95#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
98pub enum InstrumentedObjectStoreMode {
99 #[default]
101 Disabled,
102 Summary,
104 Trace,
106}
107
108impl fmt::Display for InstrumentedObjectStoreMode {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110 write!(f, "{self:?}")
111 }
112}
113
114impl FromStr for InstrumentedObjectStoreMode {
115 type Err = DataFusionError;
116
117 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
118 match s.to_lowercase().as_str() {
119 "disabled" => Ok(Self::Disabled),
120 "summary" => Ok(Self::Summary),
121 "trace" => Ok(Self::Trace),
122 _ => Err(DataFusionError::Execution(format!("Unrecognized mode {s}"))),
123 }
124 }
125}
126
127impl From<u8> for InstrumentedObjectStoreMode {
128 fn from(value: u8) -> Self {
129 match value {
130 1 => InstrumentedObjectStoreMode::Summary,
131 2 => InstrumentedObjectStoreMode::Trace,
132 _ => InstrumentedObjectStoreMode::Disabled,
133 }
134 }
135}
136
137#[derive(Debug)]
140pub struct InstrumentedObjectStore {
141 inner: Arc<dyn ObjectStore>,
142 instrument_mode: AtomicU8,
143 requests: Arc<Mutex<Vec<RequestDetails>>>,
144}
145
146impl InstrumentedObjectStore {
147 fn new(object_store: Arc<dyn ObjectStore>, instrument_mode: AtomicU8) -> Self {
149 Self {
150 inner: object_store,
151 instrument_mode,
152 requests: Arc::new(Mutex::new(Vec::new())),
153 }
154 }
155
156 fn set_instrument_mode(&self, mode: InstrumentedObjectStoreMode) {
157 self.instrument_mode.store(mode as u8, Ordering::Relaxed)
158 }
159
160 pub fn take_requests(&self) -> Vec<RequestDetails> {
163 let mut req = self.requests.lock();
164
165 req.drain(..).collect()
166 }
167
168 fn enabled(&self) -> bool {
169 self.instrument_mode.load(Ordering::Relaxed)
170 != InstrumentedObjectStoreMode::Disabled as u8
171 }
172
173 async fn instrumented_put_opts(
174 &self,
175 location: &Path,
176 payload: PutPayload,
177 opts: PutOptions,
178 ) -> Result<PutResult> {
179 let timestamp = Utc::now();
180 let start = Instant::now();
181 let size = payload.content_length();
182 let ret = self.inner.put_opts(location, payload, opts).await?;
183 let elapsed = start.elapsed();
184
185 self.requests.lock().push(RequestDetails {
186 op: Operation::Put,
187 path: location.clone(),
188 timestamp,
189 duration_nanos: Arc::new(AtomicU64::new(elapsed.as_nanos() as u64)),
190 size: Some(size),
191 range: None,
192 extra_display: None,
193 });
194
195 Ok(ret)
196 }
197
198 async fn instrumented_put_multipart(
199 &self,
200 location: &Path,
201 opts: PutMultipartOptions,
202 ) -> Result<Box<dyn MultipartUpload>> {
203 let timestamp = Utc::now();
204 let start = Instant::now();
205 let ret = self.inner.put_multipart_opts(location, opts).await?;
206 let elapsed = start.elapsed();
207
208 self.requests.lock().push(RequestDetails {
209 op: Operation::Put,
210 path: location.clone(),
211 timestamp,
212 duration_nanos: Arc::new(AtomicU64::new(elapsed.as_nanos() as u64)),
213 size: None,
214 range: None,
215 extra_display: None,
216 });
217
218 Ok(ret)
219 }
220
221 async fn instrumented_get_opts(
222 &self,
223 location: &Path,
224 options: GetOptions,
225 ) -> Result<GetResult> {
226 let timestamp = Utc::now();
227 let range = options.range.clone();
228
229 let head = options.head;
230 let start = Instant::now();
231 let ret = self.inner.get_opts(location, options).await?;
232 let elapsed = start.elapsed();
233
234 let (op, size) = if head {
235 (Operation::Head, None)
236 } else {
237 (
238 Operation::Get,
239 Some((ret.range.end - ret.range.start) as usize),
240 )
241 };
242
243 self.requests.lock().push(RequestDetails {
244 op,
245 path: location.clone(),
246 timestamp,
247 duration_nanos: Arc::new(AtomicU64::new(elapsed.as_nanos() as u64)),
248 size,
249 range,
250 extra_display: None,
251 });
252
253 Ok(ret)
254 }
255
256 fn instrumented_delete_stream(
257 &self,
258 locations: BoxStream<'static, Result<Path>>,
259 ) -> BoxStream<'static, Result<Path>> {
260 let requests_captured = Arc::clone(&self.requests);
261
262 let timestamp = Utc::now();
263 let start = Instant::now();
264 self.inner
265 .delete_stream(locations)
266 .and_then(move |location| {
267 let elapsed = start.elapsed();
268 requests_captured.lock().push(RequestDetails {
269 op: Operation::Delete,
270 path: location.clone(),
271 timestamp,
272 duration_nanos: Arc::new(AtomicU64::new(elapsed.as_nanos() as u64)),
273 size: None,
274 range: None,
275 extra_display: None,
276 });
277 futures::future::ok(location)
278 })
279 .boxed()
280 }
281
282 fn instrumented_list(
283 &self,
284 prefix: Option<&Path>,
285 ) -> BoxStream<'static, Result<ObjectMeta>> {
286 let timestamp = Utc::now();
287 let inner_stream = self.inner.list(prefix);
288
289 let duration_nanos = Arc::new(AtomicU64::new(0));
290 self.requests.lock().push(RequestDetails {
291 op: Operation::List,
292 path: prefix.cloned().unwrap_or_else(|| Path::from("")),
293 timestamp,
294 duration_nanos: Arc::clone(&duration_nanos),
295 size: None,
296 range: None,
297 extra_display: None,
298 });
299
300 Box::pin(TimeToFirstItemStream::new(inner_stream, duration_nanos))
301 }
302
303 async fn instrumented_list_with_delimiter(
304 &self,
305 prefix: Option<&Path>,
306 ) -> Result<ListResult> {
307 let timestamp = Utc::now();
308 let start = Instant::now();
309 let ret = self.inner.list_with_delimiter(prefix).await?;
310 let elapsed = start.elapsed();
311
312 self.requests.lock().push(RequestDetails {
313 op: Operation::List,
314 path: prefix.cloned().unwrap_or_else(|| Path::from("")),
315 timestamp,
316 duration_nanos: Arc::new(AtomicU64::new(elapsed.as_nanos() as u64)),
317 size: None,
318 range: None,
319 extra_display: None,
320 });
321
322 Ok(ret)
323 }
324
325 async fn instrumented_copy(&self, from: &Path, to: &Path) -> Result<()> {
326 let timestamp = Utc::now();
327 let start = Instant::now();
328 self.inner.copy(from, to).await?;
329 let elapsed = start.elapsed();
330
331 self.requests.lock().push(RequestDetails {
332 op: Operation::Copy,
333 path: from.clone(),
334 timestamp,
335 duration_nanos: Arc::new(AtomicU64::new(elapsed.as_nanos() as u64)),
336 size: None,
337 range: None,
338 extra_display: Some(format!("copy_to: {to}")),
339 });
340
341 Ok(())
342 }
343
344 async fn instrumented_copy_if_not_exists(
345 &self,
346 from: &Path,
347 to: &Path,
348 ) -> Result<()> {
349 let timestamp = Utc::now();
350 let start = Instant::now();
351 self.inner.copy_if_not_exists(from, to).await?;
352 let elapsed = start.elapsed();
353
354 self.requests.lock().push(RequestDetails {
355 op: Operation::Copy,
356 path: from.clone(),
357 timestamp,
358 duration_nanos: Arc::new(AtomicU64::new(elapsed.as_nanos() as u64)),
359 size: None,
360 range: None,
361 extra_display: Some(format!("copy_to: {to}")),
362 });
363
364 Ok(())
365 }
366}
367
368impl fmt::Display for InstrumentedObjectStore {
369 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
370 let mode: InstrumentedObjectStoreMode =
371 self.instrument_mode.load(Ordering::Relaxed).into();
372 write!(
373 f,
374 "Instrumented Object Store: instrument_mode: {mode}, inner: {}",
375 self.inner
376 )
377 }
378}
379
380#[async_trait]
381impl ObjectStore for InstrumentedObjectStore {
382 async fn put_opts(
383 &self,
384 location: &Path,
385 payload: PutPayload,
386 opts: PutOptions,
387 ) -> Result<PutResult> {
388 if self.enabled() {
389 return self.instrumented_put_opts(location, payload, opts).await;
390 }
391
392 self.inner.put_opts(location, payload, opts).await
393 }
394
395 async fn put_multipart_opts(
396 &self,
397 location: &Path,
398 opts: PutMultipartOptions,
399 ) -> Result<Box<dyn MultipartUpload>> {
400 if self.enabled() {
401 return self.instrumented_put_multipart(location, opts).await;
402 }
403
404 self.inner.put_multipart_opts(location, opts).await
405 }
406
407 async fn get_opts(&self, location: &Path, options: GetOptions) -> Result<GetResult> {
408 if self.enabled() {
409 return self.instrumented_get_opts(location, options).await;
410 }
411
412 self.inner.get_opts(location, options).await
413 }
414
415 fn delete_stream(
416 &self,
417 locations: BoxStream<'static, Result<Path>>,
418 ) -> BoxStream<'static, Result<Path>> {
419 if self.enabled() {
420 return self.instrumented_delete_stream(locations);
421 }
422
423 self.inner.delete_stream(locations)
424 }
425
426 fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, Result<ObjectMeta>> {
427 if self.enabled() {
428 return self.instrumented_list(prefix);
429 }
430
431 self.inner.list(prefix)
432 }
433
434 async fn list_with_delimiter(&self, prefix: Option<&Path>) -> Result<ListResult> {
435 if self.enabled() {
436 return self.instrumented_list_with_delimiter(prefix).await;
437 }
438
439 self.inner.list_with_delimiter(prefix).await
440 }
441
442 async fn copy_opts(
443 &self,
444 from: &Path,
445 to: &Path,
446 options: CopyOptions,
447 ) -> Result<()> {
448 if self.enabled() {
449 return match options.mode {
450 object_store::CopyMode::Create => {
451 self.instrumented_copy_if_not_exists(from, to).await
452 }
453 object_store::CopyMode::Overwrite => {
454 self.instrumented_copy(from, to).await
455 }
456 };
457 }
458
459 self.inner.copy_opts(from, to, options).await
460 }
461}
462
463#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
465pub enum Operation {
466 Copy,
467 Delete,
468 Get,
469 Head,
470 List,
471 Put,
472}
473
474impl fmt::Display for Operation {
475 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
476 write!(f, "{self:?}")
477 }
478}
479
480pub struct RequestDetails {
482 op: Operation,
483 path: Path,
484 timestamp: chrono::DateTime<Utc>,
485 duration_nanos: Arc<AtomicU64>,
487 size: Option<usize>,
488 range: Option<GetRange>,
489 extra_display: Option<String>,
490}
491
492impl fmt::Debug for RequestDetails {
493 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
494 f.debug_struct("RequestDetails")
495 .field("op", &self.op)
496 .field("path", &self.path)
497 .field("timestamp", &self.timestamp)
498 .field("duration", &self.duration())
499 .field("size", &self.size)
500 .field("range", &self.range)
501 .field("extra_display", &self.extra_display)
502 .finish()
503 }
504}
505
506impl RequestDetails {
507 fn duration(&self) -> Option<Duration> {
508 let nanos = self.duration_nanos.load(Ordering::Acquire);
509 if nanos == 0 {
510 None
511 } else {
512 Some(Duration::from_nanos(nanos))
513 }
514 }
515}
516
517impl fmt::Display for RequestDetails {
518 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
519 let mut output_parts = vec![format!(
520 "{} operation={:?}",
521 self.timestamp.to_rfc3339(),
522 self.op
523 )];
524
525 if let Some(d) = self.duration() {
526 output_parts.push(format!("duration={:.6}s", d.as_secs_f32()));
527 }
528 if let Some(s) = self.size {
529 output_parts.push(format!("size={s}"));
530 }
531 if let Some(r) = &self.range {
532 output_parts.push(format!("range: {r}"));
533 }
534 output_parts.push(format!("path={}", self.path));
535
536 if let Some(ed) = &self.extra_display {
537 output_parts.push(ed.clone());
538 }
539
540 write!(f, "{}", output_parts.join(" "))
541 }
542}
543
544#[derive(Default)]
546pub struct RequestSummaries {
547 summaries: Vec<RequestSummary>,
548}
549
550impl fmt::Display for RequestSummaries {
552 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
553 match pretty_format_batches(&[self.to_batch()]) {
555 Err(e) => {
556 write!(f, "Error formatting summary: {e}")
557 }
558 Ok(displayable) => {
559 write!(f, "{displayable}")
560 }
561 }
562 }
563}
564
565impl RequestSummaries {
566 pub fn new(requests: &[RequestDetails]) -> Self {
568 let mut summaries: HashMap<Operation, RequestSummary> = HashMap::new();
569 for rd in requests {
570 match summaries.get_mut(&rd.op) {
571 Some(rs) => rs.push(rd),
572 None => {
573 let mut rs = RequestSummary::new(rd.op);
574 rs.push(rd);
575 summaries.insert(rd.op, rs);
576 }
577 }
578 }
579 let mut summaries: Vec<RequestSummary> = summaries.into_values().collect();
581 summaries.sort_by_key(|s| s.operation);
582 Self { summaries }
583 }
584
585 pub fn to_batch(&self) -> RecordBatch {
597 let operations: StringArray = self
598 .iter()
599 .flat_map(|s| std::iter::repeat_n(Some(s.operation.to_string()), 2))
600 .collect();
601 let metrics: StringArray = self
602 .iter()
603 .flat_map(|_s| [Some("duration"), Some("size")])
604 .collect();
605 let mins: StringArray = self
606 .stats_iter()
607 .flat_map(|(duration_stats, size_stats)| {
608 let dur_min =
609 duration_stats.map(|d| format!("{:.6}s", d.min.as_secs_f32()));
610 let size_min = size_stats.map(|s| format!("{} B", s.min));
611 [dur_min, size_min]
612 })
613 .collect();
614 let maxs: StringArray = self
615 .stats_iter()
616 .flat_map(|(duration_stats, size_stats)| {
617 let dur_max =
618 duration_stats.map(|d| format!("{:.6}s", d.max.as_secs_f32()));
619 let size_max = size_stats.map(|s| format!("{} B", s.max));
620 [dur_max, size_max]
621 })
622 .collect();
623 let avgs: StringArray = self
624 .iter()
625 .flat_map(|s| {
626 let count = s.count as f32;
627 let duration_stats = s.duration_stats.as_ref();
628 let size_stats = s.size_stats.as_ref();
629 let dur_avg = duration_stats.map(|d| {
630 let avg = d.sum.as_secs_f32() / count;
631 format!("{avg:.6}s")
632 });
633 let size_avg = size_stats.map(|s| {
634 let avg = s.sum as f32 / count;
635 format!("{avg} B")
636 });
637 [dur_avg, size_avg]
638 })
639 .collect();
640 let sums: StringArray = self
641 .stats_iter()
642 .flat_map(|(duration_stats, size_stats)| {
643 let dur_sum =
651 duration_stats.map(|d| format!("{:.6}s", d.sum.as_secs_f32()));
652 let size_sum = size_stats.map(|s| format!("{} B", s.sum));
653 [dur_sum, size_sum]
654 })
655 .collect();
656 let counts: StringArray = self
657 .iter()
658 .flat_map(|s| {
659 let count = s.count.to_string();
660 [Some(count.clone()), Some(count)]
661 })
662 .collect();
663
664 RecordBatch::try_from_iter(vec![
665 ("Operation", Arc::new(operations) as ArrayRef),
666 ("Metric", Arc::new(metrics) as ArrayRef),
667 ("min", Arc::new(mins) as ArrayRef),
668 ("max", Arc::new(maxs) as ArrayRef),
669 ("avg", Arc::new(avgs) as ArrayRef),
670 ("sum", Arc::new(sums) as ArrayRef),
671 ("count", Arc::new(counts) as ArrayRef),
672 ])
673 .expect("Created the batch correctly")
674 }
675
676 fn iter(&self) -> impl Iterator<Item = &RequestSummary> {
678 self.summaries.iter()
679 }
680
681 fn stats_iter(
684 &self,
685 ) -> impl Iterator<Item = (Option<&Stats<Duration>>, Option<&Stats<usize>>)> {
686 self.summaries
687 .iter()
688 .map(|s| (s.duration_stats.as_ref(), s.size_stats.as_ref()))
689 }
690}
691
692pub struct RequestSummary {
695 operation: Operation,
696 count: usize,
697 duration_stats: Option<Stats<Duration>>,
698 size_stats: Option<Stats<usize>>,
699}
700
701impl RequestSummary {
702 fn new(operation: Operation) -> Self {
703 Self {
704 operation,
705 count: 0,
706 duration_stats: None,
707 size_stats: None,
708 }
709 }
710 fn push(&mut self, request: &RequestDetails) {
711 self.count += 1;
712 if let Some(dur) = request.duration() {
713 self.duration_stats.get_or_insert_default().push(dur)
714 }
715 if let Some(size) = request.size {
716 self.size_stats.get_or_insert_default().push(size)
717 }
718 }
719}
720
721struct Stats<T: Copy + Ord + AddAssign<T>> {
722 min: T,
723 max: T,
724 sum: T,
725}
726
727impl<T: Copy + Ord + AddAssign<T>> Stats<T> {
728 fn push(&mut self, val: T) {
729 self.min = cmp::min(val, self.min);
730 self.max = cmp::max(val, self.max);
731 self.sum += val;
732 }
733}
734
735impl Default for Stats<Duration> {
736 fn default() -> Self {
737 Self {
738 min: Duration::MAX,
739 max: Duration::ZERO,
740 sum: Duration::ZERO,
741 }
742 }
743}
744
745impl Default for Stats<usize> {
746 fn default() -> Self {
747 Self {
748 min: usize::MAX,
749 max: usize::MIN,
750 sum: 0,
751 }
752 }
753}
754
755#[derive(Debug)]
757pub struct InstrumentedObjectStoreRegistry {
758 inner: Arc<dyn ObjectStoreRegistry>,
759 instrument_mode: AtomicU8,
760 stores: RwLock<Vec<Arc<InstrumentedObjectStore>>>,
761}
762
763impl Default for InstrumentedObjectStoreRegistry {
764 fn default() -> Self {
765 Self::new()
766 }
767}
768
769impl InstrumentedObjectStoreRegistry {
770 pub fn new() -> Self {
773 Self {
774 inner: Arc::new(DefaultObjectStoreRegistry::new()),
775 instrument_mode: AtomicU8::new(InstrumentedObjectStoreMode::default() as u8),
776 stores: RwLock::new(Vec::new()),
777 }
778 }
779
780 pub fn with_profile_mode(self, mode: InstrumentedObjectStoreMode) -> Self {
781 self.instrument_mode.store(mode as u8, Ordering::Relaxed);
782 self
783 }
784
785 pub fn stores(&self) -> Vec<Arc<InstrumentedObjectStore>> {
788 self.stores.read().clone()
789 }
790
791 pub fn instrument_mode(&self) -> InstrumentedObjectStoreMode {
794 self.instrument_mode.load(Ordering::Relaxed).into()
795 }
796
797 pub fn set_instrument_mode(&self, mode: InstrumentedObjectStoreMode) {
799 self.instrument_mode.store(mode as u8, Ordering::Relaxed);
800 for s in self.stores.read().iter() {
801 s.set_instrument_mode(mode)
802 }
803 }
804}
805
806impl ObjectStoreRegistry for InstrumentedObjectStoreRegistry {
807 fn register_store(
808 &self,
809 url: &Url,
810 store: Arc<dyn ObjectStore>,
811 ) -> Option<Arc<dyn ObjectStore>> {
812 let mode = self.instrument_mode.load(Ordering::Relaxed);
813 let instrumented =
814 Arc::new(InstrumentedObjectStore::new(store, AtomicU8::new(mode)));
815 self.stores.write().push(Arc::clone(&instrumented));
816 self.inner.register_store(url, instrumented)
817 }
818
819 fn deregister_store(
820 &self,
821 url: &Url,
822 ) -> datafusion::common::Result<Arc<dyn ObjectStore>> {
823 self.inner.deregister_store(url)
824 }
825
826 fn get_store(&self, url: &Url) -> datafusion::common::Result<Arc<dyn ObjectStore>> {
827 self.inner.get_store(url)
828 }
829}
830
831#[cfg(test)]
832mod tests {
833 use futures::StreamExt;
834 use object_store::WriteMultipart;
835
836 use super::*;
837 use insta::assert_snapshot;
838
839 #[test]
840 fn instrumented_mode() {
841 assert!(matches!(
842 InstrumentedObjectStoreMode::default(),
843 InstrumentedObjectStoreMode::Disabled
844 ));
845
846 assert!(matches!(
847 "dIsABleD".parse().unwrap(),
848 InstrumentedObjectStoreMode::Disabled
849 ));
850 assert!(matches!(
851 "SUmMaRy".parse().unwrap(),
852 InstrumentedObjectStoreMode::Summary
853 ));
854 assert!(matches!(
855 "TRaCe".parse().unwrap(),
856 InstrumentedObjectStoreMode::Trace
857 ));
858 assert!(
859 "does_not_exist"
860 .parse::<InstrumentedObjectStoreMode>()
861 .is_err()
862 );
863
864 assert!(matches!(0.into(), InstrumentedObjectStoreMode::Disabled));
865 assert!(matches!(1.into(), InstrumentedObjectStoreMode::Summary));
866 assert!(matches!(2.into(), InstrumentedObjectStoreMode::Trace));
867 assert!(matches!(3.into(), InstrumentedObjectStoreMode::Disabled));
868 }
869
870 #[test]
871 fn instrumented_registry() {
872 let mut reg = InstrumentedObjectStoreRegistry::new();
873 assert!(reg.stores().is_empty());
874 assert_eq!(
875 reg.instrument_mode(),
876 InstrumentedObjectStoreMode::default()
877 );
878
879 reg = reg.with_profile_mode(InstrumentedObjectStoreMode::Trace);
880 assert_eq!(reg.instrument_mode(), InstrumentedObjectStoreMode::Trace);
881
882 let store = object_store::memory::InMemory::new();
883 let url = "mem://test".parse().unwrap();
884 let registered = reg.register_store(&url, Arc::new(store));
885 assert!(registered.is_none());
886
887 let fetched = reg.get_store(&url);
888 assert!(fetched.is_ok());
889 assert_eq!(reg.stores().len(), 1);
890 }
891
892 async fn setup_test_store() -> (InstrumentedObjectStore, Path) {
895 let store = Arc::new(object_store::memory::InMemory::new());
896 let mode = AtomicU8::new(InstrumentedObjectStoreMode::default() as u8);
897 let instrumented = InstrumentedObjectStore::new(store, mode);
898
899 let path = Path::from("test/data");
901 let payload = PutPayload::from_static(b"test_data");
902 instrumented.put(&path, payload).await.unwrap();
903
904 (instrumented, path)
905 }
906
907 #[tokio::test]
908 async fn instrumented_store_get() {
909 let (instrumented, path) = setup_test_store().await;
910
911 assert!(instrumented.requests.lock().is_empty());
913 let _ = instrumented.get(&path).await.unwrap();
914 assert!(instrumented.requests.lock().is_empty());
915
916 instrumented.set_instrument_mode(InstrumentedObjectStoreMode::Trace);
917 assert!(instrumented.requests.lock().is_empty());
918 let _ = instrumented.get(&path).await.unwrap();
919 assert_eq!(instrumented.requests.lock().len(), 1);
920
921 let mut requests = instrumented.take_requests();
922 assert_eq!(requests.len(), 1);
923 assert!(instrumented.requests.lock().is_empty());
924
925 let request = requests.pop().unwrap();
926 assert_eq!(request.op, Operation::Get);
927 assert_eq!(request.path, path);
928 assert!(request.duration().is_some());
929 assert_eq!(request.size, Some(9));
930 assert_eq!(request.range, None);
931 assert!(request.extra_display.is_none());
932 }
933
934 #[tokio::test]
935 async fn instrumented_store_delete() {
936 let (instrumented, path) = setup_test_store().await;
937
938 assert!(instrumented.requests.lock().is_empty());
940 instrumented.delete(&path).await.unwrap();
941 assert!(instrumented.requests.lock().is_empty());
942
943 let (instrumented, path) = setup_test_store().await;
945 instrumented.set_instrument_mode(InstrumentedObjectStoreMode::Trace);
946 assert!(instrumented.requests.lock().is_empty());
947 instrumented.delete(&path).await.unwrap();
948 assert_eq!(instrumented.requests.lock().len(), 1);
949
950 let mut requests = instrumented.take_requests();
951 assert_eq!(requests.len(), 1);
952 assert!(instrumented.requests.lock().is_empty());
953
954 let request = requests.pop().unwrap();
955 assert_eq!(request.op, Operation::Delete);
956 assert_eq!(request.path, path);
957 assert!(request.duration().is_some());
958 assert!(request.size.is_none());
959 assert!(request.range.is_none());
960 assert!(request.extra_display.is_none());
961 }
962
963 #[tokio::test]
964 async fn instrumented_store_list() {
965 let (instrumented, path) = setup_test_store().await;
966
967 assert!(instrumented.requests.lock().is_empty());
969 let _ = instrumented.list(Some(&path));
970 assert!(instrumented.requests.lock().is_empty());
971
972 instrumented.set_instrument_mode(InstrumentedObjectStoreMode::Trace);
973 assert!(instrumented.requests.lock().is_empty());
974 let mut stream = instrumented.list(Some(&path));
975 let delay = Duration::from_millis(50);
978 tokio::time::sleep(delay).await;
979 let _ = stream.next().await;
980 assert_eq!(instrumented.requests.lock().len(), 1);
981
982 let request = instrumented.take_requests().pop().unwrap();
983 assert_eq!(request.op, Operation::List);
984 assert_eq!(request.path, path);
985 let duration = request
986 .duration()
987 .expect("duration should be set after consuming stream");
988 assert!(
989 duration < delay,
990 "duration {duration:?} should exclude the {delay:?} sleep before first poll"
991 );
992 assert!(request.size.is_none());
993 assert!(request.range.is_none());
994 assert!(request.extra_display.is_none());
995 }
996
997 #[tokio::test]
998 async fn time_to_first_item_stream_captures_inner_latency() {
999 let inner_delay = Duration::from_millis(50);
1000 let inner_stream = futures::stream::once(async move {
1001 tokio::time::sleep(inner_delay).await;
1002 Ok(ObjectMeta {
1003 location: Path::from("test"),
1004 last_modified: Utc::now(),
1005 size: 0,
1006 e_tag: None,
1007 version: None,
1008 })
1009 })
1010 .boxed();
1011
1012 let duration_nanos = Arc::new(AtomicU64::new(0));
1013 let mut stream = Box::pin(TimeToFirstItemStream::new(
1014 inner_stream,
1015 Arc::clone(&duration_nanos),
1016 ));
1017 let _ = stream.next().await;
1018
1019 let recorded = Duration::from_nanos(duration_nanos.load(Ordering::Acquire));
1020 assert!(
1021 recorded >= inner_delay,
1022 "recorded duration {recorded:?} should be >= inner stream delay {inner_delay:?}"
1023 );
1024 }
1025
1026 #[tokio::test]
1027 async fn instrumented_store_list_with_delimiter() {
1028 let (instrumented, path) = setup_test_store().await;
1029
1030 assert!(instrumented.requests.lock().is_empty());
1032 let _ = instrumented.list_with_delimiter(Some(&path)).await.unwrap();
1033 assert!(instrumented.requests.lock().is_empty());
1034
1035 instrumented.set_instrument_mode(InstrumentedObjectStoreMode::Trace);
1036 assert!(instrumented.requests.lock().is_empty());
1037 let _ = instrumented.list_with_delimiter(Some(&path)).await.unwrap();
1038 assert_eq!(instrumented.requests.lock().len(), 1);
1039
1040 let request = instrumented.take_requests().pop().unwrap();
1041 assert_eq!(request.op, Operation::List);
1042 assert_eq!(request.path, path);
1043 assert!(request.duration().is_some());
1044 assert!(request.size.is_none());
1045 assert!(request.range.is_none());
1046 assert!(request.extra_display.is_none());
1047 }
1048
1049 #[tokio::test]
1050 async fn instrumented_store_put_opts() {
1051 let store = Arc::new(object_store::memory::InMemory::new());
1054 let mode = AtomicU8::new(InstrumentedObjectStoreMode::default() as u8);
1055 let instrumented = InstrumentedObjectStore::new(store, mode);
1056
1057 let path = Path::from("test/data");
1058 let payload = PutPayload::from_static(b"test_data");
1059 let size = payload.content_length();
1060
1061 assert!(instrumented.requests.lock().is_empty());
1063 instrumented.put(&path, payload.clone()).await.unwrap();
1064 assert!(instrumented.requests.lock().is_empty());
1065
1066 instrumented.set_instrument_mode(InstrumentedObjectStoreMode::Trace);
1067 assert!(instrumented.requests.lock().is_empty());
1068 instrumented.put(&path, payload).await.unwrap();
1069 assert_eq!(instrumented.requests.lock().len(), 1);
1070
1071 let request = instrumented.take_requests().pop().unwrap();
1072 assert_eq!(request.op, Operation::Put);
1073 assert_eq!(request.path, path);
1074 assert!(request.duration().is_some());
1075 assert_eq!(request.size.unwrap(), size);
1076 assert!(request.range.is_none());
1077 assert!(request.extra_display.is_none());
1078 }
1079
1080 #[tokio::test]
1081 async fn instrumented_store_put_multipart() {
1082 let store = Arc::new(object_store::memory::InMemory::new());
1085 let mode = AtomicU8::new(InstrumentedObjectStoreMode::default() as u8);
1086 let instrumented = InstrumentedObjectStore::new(store, mode);
1087
1088 let path = Path::from("test/data");
1089
1090 assert!(instrumented.requests.lock().is_empty());
1092 let mp = instrumented.put_multipart(&path).await.unwrap();
1093 let mut write = WriteMultipart::new(mp);
1094 write.write(b"test_data");
1095 write.finish().await.unwrap();
1096 assert!(instrumented.requests.lock().is_empty());
1097
1098 instrumented.set_instrument_mode(InstrumentedObjectStoreMode::Trace);
1099 assert!(instrumented.requests.lock().is_empty());
1100 let mp = instrumented.put_multipart(&path).await.unwrap();
1101 let mut write = WriteMultipart::new(mp);
1102 write.write(b"test_data");
1103 write.finish().await.unwrap();
1104 assert_eq!(instrumented.requests.lock().len(), 1);
1105
1106 let request = instrumented.take_requests().pop().unwrap();
1107 assert_eq!(request.op, Operation::Put);
1108 assert_eq!(request.path, path);
1109 assert!(request.duration().is_some());
1110 assert!(request.size.is_none());
1111 assert!(request.range.is_none());
1112 assert!(request.extra_display.is_none());
1113 }
1114
1115 #[tokio::test]
1116 async fn instrumented_store_copy() {
1117 let (instrumented, path) = setup_test_store().await;
1118 let copy_to = Path::from("test/copied");
1119
1120 assert!(instrumented.requests.lock().is_empty());
1122 instrumented.copy(&path, ©_to).await.unwrap();
1123 assert!(instrumented.requests.lock().is_empty());
1124
1125 instrumented.set_instrument_mode(InstrumentedObjectStoreMode::Trace);
1126 assert!(instrumented.requests.lock().is_empty());
1127 instrumented.copy(&path, ©_to).await.unwrap();
1128 assert_eq!(instrumented.requests.lock().len(), 1);
1129
1130 let mut requests = instrumented.take_requests();
1131 assert_eq!(requests.len(), 1);
1132 assert!(instrumented.requests.lock().is_empty());
1133
1134 let request = requests.pop().unwrap();
1135 assert_eq!(request.op, Operation::Copy);
1136 assert_eq!(request.path, path);
1137 assert!(request.duration().is_some());
1138 assert!(request.size.is_none());
1139 assert!(request.range.is_none());
1140 assert_eq!(
1141 request.extra_display.unwrap(),
1142 format!("copy_to: {copy_to}")
1143 );
1144 }
1145
1146 #[tokio::test]
1147 async fn instrumented_store_copy_if_not_exists() {
1148 let (instrumented, path) = setup_test_store().await;
1149 let mut copy_to = Path::from("test/copied");
1150
1151 assert!(instrumented.requests.lock().is_empty());
1153 instrumented
1154 .copy_if_not_exists(&path, ©_to)
1155 .await
1156 .unwrap();
1157 assert!(instrumented.requests.lock().is_empty());
1158
1159 copy_to = Path::from("test/copied_again");
1161 instrumented.set_instrument_mode(InstrumentedObjectStoreMode::Trace);
1162 assert!(instrumented.requests.lock().is_empty());
1163 instrumented
1164 .copy_if_not_exists(&path, ©_to)
1165 .await
1166 .unwrap();
1167 assert_eq!(instrumented.requests.lock().len(), 1);
1168
1169 let mut requests = instrumented.take_requests();
1170 assert_eq!(requests.len(), 1);
1171 assert!(instrumented.requests.lock().is_empty());
1172
1173 let request = requests.pop().unwrap();
1174 assert_eq!(request.op, Operation::Copy);
1175 assert_eq!(request.path, path);
1176 assert!(request.duration().is_some());
1177 assert!(request.size.is_none());
1178 assert!(request.range.is_none());
1179 assert_eq!(
1180 request.extra_display.unwrap(),
1181 format!("copy_to: {copy_to}")
1182 );
1183 }
1184
1185 #[tokio::test]
1186 async fn instrumented_store_head() {
1187 let (instrumented, path) = setup_test_store().await;
1188
1189 assert!(instrumented.requests.lock().is_empty());
1191 let _ = instrumented.head(&path).await.unwrap();
1192 assert!(instrumented.requests.lock().is_empty());
1193
1194 instrumented.set_instrument_mode(InstrumentedObjectStoreMode::Trace);
1195 assert!(instrumented.requests.lock().is_empty());
1196 let _ = instrumented.head(&path).await.unwrap();
1197 assert_eq!(instrumented.requests.lock().len(), 1);
1198
1199 let mut requests = instrumented.take_requests();
1200 assert_eq!(requests.len(), 1);
1201 assert!(instrumented.requests.lock().is_empty());
1202
1203 let request = requests.pop().unwrap();
1204 assert_eq!(request.op, Operation::Head);
1205 assert_eq!(request.path, path);
1206 assert!(request.duration().is_some());
1207 assert!(request.size.is_none());
1208 assert!(request.range.is_none());
1209 assert!(request.extra_display.is_none());
1210 }
1211
1212 #[test]
1213 fn request_details() {
1214 let rd = RequestDetails {
1215 op: Operation::Get,
1216 path: Path::from("test"),
1217 timestamp: chrono::DateTime::from_timestamp(0, 0).unwrap(),
1218 duration_nanos: Arc::new(AtomicU64::new(
1219 Duration::new(5, 0).as_nanos() as u64
1220 )),
1221 size: Some(10),
1222 range: Some((..10).into()),
1223 extra_display: Some(String::from("extra info")),
1224 };
1225
1226 assert_eq!(
1227 format!("{rd}"),
1228 "1970-01-01T00:00:00+00:00 operation=Get duration=5.000000s size=10 range: bytes=0-9 path=test extra info"
1229 );
1230 }
1231
1232 #[test]
1233 fn request_summary() {
1234 let mut requests = Vec::new();
1236 assert_snapshot!(RequestSummaries::new(&requests), @r"
1237 +-----------+--------+-----+-----+-----+-----+-------+
1238 | Operation | Metric | min | max | avg | sum | count |
1239 +-----------+--------+-----+-----+-----+-----+-------+
1240 +-----------+--------+-----+-----+-----+-----+-------+
1241 ");
1242
1243 requests.push(RequestDetails {
1244 op: Operation::Get,
1245 path: Path::from("test1"),
1246 timestamp: chrono::DateTime::from_timestamp(0, 0).unwrap(),
1247 duration_nanos: Arc::new(AtomicU64::new(
1248 Duration::from_secs(5).as_nanos() as u64
1249 )),
1250 size: Some(100),
1251 range: None,
1252 extra_display: None,
1253 });
1254
1255 assert_snapshot!(RequestSummaries::new(&requests), @r"
1256 +-----------+----------+-----------+-----------+-----------+-----------+-------+
1257 | Operation | Metric | min | max | avg | sum | count |
1258 +-----------+----------+-----------+-----------+-----------+-----------+-------+
1259 | Get | duration | 5.000000s | 5.000000s | 5.000000s | 5.000000s | 1 |
1260 | Get | size | 100 B | 100 B | 100 B | 100 B | 1 |
1261 +-----------+----------+-----------+-----------+-----------+-----------+-------+
1262 ");
1263
1264 requests.push(RequestDetails {
1266 op: Operation::Get,
1267 path: Path::from("test2"),
1268 timestamp: chrono::DateTime::from_timestamp(1, 0).unwrap(),
1269 duration_nanos: Arc::new(AtomicU64::new(
1270 Duration::from_secs(8).as_nanos() as u64
1271 )),
1272 size: Some(150),
1273 range: None,
1274 extra_display: None,
1275 });
1276 requests.push(RequestDetails {
1277 op: Operation::Get,
1278 path: Path::from("test3"),
1279 timestamp: chrono::DateTime::from_timestamp(2, 0).unwrap(),
1280 duration_nanos: Arc::new(AtomicU64::new(
1281 Duration::from_secs(2).as_nanos() as u64
1282 )),
1283 size: Some(50),
1284 range: None,
1285 extra_display: None,
1286 });
1287 assert_snapshot!(RequestSummaries::new(&requests), @r"
1288 +-----------+----------+-----------+-----------+-----------+------------+-------+
1289 | Operation | Metric | min | max | avg | sum | count |
1290 +-----------+----------+-----------+-----------+-----------+------------+-------+
1291 | Get | duration | 2.000000s | 8.000000s | 5.000000s | 15.000000s | 3 |
1292 | Get | size | 50 B | 150 B | 100 B | 300 B | 3 |
1293 +-----------+----------+-----------+-----------+-----------+------------+-------+
1294 ");
1295
1296 requests.push(RequestDetails {
1298 op: Operation::Put,
1299 path: Path::from("test4"),
1300 timestamp: chrono::DateTime::from_timestamp(3, 0).unwrap(),
1301 duration_nanos: Arc::new(AtomicU64::new(
1302 Duration::from_millis(200).as_nanos() as u64,
1303 )),
1304 size: Some(75),
1305 range: None,
1306 extra_display: None,
1307 });
1308
1309 assert_snapshot!(RequestSummaries::new(&requests), @r"
1310 +-----------+----------+-----------+-----------+-----------+------------+-------+
1311 | Operation | Metric | min | max | avg | sum | count |
1312 +-----------+----------+-----------+-----------+-----------+------------+-------+
1313 | Get | duration | 2.000000s | 8.000000s | 5.000000s | 15.000000s | 3 |
1314 | Get | size | 50 B | 150 B | 100 B | 300 B | 3 |
1315 | Put | duration | 0.200000s | 0.200000s | 0.200000s | 0.200000s | 1 |
1316 | Put | size | 75 B | 75 B | 75 B | 75 B | 1 |
1317 +-----------+----------+-----------+-----------+-----------+------------+-------+
1318 ");
1319 }
1320
1321 #[test]
1322 fn request_summary_only_duration() {
1323 let only_duration = vec![RequestDetails {
1325 op: Operation::Get,
1326 path: Path::from("test1"),
1327 timestamp: chrono::DateTime::from_timestamp(0, 0).unwrap(),
1328 duration_nanos: Arc::new(AtomicU64::new(
1329 Duration::from_secs(3).as_nanos() as u64
1330 )),
1331 size: None,
1332 range: None,
1333 extra_display: None,
1334 }];
1335 assert_snapshot!(RequestSummaries::new(&only_duration), @r"
1336 +-----------+----------+-----------+-----------+-----------+-----------+-------+
1337 | Operation | Metric | min | max | avg | sum | count |
1338 +-----------+----------+-----------+-----------+-----------+-----------+-------+
1339 | Get | duration | 3.000000s | 3.000000s | 3.000000s | 3.000000s | 1 |
1340 | Get | size | | | | | 1 |
1341 +-----------+----------+-----------+-----------+-----------+-----------+-------+
1342 ");
1343 }
1344
1345 #[test]
1346 fn request_summary_only_size() {
1347 let only_size = vec![RequestDetails {
1349 op: Operation::Get,
1350 path: Path::from("test1"),
1351 timestamp: chrono::DateTime::from_timestamp(0, 0).unwrap(),
1352 duration_nanos: Arc::new(AtomicU64::new(0)),
1353 size: Some(200),
1354 range: None,
1355 extra_display: None,
1356 }];
1357 assert_snapshot!(RequestSummaries::new(&only_size), @r"
1358 +-----------+----------+-------+-------+-------+-------+-------+
1359 | Operation | Metric | min | max | avg | sum | count |
1360 +-----------+----------+-------+-------+-------+-------+-------+
1361 | Get | duration | | | | | 1 |
1362 | Get | size | 200 B | 200 B | 200 B | 200 B | 1 |
1363 +-----------+----------+-------+-------+-------+-------+-------+
1364 ");
1365 }
1366
1367 #[test]
1368 fn request_summary_neither_duration_or_size() {
1369 let no_stats = vec![RequestDetails {
1371 op: Operation::Get,
1372 path: Path::from("test1"),
1373 timestamp: chrono::DateTime::from_timestamp(0, 0).unwrap(),
1374 duration_nanos: Arc::new(AtomicU64::new(0)),
1375 size: None,
1376 range: None,
1377 extra_display: None,
1378 }];
1379 assert_snapshot!(RequestSummaries::new(&no_stats), @r"
1380 +-----------+----------+-----+-----+-----+-----+-------+
1381 | Operation | Metric | min | max | avg | sum | count |
1382 +-----------+----------+-----+-----+-----+-----+-------+
1383 | Get | duration | | | | | 1 |
1384 | Get | size | | | | | 1 |
1385 +-----------+----------+-----+-----+-----+-----+-------+
1386 ");
1387 }
1388}