Traceable Layer

The Traceable Layer consists of several decorator objects which have these requirements:

  1. There should be a one to one correspondence between a class in the Functional Layer and the Traceable Layer.
  2. The traceable class must directly derive from the functional class it is mimicking.
  3. The Traceable Layer must never have business logic.
  4. Factories must be used to generate any new instances of a class.
  5. Auto-generated code should be used create most traceable classes.
  6. The Traceable Layer should be kept in a separate library from the Functional Layer.

The one-to-one ratio is fairly straightforward: if we want to track what is going inside of a class in the Functional Layer, then we want to make a corresponding class in the Traceable Layer. What is less straightforward is that any class in the Traceable Layer may only inherit directly from the corresponding Functional Layer. This requirement ensures that the Traceable class is able to take the place of the Functional class with no issues. The idea behind this is that then a Traceable class may be introduced at any point, and there is no fear its presence will will cause any disruption in the service.

Making sure that no new logic is introduced in the Traceable Layer is of the utmost importance. The purpose of the Traceable Layer is to give us visibility into what is happening inside of the Functional Layer. If we add new logic in the Traceable Layer, we no longer have an accurate view. See Single Responsibility Principle for more details.

By using factories we are able to greatly reduce the complexity of maintaining the Traceable Layer. See Factories for more details.

Finally, by keeping the code of the Traceable Layer in a separate library, we are better able to ensure that the Traceable Layer is not accidentally used in a performance critical environment. Simply put, if the Traceable library is not present on the machine, we know it is not being used.

Trace Data

The data the gathered from the Traceable Layer is referred to as Trace Data. I have found that modeling the data as XML nodes is a very useful representation. Each function is its own node, with any internal function calls being represented as a child node.

One significant deviation from standard XML is that attributes are used to show results. This way a collapsed node shows the function call and returned values. Any parameters instead will be the first child nodes contained function node.

The Tracer provides more details on how the specific calls below will handle the data.

Constructors

The interface of a Traceable Layer class will only differ from the interface of the corresponding Functional Layer class in one way: all of the constructors in the Traceable class will take an additional parameter: the Tracer instance. For example: namespace FunctionalLayer.Objects { public class Example { ... public Example() { ... } Public Example(DataStream dataStream) { ... } } } namespace TraceableLayer.Objects { public class Example : FunctionalLayer.Objects.Example { private Tracer _tracer; ... public Example(Tracer tracer) { _tracer = tracer; } Public Example(DataStream dataStream, Tracer tracer) : base(dataStream) { _tracer = tracer; } } }

This single difference is why we will use Factories to create everything. The factories will allow us to to switch between using Functional classes and Traceable classes transparently since their object construction calls will appear the same.

Tracing Function Calls

If we look at a function call as a few discrete steps, it becomes very easy to intercept and store important information about the function. The specific steps are:

When we trace a function call, add a few steps into it:

Traceable Function Call

Here is a more concrete example. We have a class used to search for customer data, and it has a function that locates the customer based on their last and first name. namespace FunctionalLayer.Customers { public class CustomerSearch { ... public virtual CustomerData FindCustomerByName(string firstName, string lastName) { ... return customerData; } } }

The Traceable version of the class stores the class name and function name, stores the parameters, and then calls the base function. Finally it stores the results that are returned, and then actually returnes the results. namespace TraceableLayer.Customers { public class CustomerSearch : FunctionalLayer.Customers { private Tracer _tracer; ... public override CustomerData FindCustomerByName(string lastName, string firstName) { _tracer.OpenNode("CustomerSearch.FindCustomerByName"); _tracer.AddNode("lastName", lastName); _tracer.AddNode("firstName", firstName); var result = base.FindCustomerByName(lastName, firstName); _tracer.AddAttribute("result", result); _tracer.CloseNode(); return result; } } }

The trace data that results from this call looks like this: <CustomerSearch.FindCustomerByName result="Smith,John"> <lastName>Smith</lastName> <firstName>John</firstName> ... additional trace data </CustomerSearch.FindCustomerByName>

It should be noted that the trace data does not display the actual logic in the function. But that is not a problem. Since we have the class and function name, we are able to easily look up the code that is being called. The values in the trace data show us the values being used in the code. Since we know where to find the code, and the values being used in it, we are able to look at the code, and verify it visually.

Tracing Properties

Tracing properties is very similar to tracing functions; this should not come as to much as surprise, since defining a property is actually just syntactic shorthand that defines a getter and setter function with a common name.

From an external view, we can do two things with a property: read a property, or write to a property. The Traceable Layer is primarily concerned with intercepting the read component, since that allows us to store the values when the property is used. Traceable Properties

We can use the CustomerData class from the prior example as a way to show Traceable properties. namespace FunctionalLayer.Customers { public class CustomerData { public virtual string LastName { get; set; } public virtual string FirstName { get; set; } ... } }

The Traceable class intercepts the reads, and stores the class name, property name, and property value in the trace data. namespace TraceableLayer.Customers { public class CustomerData : FunctionalLayer.Customers { private Tracer _tracer; public override string LastName { get { var result = base.LastName; _tracer.AddNode("CustomerData.LastName", result); return result; } set; } public override string FirstName { get { var result = base.FirstName; _tracer.AddNode("CustomerData.LastName", result); return result; } set; } } }

Anytime LastName was requested in the code, the following node would be added to the Trace data. <CustomerData.LastName>Smith</CustomerData.LastName>

Tracing Memoizing Properties

In some cases, a property may memoize the data. Prior to memoizing the data, it would need to detect that the data is not present, and then request it. One way to handle it is to treat the property as a function.

namespace FunctionalLayer.BusinessLogic { public class Example { private decimal? _expensiveCall; public virtual decimal ExpensiveCall { get { if (!_expensiveCall.HasValue) { _expensiveCall = RetrieveData(); } return _expensiveCall.Value; } } protected virtual decimal RetrieveData() { ... } } }

namespace TraceableLayer.BusinessLogic { public class Example : FunctionalLayer.BusinessLogic { private Tracer _tracer; public override decimal ExpensiveCall { get { _tracer.OpenNode("Example.ExpensiveCall"); var result = base.ExpensiveCall; _tracer.AddAttribute("result", result); _tracer.CloseNode(); return result; } } protected override decimal RetrieveData() { _tracer.OpenNode("Example.RetrieveData"); var result = base.RetrieveData(); _tracer.AddAttribute("result", result); return result; } } }

Tracing Copies

Copying an instance has the potential to generate several lines of trace data; and most likely this extra data is unnecessary. One way to handle this extra data is to simply remove it. However, you may still want to at least add a single line to the trace data so that you can see that a copy did occur. This way, you can detect situations where perhaps copying is occurring too much, or should not even be occurring at all. namespace FunctionalLayer.Customers { public class CustomerData { ... public virtual CustomerData Copy() { var to = new CustomerData { ... copy data ... }; return to; } } } namespace TraceableLayer.Customers { public class CustomerData : FunctionalLayer.Customers.CustomerData { Tracer _tracer; ... public override CustomerData Copy() { _tracer.OpenNode("IgnoreCopy"); var value = base.Copy(); _tracer.CloseAndRemoveNode(); _tracer.AddNode("CustomerData.Copy", TraceString()); } public string TraceString() { return base.LastName + "," + base.FirstName + "(" + base.CustomerId + ")"; } } }

The output of the copy would look this: <CustomerData.Copy>Smith,John(C123-723J6Q)</CustomerData.Copy>

Next: Traceable Layer Return Types