Client Synergy

Introduction

As the product overview page explains, the client components of AspectJS automate the use of the AJS object in well-defined ways, and in many cases you would use each client independently of the others. However, as this page shows, you can use AJS_Logger and AJS_Validator in concert, which demonstrates in turn the high degree of modularity that method-call interception facilitates.

Given this, and given that such a use-case involves both of those clients, the following exposition does not belong wholly in either of their user guides, and so it is given here in a page of its own. Presenting the arguments below in a dedicated page also acts as an excellent showcase for the technology.

Contents Introduction
The Ideal
Principle
Custom-Stream Anatomy
Reporting Method-Return Values
Taking it Further

The Ideal

A powerful application of AJS_Logger is to include it in a deployed application in order to conduct logging and profiling as real users interact with that system. Alternatively, you can deploy an application that includes AJS_Validator, which allows you to trap and diagnose run-time errors remotely; one might call this 'debugging in the wild'. However, JavaScript run-times do not include function arguments in the stack property that their native exception-objects possess. At best, each stack entry gives only the function name, and the file in which that function resides, along with the relevant line and column numbers.

It follows that including argument values for the entries in a given stack trace would be especially useful, as rather than knowing simply the path to the exceptional state, we could understand what happened along that path. However, we do not want a record of all call chains from the time of application instantiation, because most of that information would be entirely redundant. That is: the bulk of it would document entirely successful call chains, where we want only the traces for failed call-chains, along with the rich diagnostic information that AJS_Validator provides, as these data will enable us to reconstruct the steps that led to the exceptional condition, thus expediting defect resolution.

One might liken this to the concept of the cockpit voice recorders that modern passenger aircraft are required to carry, and which record the sounds in the cockpit for only the last 30 minutes that precede some calamity.

Principle

Happily, we can achieve this ideal rather easily by using AJS_Validator and AJS_Logger in conjunction. The technique pivots upon the fact that AJS_Validator delegates the handling of exceptional conditions to an externally-defined function, and that you can cause a logging object to delegate the recording of method-entry/-return to a custom stream-object.

That is to say: the AspectJS distribution set includes two functions that you can use as default exception handlers, but there is nothing to stop you providing a reference to your own function that does whatever you want when an exceptional condition arises. Similarly, you can furnish a logging object, upon creation, with a stream object of any specification, as long as it supports just two methods: onMethodEntry and onMethodExit.

It follows that, if you implement a stream object where onMethodEntry records the arguments passed-to method calls, then you can pass an exception handler to createAJS_Validator that, when called, will cause that stream-object to send the stack of argument-sets back to the server. Those data can include the detailed exception notification that AJS_Validator generates, thus giving both the steps that led up to the problem, and an exacting portrait of the 'crash site' (to re-visit the aircraft analogy).

Example 1 shows the idea in action (but defers the definition of the stream object to Example 2 below, in order to maximise clarity).

The first line in the sequence creates the custom stream-object, and the next few statements simply set up AJS_Validator and create a logger-object factory, before creating a logging object, to which is passed the stream object. Essentially, this is all standard stuff that the relevant user guides on this site cover in detail. But the central point is that the exception handler that is passed to createAJS_Validator is the onException method of the stream object. This is the 'bridge' between AJS_Validator and the logger, which gives the former control over the behaviour of the latter, and is the means by which we achieve synergy.

Lines 12 to 36 of the code set up three objects that do nothing of real interest (in order to clarify the demonstration), but where each possesses a method, such that someMethod calls someOtherMethod, which calls yetAnotherMethod in turn. someMethod passes the arguments it receives directly to someOtherMethod, but that function passes a boolean literal as the third argument to MyThirdObj's method rather than the object that it received as its third argument.

This is a critical point, as the next part of the listing – lines 39 to 51 – create and apply a ValidationDef to that method. In more conventional terms, it gives AJS_Validator a packet of instructions that tell that client what classes of argument are allowed for yetAnotherMethod (note that, as the AJS_Validator user-guide shows, ValidationDefs can be considerably more stringent and far reaching than in this simple example). The key issue here is that the ValidationDef's CallDef states that only values of class Object may be passed as the third argument to yetAnotherMethod.

Finally, lines 53 to 56 tell the logging object to log calls to the methods of the three test objects, and then line 58 invokes MyObj.someMethod. This seeds the call chain that builds from someMethod to yetAnotherMethod, where the stream maintains a record of the values passed at each stage. Subsequently, when AJS_Validator detects the erroneous 'true' value passed to yetAnotherMethod, it calls the onException function it was passed in line 06.

We assume here that the stream object would use, say, an XHR object to report the data back to the server (although, for the sake of simplicity, the example logs the output to the console), and so the output section of the example shows the text that we would receive. As prescribed above, it yields a record of the call to someMethod, replete with argument values, then a record of the call to someOtherMethod, and then the detailed exception message from AJS_Validator, which states the nature of the contravention, and which includes file, line and column information.

The upshot is that you could be in Vancouver, with the client machine in Ascension Island, and you would be able immediately, given this information, to reconstruct the scenario, and thus diagnose and resolve the underlying problem. Meanwhile the code under study – in the example, the structure and contents of the three test objects – would remain unperturbed, allowing us to dispense trivially with all the instrumentation before re-deploying our application.

To acheive that, you would need only to ensure that all validation and logging-related statements resided in files of their own (i.e. files that were devoid of any application-related code), as this would allow you to elide those sources from the code base that went out to the client.

 01
 02  // -- Example 1 --------------------------------------------------------------
 03
 04  var Stream           = createERStream          ("OnRemoteException.php");                   // Create the exception-report stream object.
 05                                                                                              // telling it where to send its reports.
 06  var AJS_Validator    = createAJS_Validator     (AJS, createAJS_DefMgr, Stream.onException); // Create AJS_Validator, telling it which
 07                                                                                              // function to call when an exception occurs.
 08  var createAJS_Logger = createAJS_LoggerFactory (AJS, createAJS_DefMgr, throwException);     // Create the logger factory...
 09  var Logger           = createAJS_Logger        (Stream);                                    // ...And create the logger, passing it the
 10                                                                                              // custom stream object.
 11  //------------------------------------------------------------
 12
 13  var MyObj =                                                                                 // Set up three objects, which we assume do
 14     {                                                                                        // something of interest.
 15     someMethod : function (Num, Str, Obj)
 16        {
 17        MyOtherObj.someOtherMethod (Num, Str, Obj);
 18        }
 19
 20     };
 21
 22  var MyOtherObj =
 23     {
 24     someOtherMethod : function (Num, Str, Obj)
 25        {
 26        MyThirdObj.yetAnotherMethod (Num, Str, true);                                         // A deliberate bug here, inject a boolean
 27        }                                                                                     // literal so as to order to contravene the
 28                                                                                              // ValidationDef defined below.
 29     };
 30
 31  var MyThirdObj =
 32     {
 33     yetAnotherMethod : function (Num, Str, Obj) {  }
 34     };
 36
 37  //------------------------------------------------------------
 38
 39  AJS_Validator.applyValidationDef (MyThirdObj,                                               // Apply validation to the yetAnotherMethod
 40     {                                                                                        // property of MyThirdObject.
 41     CallDef :
 42        [
 43        { AllowClasses : ['NumberLiteral'] },                                                 // First argument must be a literal number.
 44        { AllowClasses : ['StringLiteral'] },                                                 // Second must be a literal string.
 46        { AllowClasses : ['Object'       ] }                                                  // Third must be an object of some form
 47        ],
 48
 49     RtnDef : {  }                                                                            // And the method must return nothing.
 50
 51     });
 52
 53  Logger.applyLogDef (MyObj,           { });                                                  // Apply logging to the methods in the three
 54  Logger.applyLogDef (MyOtherObj,      { });                                                  // objects defined above.
 56  Logger.applyLogDef (MyThirdObj,      { });
 57
 58  MyObj .someMethod  (42, "Forty Two", { });                                                  // And light the blue touch-paper.

   -- Output --------------------------------------------------------------------

   someMethod: 42, Forty Two, [object Object]
   someOtherMethod: 42, Forty Two, [object Object]
   AJS_Validator_Exception: argument 2 (Obj) in call to yetAnotherMethod is of
   class BooleanLiteral (value: true), which is not included in the corresponding
   ArgDef's AllowClasses property in the corresponding ValidationDef. Permissible
   classes are: Object. (Object's AJS_Validator_Tag : undefined, Method-Owner's
   Class: Object, Method-Owner's AJS_Validator_Tag: undefined).

   Exception occurred during execution of MyOtherObj.someOtherMethod, at line 26,
   column 7, in //localhost/Test/Example_1.js
            

Custom-Stream Anatomy

So far, so good, but the business-end of the equation is the composition and modus operandi of the custom stream-object.

Example 2 dishes the dirt, and shows a function that accepts a string argument representing the URL to which exception-report data should be sent. This is the function that is called at line 04 in Example 1 above, and it instantiates two inner functions, where the first exists to send data back to the server (but which, in order to clarify the demonstration, simply echoes the data to the console object, as pointed out above).

Whn called, createExcpStream creates a closure by returning an object with three methods. This is the stream object itself, which posseses the onMethodEntry and onMethodExit methods that AJS_Logger objects require, along with a method called onException. This is the exception handler function that is passed to createAJS_Validator on line 06 in Example 1. The closure encapsulates an array called Stack, and it is to this that onMethodEntry pushes an array that contains the method name for a given call, along with the values for any arguments that were passed.

As one method calls another, the Stack array grows by one more member and, when a given method returns, its entry is popped from that array by onMethodExit. Note that a reference to that popped entry is retained temporarily by means of the MostRecentlyPopped reference, and this small but important feature is explored below.

A call to onException causes that function to invoke a function called throwException (one of the default exception handlers mentioned above), which throws an exception object that contains detailed information about the nature of the problem. onException catches that object, creates a string called Data, and then iterates through the members of Stack, calling serialiseStackEntry for each member, passing that inner function the stack-member in question.

As pointed out above, a Stack member is itself an array, and so serialiseStackEntry simply iterates through the array members, concatenating the values to form a string (using a little conditional logic to emplace commas where appropriate). It then returns that string to onException, which concatenates that and a new-line on to the end of its Data string.

When the iteration through Stack's members terminates, onException concatenates the exception message generated by the call to throwException, and then passes the resulting string to reportException.

That, in the main, is how we achieve the ideal outlined above. But a few minor issues remain, the first being that you might suggest using JSON.stringify to serialise the Stack members. In fact, this would not work because stringify elides function references, and performs other conversions, which is not the behaviour we desire. In principle, you could provide stringify with a 'replacer' argument in order to work around this, but that approach would yield code that was no simpler than that presented in Example 2. Moreover, by implementing our own stringification logic, we give ourselves the latitude to include all sort of proprietary information in the final report, which would be tricky if not impossible when using the JSON object.


 // -- Example 2 --------------------------------------------------------------

 function createERStream (URL)
    {
    function reportException (Method, URL, Data)
       {
       console.log (Data);                                                                      // Assume a sequence here that dispatches a
       }                                                                                        // packet of data to the server using an
                                                                                                // XHR object or a WebSocket.
    //----------------------------------------------

    function serialiseStackEntry (Entry)
       {
       var Data = Entry[0];                                                                     // We do this first, and then start the loop
                                                                                                // off with an Idx of 1, otherwise we will
       for (var Arg = 1; Arg < Entry.length; Arg++)                                             // put a comma between the 'MethodName: '
          {                                                                                     // prefix and the argument values.
          Data += Entry[Arg] + (Arg < Entry.length - 1 ? ", " : "");
          }

       return Data;

       }

    //----------------------------------------------

    var Stack              = [];
    var MostRecentlyPopped = null;

    return {

       onMethodExit  : function (Args, MethodOwner, MethodName, RtnVal)
          {
          MostRecentlyPopped = Stack.pop ();
          },

       onMethodEntry : function (Args, MethodOwner, MethodName)
          {
          var ArgValues = [MethodName + ": "];

          for (var Idx = 0; Idx < Args.length; Idx++)
             {
             ArgValues.push (Args[Idx.toString ()]);
             }

          Stack.push (ArgValues);

          MostRecentlyPopped = null;

          },

       //----------------------------------------------

       onException   : function (ExceptionName, Message, StackOffset)
          {
          try { throwException (ExceptionName, Message, ++StackOffset); }                       // See the Pre-/Post-Validators and
          catch (E)                                                                             // Exceptions section in the
             {                                                                                  // AJS_Validator user-guide for an
             var Data = "";                                                                     // explanation of the StackOffset argument
                                                                                                // and why we increment it before passing
             for (var Call = 0; Call < Stack.length; Call++)                                    // it to throwException.
                {
                Data += serialiseStackEntry (Stack[Call]) + "\n";
                }

             Data += MostRecentlyPopped ? serialiseStackEntry (MostRecentlyPopped) + "\n" : "";
             Data += E.name + ": " + E.message;

             reportException ('POST', URL, Data);

             Stack              = [];
             MostRecentlyPopped = null;

             }

          }

       };

    }
            

Reporting Method Return-Values

The second outstanding issue is that AJS_Validator can trap erroneous return values (which is the purpose of the RtnDef property in the ValidationDef given in lines 40 to 51 in Example 1), and so, in cases where a method exhibits such a contravention, we would like to know what went into the method in the first place, in order to ascertain why a duff value came back out.

It is here that 'affix cardinality' has a bearing on matters. AJS_Validator and AJS_Logger both use the AJS object to apply 'affix' functions to methods that must be validated or logged, where affixes are user-defined functions that execute before a given method when that method is called (in which case they are dubbed 'prefixes'), or after a method has executed (and thus are called 'suffixes'). The AJS object allows you to apply multiple prefixes and suffixes to a given method, and it is this very powerful feature that allows you to use AJS_Validator and AJS_Logger simultaneously but independently to both validate and log calls to a given method (the diffference in this exposition being that we are interested in linking the behaviour of the two clients together). However, in this case, the order in which the validation and logging affixes execute (their cardinality) is critical.

If a method returns a bad value (or returns a value where it should not), we want the report that is sent back to the server to include that value. Given this, and given the cardinality rules that the AJS object imposes, we should, in principle, apply the ValidationDef to a given method first, and then apply the logging instrumentation. This will ensure that the logging suffix executes first on the method's return (thus enabling the return value to be recorded), after which the validation suffix will execute and catch the bad return.

However, when a ValidationDef is applied to a given method, AJS_Validator checks the number of arguments that the function accepts, and raises an exception if there is a discrepancy between that and the number of ArgDefs in the CallDef that the relevant ValidationDef carries. It follows that applying the logging instrumentation first will cause AJS_Validator to check the number of arguments accepted by the proxy function that the AJS object has substituted for the method body, not the signature of the method itself. That proxy is defined as taking no arguments (which is immaterial in the normal run of things, as the AJS object uses the call method of the Function prototype to invoke an intercepted method), and so AJS_Validator will report incorrectly that the method's signature does not agree with the ValidationDef.

To avoid this problem, we could apply the ValidationDef before the logging instrumentation, which would cause AJS_Validator to check the method's signature in its un-intercepted state. However, this would also cause the logging suffix to execute first after the method returns (rather than the validation suffix), which would cause the stream object to lose the Stack member. that corresponds to the method's prior invocation.

This is the purpose of the MostRecentlyPopped reference that the custom stream-object instantiates. When a logged-and-validated method returns, its corresponding member in the Stack object is popped off that array, and is retained by that reference variable. If the validation suffix then causes AJS_Validator to raise an exception, the information gathered at the point that the method was called will still be available, which will allow the stream object to incorporate that information into its report.

Example 3 demonstrates this idea in action. A replication essentially of Example 1, the difference here is that yetAnotherMethod is now coded to return a value, in deliberate contravention of the corresponding stricture laid down in the ValidationDef. Invocation of someMethod therefore sees the Stack array grow with the ensuing chain of calls, until yetAnotherMethod returns. At that point, MostRecentlyPopped comes into play, and the result is the exception report that the Output section of Example 3 shows.

 01
 02 // -- Example 3 --------------------------------------------------------------
 03
 .
 .  // Set-up code here, as given in Example 1.
 .
 10
 11 //------------------------------------------------------------
 12
 13 var MyObj =                                                                                 // Once again, we set up three objects,
 14    {                                                                                        // which we assume do something of interest.
 15    someMethod : function (Num, Str, Obj)
 16       {
 17       MyOtherObj.someOtherMethod (Num, Str, Obj);
 18       }
 19
 20    };
 21
 22 var MyOtherObj =
 23    {
 24    someOtherMethod : function (Num, Str, Obj)
 25       {
 26       MyThirdObj.yetAnotherMethod (Num, Str, Obj);                                          // No deliberate bug here this time -
 27       }                                                                                     // someOtherMethod calls yetAnotherMethod
 28                                                                                             // correctly.
 29    };
 30
 31 var MyThirdObj =
 32    {
 33    yetAnotherMethod : function (Num, Str, Obj) { return 42; }                               // But here it returns a value, which
 34    };                                                                                       // contravenes the ValidationDef below.
 36
 37 //------------------------------------------------------------
 38
 39 AJS_Validator.applyValidationDef (MyThirdObj,                                               // Apply validation to yetAnotherMethod.
 40    {
 41    CallDef :
 42       [
 43       { AllowClasses : ['NumberLiteral'] },
 44       { AllowClasses : ['StringLiteral'] },
 46       { AllowClasses : ['Object'       ] }
 47       ],
 48
 49    RtnDef : {  }                                                                            // Here's the stricture of note: the method
 50                                                                                             // must return nothing.
 51    });
 52
 53 Logger.applyLogDef (MyObj,           { });                                                  // Apply logging, as before.
 54 Logger.applyLogDef (MyOtherObj,      { });
 56 Logger.applyLogDef (MyThirdObj,      { });
 57
 58 MyObj .someMethod  (42, "Forty Two", { });                                                  // And light the blue touch-paper once again.

   -- Output --------------------------------------------------------------------

   someMethod: 42, Forty Two, [object Object]
   someOtherMethod: 42, Forty Two, [object Object]
   yetAnotherMethod: 42, Forty Two, [object Object]
   AJS_Validator_Exception: yetAnotherMethod has returned an object of class
   NumberLiteral, where its RtnDef disallows the return of anything
   (Method-Owner's Class: Object, Method-Owner's AJS_Validator_Tag: undefined).

   Exception occurred during execution of MyOtherObj.someOtherMethod, at line 26,
   column 7, in //localhost/Test/Example_3.js
            

Taking it Further

The exposition above demonstrates one way to make two of the AspectJS client components operate in concert, and it demonstrates the high degree of modularity and thus the separation of concerns that method-call interception delivers to the developer.

Moreover, we could develop the principles covered above to include extra, and entirely arbitrary information in the exception reports that our custom stream-object generates. We could include descriptions of system state, or metrics such as, for example, how many times a given method has been called. This underscores the point made at the end of the Stream-Object Anatomy section about not using JSON.stringify, and all the ideas presented here combine to yield real power in the realm of diagnostics and profiling.

Finally, note that the examples above simply catch the exception objects thrown by throwException, and discard them after the stream object has compiled and dispatched its report. This means that execution will continue after the exceptional condition has arisen, which may not be what we desire. The solution, however, is simple: you need only add an extra line of code to the end of the catch clause in the stream object's onException method, such that the exception object E is re-thrown. This would pass it up to the super-suming scope, where it could be caught again by proprietary code or, failing that, could be caught ultimately by the run-time.