Skip to content

Use Fluent API to control how complex types are logged to Serilog.

License

Notifications You must be signed in to change notification settings

Vazovsk1y/Serilog.FluentDestructuring

Repository files navigation

Serilog.FluentDestructuring

This package makes it possible to manipulate how complex objects are logged to Serilog using Fluent API.

Motivation

The Destructurama.Attributed package provides convenient ways to configure Serilog complex object logging by using attributes. With these, you can easily ignore some properties, apply masking and so on. But this attribute-based approach does introduce a dependency on Serilog in projects where such a dependency may be undesirable (a similar issue exists with Entity Framework Core and its attribute-based model configuring approach). This package emerged out of the need to eliminate this dependency and provide another way for the developers to configure complex objects logging using a Fluent API.

Installation

Download from NuGet

Package Manager
NuGet\Install-Package Serilog.FluentDestructuring -Version *version_number*
.NET CLI
dotnet add package Serilog.FluentDestructuring --version *version_number*

Usage

Define your custom policy and override configure method to specify what destructuring rules to use.

public class ApplicationFluentDestructuringPolicy : FluentDestructuringPolicy
{
    protected override void Configure(FluentDestructuringBuilder builder)
    {
        // Your configurations.
    }
}

Modify logger configuration.

var cfg = new LoggerConfiguration()
    .Destructure.WithFluentDestructuringPolicy<ApplicationFluentDestructuringPolicy>()
    ...

Destructuring rules applying.

Add configuration for specific entity type right here.

public class ApplicationFluentDestructuringPolicy : FluentDestructuringPolicy
{
    protected override void Configure(FluentDestructuringBuilder builder)
    {
        builder.Entity<UserRegisterRequest>(e => 
        {
            e.Property(p => p.Email)
                .Mask();
            
            e.Property(p => p.Password)
                .Ignore();
        });
    }
}

Apply predefined configuration for a specific entity.

public class UserRegisterRequestDestructuringConfiguration : IEntityDestructuringConfiguration<UserRegisterRequest>
{
    public void Configure(EntityDestructuringBuilder<UserRegisterRequest> builder)
    {
        builder.Property(p => p.Email)
            .Mask();
            
        builder.Property(p => p.Password)
            .Ignore();
    }
}

public class ApplicationFluentDestructuringPolicy : FluentDestructuringPolicy
{
    protected override void Configure(FluentDestructuringBuilder builder)
    {
        builder.ApplyConfiguration(new UserRegisterRequestDestructuringConfiguration());
    }
}

Apply all entity destructuring configurations found in a specified assembly.

public class ApplicationFluentDestructuringPolicy : FluentDestructuringPolicy
{
    protected override void Configure(FluentDestructuringBuilder builder)
    {
        builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
    }
}

1. Changing a property name.

Apply by calling WithAlias method.

builder.Entity<UserRegisterRequest>(e => 
{
    e.Property(p => p.Email)
        .WithAlias("user_email");
});

You can also use custom property name along with a main destructuring rule.

builder.Entity<UserRegisterRequest>(e => 
{
    e.Property(p => p.Email)
        .Mask()
        .WithAlias("user_email");
});

2. Ignoring a property.

Apply by calling Ignore method.

builder.Entity<UserRegisterRequest>(e => 
{
    e.Property(p => p.Password)
        .Ignore();
});

3. Logging types and properties as scalars.

Apply by calling AsScalar method.

// Whole entity.
builder.Entity<UserRegisterRequest>(e => e.AsScalar());

builder.Entity<UserRegisterRequest>(e => 
{
    // Individual property.
    e.Property(p => p.Passport)
        .AsScalar();
});

4. Masking a property.

Apply by calling Mask method.

Note that masking works for properties of type string, IEnumerable<string> or derived from it, for example, string[] or List<string>.

4.1. Default behaviour.

  • MaskCharacter: The character used for masking. The default is an asterisk *.
  • MaskLength: The length of the mask to be applied. This is the number of MaskCharacter that will be used to obfuscate the value. The default is 10.
  • PreserveValueLength: Value indicating whether the length of the original value should be preserved when applying the mask. If true, the masked value will have the same length as the original value, and the MaskLength property will be ignored. The default is false.
builder.Entity<UserRegisterRequest>(e => 
{
    // Default masking processor with default options.
    e.Property(p => p.Email)
        .Mask();
    
    // Customize default masking processor behaviour by specify options.
    e.Property(e => e.Password)
       .Mask(new DefaultMaskingProcessorOptions { PreserveValueLength = true, MaskCharacter = '#' })
});

4.2. Custom behaviour.

You can use custom masking processor by implementing the IMaskingProcessor interface and passing an instance to one of the overloads of Mask method.

public class PasswordMaskingProcessor : IMaskingProcessor
{
    public bool TryMask(string value, out string? maskedValue)
    {
        // Your implementation.
    }
}
builder.Entity<UserRegisterRequest>(e => 
{
    e.Property(e => e.Password)
       .Mask(new PasswordMaskingProcessor());
});

5. Conditional property destructuring.

You can define the condition under which the destructuring rule will be applied.

builder.Entity<UserRegisterRequest>(e => 
{
    // One of predefined conditions.
    e.Property(p => p.Email)
        .Ignore()
        .ApplyWhenNull();
    
    e.Property(e => e.Passport)
       .AsScalar()
       .WithAlias("user_passport")
       .ApplyWhenNotNull();
    
    
    // Define your custom condition.
    e.Property(p => p.Password)
        .Mask()
        .ApplyWhen(e => !string.IsNullOrWhiteSpace(e.Email) && e.Email.EndsWith("@gmail.com"));
});

6. Inner entities.

Only single-level properties are supported.

builder.Entity<UserRegisterRequest>(e => 
{
    // Will throw an exception.  
    e.Property(e => e.Passport.Series)
       .Mask();
});

Configure inner entities by calling InnerEntity method.

builder.Entity<UserRegisterRequest>(e => 
{  
    e.InnerEntity(o => o.Passport, x =>
    {
        x.Property(a => a.Series)
            .Mask();

        x.Property(a => a.Number)
            .Ignore()
            .ApplyWhenNull();
    })
    .WithAlias("user_passport")
    .ApplyWhen(e => !string.IsNullOrWhiteSpace(e.Email));
});

Or apply predefined configuration.

public class UserPassportRequestDestructuringConfiguration : IEntityDestructuringConfiguration<UserPassportRequest>
{
    public void Configure(EntityDestructuringBuilder<UserPassportRequest> builder)
    {
        builder.Property(a => a.Series)
            .Mask();

        builder.Property(a => a.Number)
            .Ignore()
            .ApplyWhenNull();
    }
}

builder.Entity<UserRegisterRequest>(e => 
{  
    e.InnerEntity(o => o.Passport, new UserPassportRequestDestructuringConfiguration())
        .WithAlias("user_passport")
        .ApplyWhen(e => !string.IsNullOrWhiteSpace(e.Email));
});

7. Global options.

  • IgnoreNullProperties - Indicating whether properties with null values should be ignored during destructuring. The default is false.
  • ExcludeTypeTag - Indicating whether the $type tag should be excluded from the destructured output. The default is false.
var cfg = new LoggerConfiguration()
    .Destructure.WithFluentDestructuringPolicy<ApplicationFluentDestructuringPolicy>(e => 
    {
        e.IgnoreNullProperties = true;
        e.ExcludeTypeTag = true;
    })
    ...

About

Use Fluent API to control how complex types are logged to Serilog.

Topics

Resources

License

Stars

Watchers

Forks