gstreamer/
log_context.rs

1// Take a look at the license at the top of the repository in the LICENSE file.
2
3use std::{fmt, io::Write, ptr};
4
5use glib::{prelude::*, translate::*};
6
7use crate::log::DebugLogger;
8use crate::{ffi, ClockTime, DebugCategory, DebugLevel, LogContextFlags, LogContextHashFlags};
9
10/// A context for controlling logging behavior, for example to handle
11/// logging once or periodic logging, avoiding to
12/// spam the terminal with the same log message multiple times.
13///
14/// ## Simple log context using static macros
15///
16/// **⚠️ The following code is in  c ⚠️**
17///
18/// ``` c
19/// // At global/file scope:
20/// GST_LOG_CONTEXT_STATIC_DEFINE(my_context, GST_LOG_CONTEXT_FLAG_THROTTLE, );
21/// #define MY_CONTEXT GST_LOG_CONTEXT_LAZY_INIT(my_context)
22///
23/// // Then in code:
24/// GST_CTX_INFO(MY_CONTEXT, "This will only appear once per file/line");
25/// ```
26///
27/// ## Periodic logging
28///
29/// For messages that should be logged periodically (e.g., maximum once per minute):
30///
31/// **⚠️ The following code is in  c ⚠️**
32///
33/// ``` c
34/// // At global/file scope:
35/// GST_LOG_CONTEXT_STATIC_DEFINE(my_periodic_context, GST_LOG_CONTEXT_FLAG_THROTTLE,
36///   GST_LOG_CONTEXT_BUILDER_SET_INTERVAL(60 * GST_SECOND);
37/// );
38/// #define MY_PERIODIC_CONTEXT GST_LOG_CONTEXT_LAZY_INIT(my_periodic_context)
39///
40/// // Then in code:
41/// GST_CTX_INFO(MY_PERIODIC_CONTEXT, "This appears once per minute");
42/// ```
43///
44/// ## Customizing Message hash with custom flags and category
45///
46/// By default, a message's hash is determined by the file name, object pointer,
47/// and format string. You can customize this with builder operations:
48///
49/// **⚠️ The following code is in  c ⚠️**
50///
51/// ``` c
52/// // Ignore the object pointer when determining message hash (with throttling)
53/// GST_LOG_CONTEXT_STATIC_DEFINE(obj_independent_ctx, GST_LOG_CONTEXT_FLAG_THROTTLE,
54///   GST_LOG_CONTEXT_BUILDER_SET_HASH_FLAGS(GST_LOG_CONTEXT_IGNORE_OBJECT);
55/// );
56///
57/// // Use a custom category (without throttling)
58/// GST_LOG_CONTEXT_STATIC_DEFINE(custom_cat_ctx, GST_LOG_CONTEXT_FLAG_NONE,
59///   GST_LOG_CONTEXT_BUILDER_SET_CATEGORY(my_category);
60/// );
61/// ```
62#[derive(Debug)]
63#[doc(alias = "GstLogContext")]
64#[repr(transparent)]
65pub struct LogContext(ptr::NonNull<ffi::GstLogContext>);
66
67impl LogContext {
68    /// Get the [`DebugCategory`][crate::DebugCategory] associated with this log context.
69    ///
70    /// # Returns
71    ///
72    /// the [`DebugCategory`][crate::DebugCategory] to which the context is bound
73    #[cfg_attr(docsrs, doc(cfg(feature = "v1_28")))]
74    #[doc(alias = "gst_log_context_get_category")]
75    #[doc(alias = "get_category")]
76    #[inline]
77    pub fn category(&self) -> DebugCategory {
78        unsafe { from_glib_none(ffi::gst_log_context_get_category(self.0.as_ptr())) }
79    }
80
81    /// Resets the logging context, clearing all tracked messages.
82    #[cfg_attr(docsrs, doc(cfg(feature = "v1_28")))]
83    #[doc(alias = "gst_log_context_reset")]
84    #[inline]
85    pub fn reset(&self) {
86        unsafe {
87            ffi::gst_log_context_reset(self.0.as_ptr());
88        }
89    }
90
91    #[inline]
92    pub fn as_ptr(&self) -> *mut ffi::GstLogContext {
93        self.0.as_ptr()
94    }
95
96    // rustdoc-stripper-ignore-next
97    /// Logs without checking the log level.
98    #[inline(never)]
99    fn log_unfiltered_internal(
100        &self,
101        obj: Option<&glib::Object>,
102        level: DebugLevel,
103        file: &glib::GStr,
104        function: &str,
105        line: u32,
106        args: fmt::Arguments,
107    ) {
108        let mut w = smallvec::SmallVec::<[u8; 256]>::new();
109
110        // Can't really happen but better safe than sorry
111        if Write::write_fmt(&mut w, args).is_err() {
112            return;
113        }
114        w.push(0);
115
116        self.log_literal_unfiltered_internal(obj, level, file, function, line, unsafe {
117            glib::GStr::from_utf8_with_nul_unchecked(&w)
118        });
119    }
120
121    #[inline(never)]
122    fn log_literal_unfiltered_internal(
123        &self,
124        obj: Option<&glib::Object>,
125        level: DebugLevel,
126        file: &glib::GStr,
127        function: &str,
128        line: u32,
129        msg: &glib::GStr,
130    ) {
131        let obj_ptr = match obj {
132            Some(obj) => obj.as_ptr(),
133            None => ptr::null_mut(),
134        };
135
136        function.run_with_gstr(|function| unsafe {
137            ffi::gst_debug_log_literal_with_context(
138                self.0.as_ptr(),
139                level.into_glib(),
140                file.as_ptr(),
141                function.as_ptr(),
142                line as i32,
143                obj_ptr,
144                msg.as_ptr(),
145            );
146        });
147    }
148
149    #[inline(never)]
150    fn log_id_unfiltered_internal(
151        &self,
152        id: &glib::GStr,
153        level: DebugLevel,
154        file: &glib::GStr,
155        function: &str,
156        line: u32,
157        args: fmt::Arguments,
158    ) {
159        let mut w = smallvec::SmallVec::<[u8; 256]>::new();
160
161        // Can't really happen but better safe than sorry
162        if Write::write_fmt(&mut w, args).is_err() {
163            return;
164        }
165        w.push(0);
166
167        self.log_id_literal_unfiltered_internal(id, level, file, function, line, unsafe {
168            glib::GStr::from_utf8_with_nul_unchecked(&w)
169        });
170    }
171
172    #[inline(never)]
173    fn log_id_literal_unfiltered_internal(
174        &self,
175        id: &glib::GStr,
176        level: DebugLevel,
177        file: &glib::GStr,
178        function: &str,
179        line: u32,
180        msg: &glib::GStr,
181    ) {
182        function.run_with_gstr(|function| unsafe {
183            ffi::gst_debug_log_id_literal_with_context(
184                self.0.as_ptr(),
185                level.into_glib(),
186                file.as_ptr(),
187                function.as_ptr(),
188                line as i32,
189                id.as_ptr(),
190                msg.as_ptr(),
191            );
192        });
193    }
194
195    #[cfg_attr(docsrs, doc(cfg(feature = "v1_28")))]
196    #[doc(alias = "gst_debug_log_with_context")]
197    pub fn log(
198        &self,
199        obj: Option<&impl IsA<glib::Object>>,
200        level: DebugLevel,
201        file: &glib::GStr,
202        function: &str,
203        line: u32,
204        args: fmt::Arguments,
205    ) {
206        if !self.above_threshold(level) {
207            return;
208        }
209
210        self.log_unfiltered_internal(
211            obj.map(|obj| obj.as_ref()),
212            level,
213            file,
214            function,
215            line,
216            args,
217        )
218    }
219
220    #[cfg_attr(docsrs, doc(cfg(feature = "v1_28")))]
221    #[doc(alias = "gst_debug_log_literal_with_context")]
222    pub fn log_literal(
223        &self,
224        obj: Option<&impl IsA<glib::Object>>,
225        level: DebugLevel,
226        file: &glib::GStr,
227        function: &str,
228        line: u32,
229        msg: &glib::GStr,
230    ) {
231        if !self.above_threshold(level) {
232            return;
233        }
234
235        self.log_literal_unfiltered_internal(
236            obj.map(|obj| obj.as_ref()),
237            level,
238            file,
239            function,
240            line,
241            msg,
242        )
243    }
244
245    #[cfg_attr(docsrs, doc(cfg(feature = "v1_28")))]
246    #[doc(alias = "gst_debug_log_id_with_context")]
247    pub fn log_id(
248        &self,
249        id: impl AsRef<glib::GStr>,
250        level: DebugLevel,
251        file: &glib::GStr,
252        function: &str,
253        line: u32,
254        args: fmt::Arguments,
255    ) {
256        if !self.above_threshold(level) {
257            return;
258        }
259
260        self.log_id_unfiltered_internal(id.as_ref(), level, file, function, line, args);
261    }
262
263    #[cfg_attr(docsrs, doc(cfg(feature = "v1_28")))]
264    #[doc(alias = "gst_debug_log_id_literal_with_context")]
265    pub fn log_id_literal(
266        &self,
267        id: impl AsRef<glib::GStr>,
268        level: DebugLevel,
269        file: &glib::GStr,
270        function: &str,
271        line: u32,
272        msg: &glib::GStr,
273    ) {
274        if !self.above_threshold(level) {
275            return;
276        }
277
278        self.log_id_literal_unfiltered_internal(id.as_ref(), level, file, function, line, msg);
279    }
280}
281
282impl Drop for LogContext {
283    fn drop(&mut self) {
284        unsafe {
285            ffi::gst_log_context_free(self.0.as_ptr());
286        }
287    }
288}
289
290unsafe impl Send for LogContext {}
291unsafe impl Sync for LogContext {}
292
293impl crate::log::DebugLogger for LogContext {
294    #[inline]
295    fn above_threshold(&self, level: crate::DebugLevel) -> bool {
296        self.category().above_threshold(level)
297    }
298
299    #[inline]
300    fn log_unfiltered(
301        &self,
302        obj: Option<&impl IsA<glib::Object>>,
303        level: crate::DebugLevel,
304        file: &glib::GStr,
305        function: &str,
306        line: u32,
307        args: std::fmt::Arguments,
308    ) {
309        self.log_unfiltered_internal(
310            obj.map(|obj| obj.as_ref()),
311            level,
312            file,
313            function,
314            line,
315            args,
316        )
317    }
318
319    #[inline]
320    fn log_literal_unfiltered(
321        &self,
322        obj: Option<&impl IsA<glib::Object>>,
323        level: crate::DebugLevel,
324        file: &glib::GStr,
325        function: &str,
326        line: u32,
327        msg: &glib::GStr,
328    ) {
329        self.log_literal_unfiltered_internal(
330            obj.map(|obj| obj.as_ref()),
331            level,
332            file,
333            function,
334            line,
335            msg,
336        )
337    }
338
339    #[inline]
340    fn log_id_unfiltered(
341        &self,
342        id: impl AsRef<glib::GStr>,
343        level: crate::DebugLevel,
344        file: &glib::GStr,
345        function: &str,
346        line: u32,
347        args: std::fmt::Arguments,
348    ) {
349        self.log_id_unfiltered_internal(id.as_ref(), level, file, function, line, args)
350    }
351
352    // rustdoc-stripper-ignore-next
353    /// Logs without checking the log level.
354    #[inline]
355    fn log_id_literal_unfiltered(
356        &self,
357        id: impl AsRef<glib::GStr>,
358        level: crate::DebugLevel,
359        file: &glib::GStr,
360        function: &str,
361        line: u32,
362        msg: &glib::GStr,
363    ) {
364        self.log_id_literal_unfiltered_internal(id.as_ref(), level, file, function, line, msg)
365    }
366}
367
368/// A builder for creating a [`LogContext`][crate::LogContext]. This provides a flexible way to
369/// configure a log context with various options while maintaining immutability
370/// of the resulting context.
371#[derive(Debug)]
372#[doc(alias = "GstLogContextBuilder")]
373#[repr(transparent)]
374pub struct LogContextBuilder(ptr::NonNull<ffi::GstLogContextBuilder>);
375
376impl LogContextBuilder {
377    /// Creates a new builder for configuring a [`LogContext`][crate::LogContext] with the specified
378    /// debug category and flags.
379    /// ## `category`
380    /// the debug category to use
381    /// ## `flags`
382    /// the flags to use for the log context
383    ///
384    /// # Returns
385    ///
386    /// a new [`LogContextBuilder`][crate::LogContextBuilder]
387    #[cfg_attr(docsrs, doc(cfg(feature = "v1_28")))]
388    #[doc(alias = "gst_log_context_builder_new")]
389    pub fn new(category: DebugCategory, flags: LogContextFlags) -> Self {
390        skip_assert_initialized!();
391        unsafe {
392            let ptr = ffi::gst_log_context_builder_new(category.as_ptr(), flags.into_glib());
393            LogContextBuilder(ptr::NonNull::new_unchecked(ptr))
394        }
395    }
396
397    #[cfg_attr(docsrs, doc(cfg(feature = "v1_28")))]
398    #[doc(alias = "gst_log_context_builder_set_hash_flags")]
399    pub fn hash_flags(self, flags: LogContextHashFlags) -> Self {
400        unsafe {
401            ffi::gst_log_context_builder_set_hash_flags(self.0.as_ptr(), flags.into_glib());
402        }
403        self
404    }
405
406    #[cfg_attr(docsrs, doc(cfg(feature = "v1_28")))]
407    #[doc(alias = "gst_log_context_builder_set_category")]
408    pub fn category(self, category: DebugCategory) -> Self {
409        unsafe {
410            ffi::gst_log_context_builder_set_category(self.0.as_ptr(), category.as_ptr());
411        }
412        self
413    }
414
415    #[cfg_attr(docsrs, doc(cfg(feature = "v1_28")))]
416    #[doc(alias = "gst_log_context_builder_set_interval")]
417    pub fn interval(self, interval: impl Into<Option<ClockTime>>) -> Self {
418        unsafe {
419            ffi::gst_log_context_builder_set_interval(self.0.as_ptr(), interval.into().into_glib());
420        }
421        self
422    }
423
424    /// Builds a [`LogContext`][crate::LogContext] from the builder configuration.
425    /// The builder is consumed by this function and should not be used afterward.
426    ///
427    /// # Returns
428    ///
429    /// a new [`LogContext`][crate::LogContext]
430    #[cfg_attr(docsrs, doc(cfg(feature = "v1_28")))]
431    #[doc(alias = "gst_log_context_builder_build")]
432    pub fn build(self) -> LogContext {
433        unsafe {
434            let ptr = ffi::gst_log_context_builder_build(self.0.as_ptr());
435            LogContext(ptr::NonNull::new_unchecked(ptr))
436        }
437    }
438}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443    use crate::{log::DebugLogger, DebugLevel, LogContextFlags, LogContextHashFlags};
444
445    #[test]
446    fn log_context_builder_basic() {
447        crate::init().unwrap();
448
449        let cat = crate::DebugCategory::new(
450            "test-log-context",
451            crate::DebugColorFlags::empty(),
452            Some("Test log context category"),
453        );
454
455        // Test basic builder pattern
456        let context = LogContextBuilder::new(cat, LogContextFlags::empty()).build();
457
458        assert_eq!(context.category(), cat);
459    }
460
461    #[test]
462    fn log_context_builder_with_flags() {
463        crate::init().unwrap();
464
465        let cat = crate::DebugCategory::new(
466            "test-log-context-flags",
467            crate::DebugColorFlags::empty(),
468            Some("Test log context with flags"),
469        );
470
471        // Test builder with various configuration options
472        let context = LogContextBuilder::new(cat, LogContextFlags::THROTTLE)
473            .hash_flags(LogContextHashFlags::USE_LINE_NUMBER)
474            .interval(Some(crate::ClockTime::from_seconds(1)))
475            .build();
476
477        assert_eq!(context.category(), cat);
478    }
479
480    #[test]
481    fn log_context_trait_implementation() {
482        crate::init().unwrap();
483
484        let cat = crate::DebugCategory::new(
485            "test-trait-impl",
486            crate::DebugColorFlags::empty(),
487            Some("Test trait implementation"),
488        );
489
490        let context = LogContextBuilder::new(cat, LogContextFlags::empty()).build();
491
492        // Test that LogContext implements DebugLogger trait
493        assert_eq!(
494            context.above_threshold(DebugLevel::Error),
495            cat.above_threshold(DebugLevel::Error)
496        );
497        assert_eq!(
498            context.above_threshold(DebugLevel::Debug),
499            cat.above_threshold(DebugLevel::Debug)
500        );
501    }
502
503    #[test]
504    fn log_context_with_macros() {
505        crate::init().unwrap();
506
507        let cat = crate::DebugCategory::new(
508            "test-macro-usage",
509            crate::DebugColorFlags::empty(),
510            Some("Test macro usage"),
511        );
512        cat.set_threshold(DebugLevel::Trace);
513
514        let context = LogContextBuilder::new(cat, LogContextFlags::empty()).build();
515
516        // Test that LogContext works with all the logging macros
517        crate::error!(context, "error message");
518        crate::error!(&context, "error message");
519        crate::warning!(context, "warning message");
520        crate::warning!(&context, "warning message");
521        crate::info!(context, "info message");
522        crate::info!(&context, "info message");
523        crate::debug!(context, "debug message");
524        crate::debug!(&context, "debug message");
525        crate::trace!(context, "trace message");
526        crate::trace!(&context, "trace message");
527
528        // Test with object
529        let obj = crate::Bin::with_name("test-bin");
530        crate::error!(context, obj = &obj, "error with object");
531        crate::warning!(context, obj = &obj, "warning with object");
532
533        // Test with formatting
534        let value = 42;
535        crate::info!(context, "formatted message: {}", value);
536        crate::debug!(context, obj = &obj, "formatted with obj: {}", value);
537    }
538
539    #[test]
540    fn log_context_interchangeable_with_category() {
541        crate::init().unwrap();
542
543        let cat = crate::DebugCategory::new(
544            "test-interchangeable",
545            crate::DebugColorFlags::empty(),
546            Some("Test interchangeable usage"),
547        );
548        cat.set_threshold(DebugLevel::Info);
549
550        let context = LogContextBuilder::new(cat, LogContextFlags::empty()).build();
551
552        // Test that we can use both category and context in the same way
553        let test_message = "test message";
554
555        // These should both work identically
556        crate::info!(cat, "{}", test_message);
557        crate::info!(&cat, "{}", test_message);
558        crate::info!(context, "{}", test_message);
559        crate::info!(&context, "{}", test_message);
560
561        // With objects too
562        let obj = crate::Bin::with_name("test-bin-2");
563        crate::info!(cat, obj = &obj, "{}", test_message);
564        crate::info!(context, obj = &obj, "{}", test_message);
565    }
566
567    #[test]
568    fn static_log_context() {
569        crate::init().unwrap();
570
571        // Create a static category first
572        static TEST_CATEGORY: std::sync::LazyLock<crate::DebugCategory> =
573            std::sync::LazyLock::new(|| {
574                crate::DebugCategory::new(
575                    "test-static-context",
576                    crate::DebugColorFlags::empty(),
577                    Some("Test static context"),
578                )
579            });
580
581        // Create static context directly with LazyLock
582        static TEST_CONTEXT: std::sync::LazyLock<LogContext> = std::sync::LazyLock::new(|| {
583            LogContextBuilder::new(*TEST_CATEGORY, LogContextFlags::empty()).build()
584        });
585
586        // Use the static context
587        crate::info!(TEST_CONTEXT, "message from static context");
588
589        assert_eq!(TEST_CONTEXT.category(), *TEST_CATEGORY);
590    }
591
592    #[test]
593    fn static_log_context_with_advanced_options() {
594        crate::init().unwrap();
595
596        // Create a static category first
597        static ADVANCED_CATEGORY: std::sync::LazyLock<crate::DebugCategory> =
598            std::sync::LazyLock::new(|| {
599                crate::DebugCategory::new(
600                    "test-static-advanced",
601                    crate::DebugColorFlags::empty(),
602                    Some("Test static context advanced"),
603                )
604            });
605
606        // Create static context with advanced options using LazyLock
607        static ADVANCED_CONTEXT: std::sync::LazyLock<LogContext> = std::sync::LazyLock::new(|| {
608            LogContextBuilder::new(*ADVANCED_CATEGORY, LogContextFlags::THROTTLE)
609                .hash_flags(LogContextHashFlags::USE_LINE_NUMBER)
610                .interval(Some(crate::ClockTime::from_seconds(2)))
611                .build()
612        });
613
614        crate::debug!(ADVANCED_CONTEXT, "advanced static context message");
615        assert_eq!(ADVANCED_CONTEXT.category(), *ADVANCED_CATEGORY);
616    }
617
618    #[test]
619    fn log_context_with_id_logging() {
620        crate::init().unwrap();
621
622        let cat = crate::DebugCategory::new(
623            "test-id-logging",
624            crate::DebugColorFlags::empty(),
625            Some("Test ID logging"),
626        );
627        cat.set_threshold(DebugLevel::Trace);
628
629        let context = LogContextBuilder::new(cat, LogContextFlags::empty()).build();
630
631        // Test ID-based logging with LogContext
632        crate::trace!(context, id = "test-id-123", "message with ID");
633        crate::debug!(
634            context,
635            id = "test-id-456",
636            "another message with ID: {}",
637            42
638        );
639    }
640
641    #[test]
642    fn log_context_memory_safety() {
643        crate::init().unwrap();
644
645        let cat = crate::DebugCategory::new(
646            "test-memory-safety",
647            crate::DebugColorFlags::empty(),
648            Some("Test memory safety"),
649        );
650
651        // Test that LogContext can be safely dropped
652        {
653            let context = LogContextBuilder::new(cat, LogContextFlags::empty()).build();
654            crate::info!(context, "message before drop");
655        } // context is dropped here
656
657        // Create another context to ensure no memory issues
658        let context2 = LogContextBuilder::new(cat, LogContextFlags::THROTTLE).build();
659        crate::info!(context2, "message from second context");
660    }
661
662    #[test]
663    fn log_context_category_consistency() {
664        crate::init().unwrap();
665
666        let cat1 = crate::DebugCategory::new(
667            "test-consistency-1",
668            crate::DebugColorFlags::empty(),
669            Some("Test consistency 1"),
670        );
671
672        let cat2 = crate::DebugCategory::new(
673            "test-consistency-2",
674            crate::DebugColorFlags::empty(),
675            Some("Test consistency 2"),
676        );
677
678        let context1 = LogContextBuilder::new(cat1, LogContextFlags::empty()).build();
679        let context2 = LogContextBuilder::new(cat2, LogContextFlags::empty()).build();
680
681        // Verify that contexts maintain their respective categories
682        assert_eq!(context1.category(), cat1);
683        assert_eq!(context2.category(), cat2);
684        assert_ne!(context1.category(), cat2);
685        assert_ne!(context2.category(), cat1);
686    }
687
688    #[test]
689    fn log_context_threshold_behavior() {
690        crate::init().unwrap();
691
692        let cat = crate::DebugCategory::new(
693            "test-threshold",
694            crate::DebugColorFlags::empty(),
695            Some("Test threshold behavior"),
696        );
697
698        let context = LogContextBuilder::new(cat, LogContextFlags::empty()).build();
699
700        // Test threshold behavior matches between category and context
701        cat.set_threshold(DebugLevel::Warning);
702
703        assert!(context.above_threshold(DebugLevel::Error));
704        assert!(context.above_threshold(DebugLevel::Warning));
705        assert!(!context.above_threshold(DebugLevel::Info));
706        assert!(!context.above_threshold(DebugLevel::Debug));
707
708        // Same as category
709        assert_eq!(
710            context.above_threshold(DebugLevel::Error),
711            cat.above_threshold(DebugLevel::Error)
712        );
713        assert_eq!(
714            context.above_threshold(DebugLevel::Warning),
715            cat.above_threshold(DebugLevel::Warning)
716        );
717        assert_eq!(
718            context.above_threshold(DebugLevel::Info),
719            cat.above_threshold(DebugLevel::Info)
720        );
721        assert_eq!(
722            context.above_threshold(DebugLevel::Debug),
723            cat.above_threshold(DebugLevel::Debug)
724        );
725    }
726}