danielwertheim

danielwertheim


notes from a passionate developer

Developer that lives by the mantra "code is meant to be shared".

Share


Tags


Disclaimer

This is a personal blog. The opinions expressed here represent my own and not those of my employer, nor current or previous. All content is published "as is", without warranty of any kind and I don't take any responsibility and can't be liable for any claims, damages or other liabilities that might be caused by the content.

Creating a simple key-value logger for an object graph

Daniel WertheimDaniel Wertheim

Recently I got the question if Structurizer could be used with anonymous types for flattening of object-graphs for logging. I've had the similar need, producing a key-value representation of some object graphs for the sake of logging and analytics. So lets have a look if we can build this with a few lines of code using Structurizer.

StructureLogger

In Structurizer there's something called a FlexibleStructureBuilder which upon request, generates a cached schema representation of an object passed to it. Normal POCO classes or anonymous types. You can also configure and reconfigure it to control what to index in a certain type. But that is optional. Using this we can put together a StructureLogger.

install-package structurizer  
public delegate bool IndexPredicate(  
  IStructureIndex index);

public delegate string IndexFormatter(  
  Guid logEntryId,
  IStructureIndex index);

public class StructureLogger  
{
  private readonly FlexibleStructureBuilder _builder;
  private readonly Action<Type, string[]> _writer;

  public IndexPredicate IndexPredicate { private get; set; }
  public IndexFormatter IndexFormatter { private get; set; }

  public StructureLogger(Action<Type, string[]> writer)
  {
    EnsureArg.IsNotNull(writer, nameof(writer));

    _builder = new FlexibleStructureBuilder();
    _writer = writer;

    IndexPredicate = IndexPredicates.AllButNulls;
    IndexFormatter = IndexFormatters.UsingPathAsKey;
  }

  public void Configure(
    Type structureType,
    Action<IStructureTypeConfigurator> configurator = null)
    => _builder.Configure(structureType, configurator);

  public void Configure<T>(
    Action<IStructureTypeConfigurator<T>> configurator = null)
    where T : class
    => _builder.Configure(configurator);

  public void ConfigureUsingTemplate<T>(
    T template,
    Action<IStructureTypeConfigurator<T>> configurator = null)
    where T : class
    => _builder.ConfigureUsingTemplate(configurator);

  public void Log<T>(T item) where T : class
  {
    if(item == null)
      return;

    var structure = _builder.CreateStructure(item);
    if (!structure.Indexes.Any())
      return;

    var logEntryId = Guid.NewGuid();

    _writer(
      typeof(T),
      structure.Indexes
      .Where(i => IndexPredicate(i))
      .Select(i => IndexFormatter(logEntryId, i))
      .ToArray());
  }
}

The IndexPredicate allows control of if some value should be excluded. The default one just excludes nulls.

The IndexFormatter controls how each IStructureIndex should be transformed to a string.

I've just for demo sake, produced two different ones:

public static class IndexFormatters  
{
    public static string UsingNameAsKey(Guid logEntryId, IStructureIndex index)
        => Format(logEntryId, index.Name, index);

    public static string UsingPathAsKey(Guid logEntryId, IStructureIndex index)
        => Format(logEntryId, index.Path, index);

    private static string Format(Guid logEntryId, string key, IStructureIndex index)
    {
        string row;

        switch (index.DataTypeCode)
        {
            case DataTypeCode.String:
            case DataTypeCode.Guid:
            case DataTypeCode.Enum:
                row = $"{key}=\"{index.Value}\"";
                break;
            case DataTypeCode.DateTime:
                row = $"{key}=\"{(DateTime)index.Value:O}\"";
                break;
            default:
                row = $"{key}={index.Value}";
                break;
        }

        return $"LogId={logEntryId:N} {row}";
    }
}

The LogId is just something introduced that you can use to aggregate/correlate values to in e.g. Splunk or what have you.

Usage

The usage is really straight forward. Create an instance and inject logic for where it should write stuff:

var logger = new StructureLogger((entityType, indexes) =>  
{
    //Here you can write to logs/X using NLog, Splunk client or whatever
    Console.WriteLine($"Entity: {entityType.Name}");
    foreach (var index in indexes)
        Console.WriteLine(index);
});

Now, all you need is to pass something:

logger.Log(order);  

Maybe I don't want everything of an Order logged. Well, just configure it once:

//Optional: tell exactly what to extract
logger.Configure<Order>(cfg => cfg  
    .UsingIndexMode(IndexMode.Inclusive)
    .Members(i => i.OrderNo)
    .Members(i => i.Lines[0].ArticleNo));

This would with the default formatter provide something like:

Entity: Order  
LogId=a0ac4640897948358674b45d2e218921 OrderNo="2016-1234"  
LogId=a0ac4640897948358674b45d2e218921 Lines[0].ArticleNo="Article-Line0"  
LogId=a0ac4640897948358674b45d2e218921 Lines[1].ArticleNo="Article-Line1"  

Anonymous types

Yes, that works as well:

//Supports anonymous types
logger.Log(new  
{
    Age = 37,
    Name = "Daniel"
});

//...another entry (person) using the same anonymous type.
logger.Log(new  
{
    Age = 42,
    Name = "Daniel"
});

which will produce:

Entity: <>f__AnonymousType0`2  
LogId=b16483ffa6344ae7b938cc46263d2793 Age=37  
LogId=b16483ffa6344ae7b938cc46263d2793 Name="Daniel"  
Entity: <>f__AnonymousType0`2  
LogId=e8cd9d77e1f144c3a51f6f0ea36cdabf Age=42  
LogId=e8cd9d77e1f144c3a51f6f0ea36cdabf Name="Daniel"  

That's all for this time. Now of course, ensure that your existing logging solution doesn't already support this. The post is more meant as an inspiration of what you can do with Structurizer.

Have fun,

//Daniel

Developer that lives by the mantra "code is meant to be shared".

Comments