better_tracing/layer/
transform.rs

1//! Field transformation layer for zero-allocation span field manipulation.
2//!
3//! This layer intercepts field recording and applies transformations based on
4//! configurable rules, storing the transformed results in the existing
5//! `FormattedFields<N>` extension storage for zero-cost access during formatting.
6
7use crate::{
8    field::Visit,
9    fmt::{format::Writer, FormattedFields},
10    layer::{Context, Layer},
11    registry::LookupSpan,
12};
13use std::{fmt, marker::PhantomData};
14use tracing_core::{
15    field::Field,
16    span::{Attributes, Id, Record},
17    Subscriber,
18};
19
20/// A layer that transforms span fields during recording.
21///
22/// This layer sits between the registry and formatting layers, intercepting
23/// field recording to apply transformations. When no transformations are
24/// configured for a span, it has zero runtime overhead.
25///
26/// # Example
27///
28/// ```rust
29/// use better_tracing::layer::transform::FieldTransformLayer;
30/// // Bring the `SubscriberExt` trait into scope to enable `.with(...)`.
31/// use better_tracing::prelude::*;
32///
33/// let transform_layer = FieldTransformLayer::new()
34///     .with_target_transform("kube", |builder| builder
35///         .rename_field("resource_name", "k8s_resource")
36///         .hide_field("internal_token")
37///         .truncate_field("uid", 8)
38///     );
39///
40/// better_tracing::registry()
41///     .with(transform_layer)
42///     .with(better_tracing::fmt::layer())
43///     .init();
44/// ```
45#[derive(Debug)]
46pub struct FieldTransformLayer<N = ()> {
47    transforms: N,
48    _phantom: PhantomData<fn()>,
49}
50
51/// Configuration for field transformations.
52#[derive(Debug)]
53pub struct TransformConfig {
54    target_rules: Vec<TargetRule>,
55}
56
57/// Transformation rules for a specific target pattern.
58#[derive(Debug)]
59pub struct TargetRule {
60    target_pattern: &'static str,
61    field_renames: Vec<(&'static str, &'static str)>,
62    hidden_fields: Vec<&'static str>,
63    field_transforms: Vec<FieldTransform>,
64}
65
66/// A field transformation.
67#[derive(Debug)]
68pub struct FieldTransform {
69    field_name: &'static str,
70    transform_type: TransformType,
71}
72
73/// Types of field transformations.
74#[derive(Debug)]
75pub enum TransformType {
76    /// Truncate to N characters
77    Truncate(usize),
78    /// Add a static prefix
79    Prefix(&'static str),
80    /// Apply a custom transformation function
81    Custom(fn(&str) -> String),
82}
83
84impl FieldTransformLayer<()> {
85    /// Create a new field transform layer with no transformations.
86    ///
87    /// This has zero runtime cost until transformations are added.
88    pub fn new() -> Self {
89        Self {
90            transforms: (),
91            _phantom: PhantomData,
92        }
93    }
94
95    /// Add transformations for a specific target pattern.
96    pub fn with_target_transform<F>(
97        self,
98        target_pattern: &'static str,
99        builder: F,
100    ) -> FieldTransformLayer<TransformConfig>
101    where
102        F: FnOnce(TargetRuleBuilder) -> TargetRuleBuilder,
103    {
104        let rule = builder(TargetRuleBuilder::new(target_pattern)).build();
105
106        FieldTransformLayer {
107            transforms: TransformConfig {
108                target_rules: vec![rule],
109            },
110            _phantom: PhantomData,
111        }
112    }
113}
114
115impl FieldTransformLayer<TransformConfig> {
116    /// Add additional transformations for another target pattern.
117    pub fn with_target_transform<F>(mut self, target_pattern: &'static str, builder: F) -> Self
118    where
119        F: FnOnce(TargetRuleBuilder) -> TargetRuleBuilder,
120    {
121        let rule = builder(TargetRuleBuilder::new(target_pattern)).build();
122        self.transforms.target_rules.push(rule);
123        self
124    }
125}
126
127/// Builder for creating target-specific transformation rules.
128#[derive(Debug)]
129pub struct TargetRuleBuilder {
130    target_pattern: &'static str,
131    field_renames: Vec<(&'static str, &'static str)>,
132    hidden_fields: Vec<&'static str>,
133    field_transforms: Vec<FieldTransform>,
134}
135
136impl TargetRuleBuilder {
137    fn new(target_pattern: &'static str) -> Self {
138        Self {
139            target_pattern,
140            field_renames: Vec::new(),
141            hidden_fields: Vec::new(),
142            field_transforms: Vec::new(),
143        }
144    }
145
146    /// Rename a field.
147    pub fn rename_field(mut self, from: &'static str, to: &'static str) -> Self {
148        self.field_renames.push((from, to));
149        self
150    }
151
152    /// Hide a field from display.
153    pub fn hide_field(mut self, field: &'static str) -> Self {
154        self.hidden_fields.push(field);
155        self
156    }
157
158    /// Truncate a field to the specified length.
159    pub fn truncate_field(mut self, field: &'static str, max_len: usize) -> Self {
160        self.field_transforms.push(FieldTransform {
161            field_name: field,
162            transform_type: TransformType::Truncate(max_len),
163        });
164        self
165    }
166
167    /// Add a static prefix to a field.
168    pub fn prefix_field(mut self, field: &'static str, prefix: &'static str) -> Self {
169        self.field_transforms.push(FieldTransform {
170            field_name: field,
171            transform_type: TransformType::Prefix(prefix),
172        });
173        self
174    }
175
176    /// Apply a custom transformation to a field.
177    pub fn transform_field(mut self, field: &'static str, transform: fn(&str) -> String) -> Self {
178        self.field_transforms.push(FieldTransform {
179            field_name: field,
180            transform_type: TransformType::Custom(transform),
181        });
182        self
183    }
184
185    /// Build the target rule (internal method).
186    pub fn build(self) -> TargetRule {
187        TargetRule {
188            target_pattern: self.target_pattern,
189            field_renames: self.field_renames,
190            hidden_fields: self.hidden_fields,
191            field_transforms: self.field_transforms,
192        }
193    }
194}
195
196/// A field visitor that applies transformations during recording.
197struct TransformingVisitor<'a> {
198    writer: Writer<'a>,
199    rule: &'a TargetRule,
200}
201
202impl<'a> TransformingVisitor<'a> {
203    fn new(writer: Writer<'a>, rule: &'a TargetRule) -> Self {
204        Self { writer, rule }
205    }
206}
207
208impl Visit for TransformingVisitor<'_> {
209    fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
210        let field_name = field.name();
211
212        // Check if field should be hidden
213        if self
214            .rule
215            .hidden_fields
216            .iter()
217            .any(|&hidden| hidden == field_name)
218        {
219            return;
220        }
221
222        // Check for field rename
223        let display_name = self
224            .rule
225            .field_renames
226            .iter()
227            .find(|(from, _)| *from == field_name)
228            .map(|(_, to)| *to)
229            .unwrap_or(field_name);
230
231        // Check for field transformation
232        if let Some(transform) = self
233            .rule
234            .field_transforms
235            .iter()
236            .find(|t| t.field_name == field_name)
237        {
238            let value_str = format!("{:?}", value);
239            let transformed_value = match &transform.transform_type {
240                TransformType::Truncate(max_len) => {
241                    if value_str.len() > *max_len {
242                        format!("{}...", &value_str[..*max_len])
243                    } else {
244                        value_str
245                    }
246                }
247                TransformType::Prefix(prefix) => {
248                    format!("{} {}", prefix, value_str)
249                }
250                TransformType::Custom(func) => func(&value_str),
251            };
252            let _ = write!(self.writer, "{}={}", display_name, transformed_value);
253        } else {
254            let _ = write!(self.writer, "{}={:?}", display_name, value);
255        }
256    }
257
258    fn record_str(&mut self, field: &Field, value: &str) {
259        let field_name = field.name();
260
261        // Check if field should be hidden
262        if self
263            .rule
264            .hidden_fields
265            .iter()
266            .any(|&hidden| hidden == field_name)
267        {
268            return;
269        }
270
271        // Check for field rename
272        let display_name = self
273            .rule
274            .field_renames
275            .iter()
276            .find(|(from, _)| *from == field_name)
277            .map(|(_, to)| *to)
278            .unwrap_or(field_name);
279
280        // Check for field transformation
281        if let Some(transform) = self
282            .rule
283            .field_transforms
284            .iter()
285            .find(|t| t.field_name == field_name)
286        {
287            let transformed_value = match &transform.transform_type {
288                TransformType::Truncate(max_len) => {
289                    if value.len() > *max_len {
290                        format!("{}...", &value[..*max_len])
291                    } else {
292                        value.to_string()
293                    }
294                }
295                TransformType::Prefix(prefix) => {
296                    format!("{} {}", prefix, value)
297                }
298                TransformType::Custom(func) => func(value),
299            };
300            let _ = write!(self.writer, "{}={}", display_name, transformed_value);
301        } else {
302            let _ = write!(self.writer, "{}={}", display_name, value);
303        }
304    }
305
306    fn record_i64(&mut self, field: &Field, value: i64) {
307        let field_name = field.name();
308        if !self
309            .rule
310            .hidden_fields
311            .iter()
312            .any(|&hidden| hidden == field_name)
313        {
314            let display_name = self
315                .rule
316                .field_renames
317                .iter()
318                .find(|(from, _)| *from == field_name)
319                .map(|(_, to)| *to)
320                .unwrap_or(field_name);
321            let _ = write!(self.writer, "{}={}", display_name, value);
322        }
323    }
324
325    fn record_u64(&mut self, field: &Field, value: u64) {
326        let field_name = field.name();
327        if !self
328            .rule
329            .hidden_fields
330            .iter()
331            .any(|&hidden| hidden == field_name)
332        {
333            let display_name = self
334                .rule
335                .field_renames
336                .iter()
337                .find(|(from, _)| *from == field_name)
338                .map(|(_, to)| *to)
339                .unwrap_or(field_name);
340            let _ = write!(self.writer, "{}={}", display_name, value);
341        }
342    }
343
344    fn record_f64(&mut self, field: &Field, value: f64) {
345        let field_name = field.name();
346        if !self
347            .rule
348            .hidden_fields
349            .iter()
350            .any(|&hidden| hidden == field_name)
351        {
352            let display_name = self
353                .rule
354                .field_renames
355                .iter()
356                .find(|(from, _)| *from == field_name)
357                .map(|(_, to)| *to)
358                .unwrap_or(field_name);
359            let _ = write!(self.writer, "{}={}", display_name, value);
360        }
361    }
362
363    fn record_bool(&mut self, field: &Field, value: bool) {
364        let field_name = field.name();
365        if !self
366            .rule
367            .hidden_fields
368            .iter()
369            .any(|&hidden| hidden == field_name)
370        {
371            let display_name = self
372                .rule
373                .field_renames
374                .iter()
375                .find(|(from, _)| *from == field_name)
376                .map(|(_, to)| *to)
377                .unwrap_or(field_name);
378            let _ = write!(self.writer, "{}={}", display_name, value);
379        }
380    }
381}
382
383// Zero-cost implementation when no transforms are configured
384impl<S> Layer<S> for FieldTransformLayer<()>
385where
386    S: Subscriber + for<'a> LookupSpan<'a>,
387{
388    // All methods are no-ops, ensuring zero cost when no transforms are configured
389}
390
391impl<S> Layer<S> for FieldTransformLayer<TransformConfig>
392where
393    S: Subscriber + for<'a> LookupSpan<'a>,
394{
395    fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
396        // Check if any rule matches this span's target
397        let target = attrs.metadata().target();
398
399        if let Some(rule) = self
400            .transforms
401            .target_rules
402            .iter()
403            .find(|rule| target.contains(rule.target_pattern))
404        {
405            // Apply transformations to this span's fields
406            if let Some(span) = ctx.span(id) {
407                let mut extensions = span.extensions_mut();
408
409                // Create a new FormattedFields with transformed content
410                let mut fields = FormattedFields::<TransformConfig>::new(String::new());
411                let mut visitor = TransformingVisitor::new(fields.as_writer(), rule);
412                attrs.record(&mut visitor);
413
414                // Store the transformed fields
415                extensions.insert(fields);
416            }
417        }
418    }
419
420    fn on_record(&self, id: &Id, values: &Record<'_>, ctx: Context<'_, S>) {
421        if let Some(span) = ctx.span(id) {
422            let target = span.metadata().target();
423
424            if let Some(rule) = self
425                .transforms
426                .target_rules
427                .iter()
428                .find(|rule| target.contains(rule.target_pattern))
429            {
430                let mut extensions = span.extensions_mut();
431
432                if let Some(fields) = extensions.get_mut::<FormattedFields<TransformConfig>>() {
433                    // Append transformed fields to existing ones
434                    if !fields.fields.is_empty() {
435                        fields.fields.push(' ');
436                    }
437                    let mut visitor = TransformingVisitor::new(fields.as_writer(), rule);
438                    values.record(&mut visitor);
439                } else {
440                    // Create new transformed fields
441                    let mut fields = FormattedFields::<TransformConfig>::new(String::new());
442                    let mut visitor = TransformingVisitor::new(fields.as_writer(), rule);
443                    values.record(&mut visitor);
444                    extensions.insert(fields);
445                }
446            }
447        }
448    }
449}
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454    use crate::{layer::SubscriberExt, registry::Registry};
455    use tracing::{span, Level};
456
457    #[test]
458    fn test_zero_cost_when_no_transforms() {
459        // Verify that the layer has zero cost when no transformations are configured
460        let layer = FieldTransformLayer::new();
461
462        // This should compile and have no runtime overhead
463        let subscriber = Registry::default().with(layer);
464
465        // Basic smoke test - ensure it doesn't panic
466        tracing::subscriber::with_default(subscriber, || {
467            let span = span!(Level::INFO, "test_span", field1 = "value1");
468            let _guard = span.enter();
469        });
470    }
471
472    #[test]
473    fn test_layer_creation_and_configuration() {
474        // Test that the builder pattern works correctly
475        let layer = FieldTransformLayer::new()
476            .with_target_transform("kube", |builder| {
477                builder
478                    .rename_field("resource_name", "k8s_resource")
479                    .hide_field("internal_token")
480                    .truncate_field("uid", 8)
481                    .prefix_field("status", "🎯")
482                    .transform_field("phase", |value| match value {
483                        "\"Running\"" => "✅ Running".to_string(),
484                        "\"Failed\"" => "❌ Failed".to_string(),
485                        other => other.to_string(),
486                    })
487            })
488            .with_target_transform("http", |builder| {
489                builder
490                    .rename_field("method", "http_method")
491                    .truncate_field("url", 50)
492            });
493
494        // Verify the configuration was built correctly
495        assert_eq!(layer.transforms.target_rules.len(), 2);
496
497        let kube_rule = &layer.transforms.target_rules[0];
498        assert_eq!(kube_rule.target_pattern, "kube");
499        assert_eq!(kube_rule.field_renames.len(), 1);
500        assert_eq!(
501            kube_rule.field_renames[0],
502            ("resource_name", "k8s_resource")
503        );
504        assert_eq!(kube_rule.hidden_fields.len(), 1);
505        assert_eq!(kube_rule.hidden_fields[0], "internal_token");
506        assert_eq!(kube_rule.field_transforms.len(), 3);
507
508        let http_rule = &layer.transforms.target_rules[1];
509        assert_eq!(http_rule.target_pattern, "http");
510        assert_eq!(http_rule.field_renames.len(), 1);
511        assert_eq!(http_rule.field_renames[0], ("method", "http_method"));
512    }
513
514    #[test]
515    fn test_target_rule_builder() {
516        // Test the builder pattern for target rules
517        let builder = TargetRuleBuilder::new("test_target");
518        let rule = builder
519            .rename_field("old", "new")
520            .hide_field("secret")
521            .truncate_field("long", 10)
522            .prefix_field("status", "🎯")
523            .transform_field("custom", |v| v.to_uppercase())
524            .build();
525
526        assert_eq!(rule.target_pattern, "test_target");
527        assert_eq!(rule.field_renames.len(), 1);
528        assert_eq!(rule.field_renames[0], ("old", "new"));
529        assert_eq!(rule.hidden_fields.len(), 1);
530        assert_eq!(rule.hidden_fields[0], "secret");
531        assert_eq!(rule.field_transforms.len(), 3);
532
533        // Test transform types
534        assert_eq!(rule.field_transforms[0].field_name, "long");
535        assert_eq!(rule.field_transforms[1].field_name, "status");
536        assert_eq!(rule.field_transforms[2].field_name, "custom");
537
538        match &rule.field_transforms[0].transform_type {
539            TransformType::Truncate(n) => assert_eq!(*n, 10),
540            _ => panic!("Expected Truncate transform"),
541        }
542
543        match &rule.field_transforms[1].transform_type {
544            TransformType::Prefix(p) => assert_eq!(*p, "🎯"),
545            _ => panic!("Expected Prefix transform"),
546        }
547
548        match &rule.field_transforms[2].transform_type {
549            TransformType::Custom(_) => {} // Can't test function equality
550            _ => panic!("Expected Custom transform"),
551        }
552    }
553
554    #[test]
555    fn test_transform_types() {
556        // Test truncation logic
557        let value = "this_is_a_very_long_string";
558        let truncated = if value.len() > 10 {
559            format!("{}...", &value[..10])
560        } else {
561            value.to_string()
562        };
563        assert_eq!(truncated, "this_is_a_...");
564
565        // Test prefix logic
566        let prefixed = format!("🎯 {}", "test_value");
567        assert_eq!(prefixed, "🎯 test_value");
568
569        // Test custom transform
570        let custom_transform = |value: &str| match value {
571            "running" => "✅ Running".to_string(),
572            "failed" => "❌ Failed".to_string(),
573            other => other.to_string(),
574        };
575        assert_eq!(custom_transform("running"), "✅ Running");
576        assert_eq!(custom_transform("failed"), "❌ Failed");
577        assert_eq!(custom_transform("other"), "other");
578    }
579
580    #[test]
581    fn test_integration_with_registry() {
582        // Test that the layer properly integrates with the registry
583        let layer = FieldTransformLayer::new().with_target_transform("test_target", |builder| {
584            builder
585                .rename_field("field1", "renamed_field1")
586                .hide_field("secret")
587        });
588
589        let subscriber = Registry::default().with(layer);
590
591        // This should not panic and should work end-to-end
592        tracing::subscriber::with_default(subscriber, || {
593            let span = tracing::span!(
594                target: "test_target",
595                Level::INFO,
596                "test_span",
597                field1 = "value1",
598                secret = "hidden_value",
599                visible = "visible_value"
600            );
601            let _guard = span.enter();
602
603            // Test recording additional fields
604            span.record("field2", &"value2");
605        });
606    }
607
608    #[test]
609    fn test_multiple_layer_composition() {
610        // Test that transform layers can be composed with other layers
611        let transform_layer = FieldTransformLayer::new().with_target_transform("app", |builder| {
612            builder
613                .rename_field("user_id", "uid")
614                .hide_field("password")
615        });
616
617        let fmt_layer = crate::fmt::layer().with_target(true).with_level(true);
618
619        let subscriber = Registry::default().with(transform_layer).with(fmt_layer);
620
621        // Should compose properly without panic
622        tracing::subscriber::with_default(subscriber, || {
623            let span = tracing::span!(
624                target: "app::auth",
625                Level::INFO,
626                "login",
627                user_id = 12345,
628                password = "secret123",
629                method = "oauth"
630            );
631            let _guard = span.enter();
632        });
633    }
634
635    #[test]
636    fn test_no_allocation_when_no_match() {
637        // Test that no work is done when target doesn't match
638        let layer = FieldTransformLayer::new()
639            .with_target_transform("specific_target", |builder| {
640                builder.rename_field("field", "renamed")
641            });
642
643        let subscriber = Registry::default().with(layer);
644
645        tracing::subscriber::with_default(subscriber, || {
646            // This span should not trigger any transformations
647            let span = tracing::span!(
648                target: "different_target",
649                Level::INFO,
650                "test_span",
651                field = "value"
652            );
653            let _guard = span.enter();
654        });
655    }
656}