Tracer Class

The concept of the Tracer is rather straightforward: it is a central object that collects all of the trace data generated by the service call. Specifically, the Tracer is responsible for storing the following information:

  1. The class and method name of each method call.
  2. The name and value of each parameter of a method call.
  3. The value returned by the method call.
  4. The class name, property name, and value of any property.

To facilitate this collection, a Tracer instance is created, and attached to all of the Traceable Layer objects that are being used for the duration of the service call. Once the service call is completed, the trace data may be returned alongside the rest of the standard output.

Note that the singleton pattern is not used here. Instead we construct an instance, and pass that instance to all of the factories that will be used to generate all of the necessary objects to execute the service call (see Factories for me details). Since we avoid using a singleton, we may have multiple service calls being traced at the same time, each with its own Tracer instance.

The design of the Tracer is based on XML. When a function is traced, the function's class and name will be used as the tag. The result of the function will appear as a result attribute in the opening node. All of the parameters passed into the function, any function call made inside of the function, as well as any properties used inside of the function will all be sub-nodes inside of the primary function's node. If we take the trace data and collapse it, we might see something like this: <ExampleClass.GetSmallestValue result="16">...

Design Note: we want both the class name and function name (or property name) to be in the tag so that we may easily go determine the specific code that was called. If you only put in the function or property name, and then three different classes all using the same name, then whomever is looking at the trace data would have to spend time determining which of the actual classes was used in the call. Someone experienced with the code may be able to surmise the correct answer very quickly, but someone less experienced will take more time, and possibly make a mistake, and then waste even more time.

By expanding that node, we can see the parameters of the function, as well as any function calls or properties it used. We can now take these values and compare them agains the logic in our code to see if we are getting expected results. Note that the parameter tag only has the name of the parameter in it, and not the function name or class. Since we know what function we are in, adding anything to the parameter tag is simply redundant. <ExampleClass.GetSmallestValue result="16"> <scale>2</scale> <ExampleClass.Calculation1 result="10">... <ExampleClass.Calculation2 result="8">... <ExampleClass.Calculation3 result="13">... </ExampleClass.GetSmallestValue>

If we want even more detail, we can also expand one or more of the sub-calls to see the values used inside of them. <ExampleClass.GetSmallestValue result="16"> <scale>2</scale> <ExampleClass.Calculation1 result="10"> <ExampleClass._m1>2</ExampleClass._m1> <ExampleClass._x1>3</ExampleClass._x1> <ExampleClass._b1>4</ExampleClass._b1> </ExampleClass.Calculation1> <ExampleClass.Calculation2 result="8"> <ExampleClass._m2>4</ExampleClass._m2> <ExampleClass._x2>2</ExampleClass._x2> <ExampleClass._b2>0</ExampleClass._b2> </ExampleClass.Calculation2> <ExampleClass.Calculation3 result="13"> <ExampleClass._m3>1</ExampleClass._m3> <ExampleClass._x3>6</ExampleClass._x3> <ExampleClass._b3>7</ExampleClass._b3> </ExampleClass.Calculation3> </ExampleClass.GetSmallestValue>

One of the subtle (and intended) effect of this design is that all of the calls and values are displayed in the exact order they were used in the Functional Layer. This means we can trace through the code and be assured what exactly was happening at what point in the code.

A side benefit of this effect is that should a null reference exception be thrown, we would have a list of all the valid references leading up to the null reference. This helped troubleshooting immensely when we had a case where the stack trace simply indicated a null reference existed inside of a function, but did not indicate a specific object or line number.

Tracer Interface

The Tracer class needs to implement several functions to help gather the trace data. Below is a minimal list of what needs to be implemented. You may find several additional functions to be useful; for example AddNode() could be overloaded to take an object in the val parameter and simply convert that to the string value.

Tracer(string tag)

Creates a new Tracer instance with a named root node.

Post State: <rootNode> // current insertion point of new data </rootNode>

NewNode(string tag)

Creates a new node inside the current node, and makes it the current node. This should be called when starting to trace a function.

Prior State: <parentNode> // current insertion point of new data </parentNode>

Post State: <parentNode> <newNode> // current insertion point of new data </newNode> </parentNode>

CloseNode()

Closes the current node, and makes the parent node the active node. This is called when a function is no longer being traced.

Prior State: <parentNode> <innerNode> // current insertion point of new data </innerNode> </parentNode>

Post State: <parentNode> <innerNode> // ... </innerNode> // current insertion point of new data </parentNode>

CloseAndRemoveNode()

Closes the current node, makes the parent node the active node, and then removes the current node. This is useful in cases spots where objects are copied, or other spots where the additional data would be chaff.

Prior State: <parentNode> <innerNode> // current insertion point of new data </innerNode> </parentNode>

Post State: <parentNode> // current insertion point of new data </parentNode>

AddAttribute(string attribute, string val)

Adds an attribute to the current node. For storing the result of the function call, this is called after completing a function call, but prior prior to CloseNode(). Adding other attributes should be done very sparingly, otherwise the trace data becomes cluttered and thus harder to read.

Prior State: <currentNode> // current insertion point of new data </currentNode>

Post State: <currentNode result="value"> // current insertion point of new data </currentNode>

AddNode(string tag, string val)

Adds a complete sub node to the current node. This is useful for adding parameter and property values into the trace data.

Prior State: <currentNode> // current insertion point of new data </currentNode>

Post State: <currentNode> <newNode>value</newNode> // current insertion point of new data </currentNode>

Root()

Forces the Tracer to return to the root node. This is useful when handling an exception. The trace info may then be output and we are able to see all of the trace info prior to the exception throw.

Prior State: <rootNode> <innerNodes> <innerInnnerNodes> ... // current insertion point of new data </innerInnnerNodes> </innerNodes> </rootNode>

Post State: <rootNode> <innerNodes> ... </innerNodes> // current insertion point of new data </rootNode>

ToXml()

Converts the trace data into a string containing formatted xml.

Next: Factories