
Introduction
In the
previous article, we looked at the main uses of macros, and now let's move on to writing a more extensive example using macros. We will write a logging system.
Logging system
This is a necessary and useful tool for logging the application. Significantly saves time spent on debugging and catching various bugs. The system consists of two parts:
- Log storage class
- A set of macros to automatically add a new entry to the log
Log storage class
We will draw up a table of what we need to store, we will immediately write when it is possible to obtain this data — at the time of compilation or execution of the program. This will be required when writing the second part of the system - macros, where we obviously strive to find out as much of the logged data as possible during compilation:
Information | Time of receipt |
---|
Event type | Compilation |
Class name | Compilation |
Method name | Compilation |
Arguments passed to the method | Compilation |
Line number of ref. Coda | Performance |
Username | Performance |
Date Time | Performance |
Message | Performance |
IP address | Performance |
Create an App.Log class. In it we will create all the above properties. When creating an App.Log class object, the Username, Date / Time, and IP address properties are filled automatically.
Class App.LogClass App.Log Extends% Persistent
{
/// Substitution of non-existent values
Parameter Null = "Null" ;
/// Event Type
Property EventType As% String ( MAXLEN = 10 , VALUELIST = ", NONE, FATAL, ERROR, WARN, INFO, STAT, DEBUG, RAW" );
/// class name
Property ClassName As% String ( MAXLEN = 256 );
/// Method Name
Property MethodName As% String ( MAXLEN = 128 );
/// String int code
Property Source As% String ( MAXLEN = 2000 );
/// Username
Property UserName As% String ( MAXLEN = 128 ) [ InitialExpression = { $ username }];
/// Arguments passed to the method
Property Arguments As% String ( MAXLEN = 32000 , TRUNCATE = 1 );
/// Date Time
Property TimeStamp As% TimeStamp [ InitialExpression = { $ zdt ( $ h , 3, 1)}]];
/// Random Message
Property Message As% String ( MAXLEN = 32000 , TRUNCATE = 1 );
/// Network address of the user
Property ClientIPAddress As% String ( MAXLEN = 32 ) [ InitialExpression = {.. GetClientAddress ()}];
')
Index idxEventType On EventType [ Type = bitmap];
Index idxUserName On UserName [ Type = bitmap];
Index idxClassName On ClassName [ Type = bitmap];
Index idxTimeStamp On TimeStamp [ Type = bitslice];
Index idxClientIPAddress On ClientIPAddress;
/// Determine the network address of the user
ClassMethod GetClientAddress ()
{
// If there is a CSP session, then take from there
#dim % request As % CSP.Request
If ( $ d ( % request )) {
Return % request . CgiEnvs ( "REMOTE_ADDR" )
}
Return $ system .Process . ClientIPAddress ()
}
/// Adding an event.
/// Use not directly, but through the $$$ LogEventTYPE () macros.
ClassMethod AddRecord ( ClassName As% String = "" , MethodName As% String = "" , Source As% String = "" , EventType As% String = "" , Arguments As% String = "" , Message As% String = "" )
{
Set record = .. % New ()
Set record . Arguments = Arguments
Set record . ClassName = ClassName
Set record . EventType = EventType
Set record . Message = Message
Set record . MethodName = MethodName
Set record . Source = Source
Do record . % Save ()
}
}
Logging macros
Usually, macros are separated into separate inc files, where only the definitions of the macros themselves are stored.
Include the necessary files to the classes with the command
Include MacroFileName . In this case,
Include App . LogMacro .
First, let's define the main macro, which the user will add to the code of his application:
#define LogEvent (% type,% message) Do ## class (App.Log) .AddRecord ($$$ CurrentClass, $$$ CurrentMethod, $$$ StackPlace,% type,
$$$ MethodArguments,% message)This macro takes 2 arguments as input - Event type and Message. The user sets the message himself, but for the transfer of the event type we will write additional macros with different names and transmitting the Event Type without the need of any actions from the user:
LogTYPE macros#define LogNone (% message) $$$ LogEvent ( "NONE" , % message )
#define LogError (% message) $$$ LogEvent ( "ERROR" , % message )
#define LogFatal (% message) $$$ LogEvent ( "FATAL" , % message )
#define LogWarn (% message) $$$ LogEvent ( "WARN" , % message )
#define LogInfo (% message) $$$ LogEvent ( "INFO" , % message )
#define LogStat (% message) $$$ LogEvent ( "STAT" , % message )
#define LogDebug (% message) $$$ LogEvent ( "DEBUG" , % message )
#define LogRaw (% message) $$$ LogEvent ( "RAW" , % message )
Thus, a user can use logging to set the
$$$ LogError ( "Additional Message" ) macro in the application code.
It remains to define the macros
$$$ CurrentClass, $$$ CurrentMethod, $$$ StackPlace, $$$ MethodArguments . Let's start with the first three:
#define CurrentClass ## Expression ($$$ quote ( % classname ) )
#define CurrentMethod ## Expression ($$$ quote ( % methodname ) )
#define StackPlace $ st ($ st (-1), "PLACE" )The variables
% classname,% methodname are described in the macro
documentation . To get the line number of int code, use the
$ stack function.
To get the list of method arguments and their values, we use the% Dictionary package. All class descriptions are stored there, including method descriptions. In particular, the% Dictionary.CompiledMethod class and its FormalSpecParsed property are of interest: a $ lb structure of the following form:
$ lb ( $ lb ( "Name" , "Class" , "Type (Output / ByRef)" , "Default Value" ), ...)
corresponding to the method signature. For example, the ClassMethod Test method ( a As% Integer = 1 , ByRef b = 2 , Output c ) will have the following FormalSpecParsed:
$ lb (
$ lb ( "a" , "% Library.Integer" , "" , "1" ),
$ lb ( "b" , "% Library.String" , "&" , "2" ),
$ lb ( "c" , "% Library.String" , "*" , "" ))
As a result of the $$$ MethodArguments macro, you need to get the following code (for the Test method): "a =" _ $ g ( a , "Null" ) _ "; b =" _ $ g ( b , "Null" ) _ " ; c = " _ $ g ( c , " Null " ) _ "; "
To do this, at the compilation stage you need:
- Get class name and method name
- Open the corresponding instance of the% Dictionary.CompiledMethod class and get the FormalSpec property
- Convert it to source line
Add the appropriate methods to the App.Log class:
Methods GetMethodArguments, GetMethodArgumentsList, ArgumentsListToStringClassMethod GetMethodArguments ( ClassName As% String , MethodName As% String ) As% String
{
Set list = .. GetMethodArgumentsList ( ClassName , MethodName )
Set string = .. ArgumentsListToString ( list )
Return string
}
ClassMethod GetMethodArgumentsList ( ClassName As% String , MethodName As% String ) As% List
{
Set result = ""
Set def = ## class ( % Dictionary.CompiledMethod ). % OpenId ( ClassName _ "||" _ MethodName )
If ( $ IsObject ( def )) {
Set result = def . FormalSpecParsed
}
Return result
}
ClassMethod ArgumentsListToString ( List As% List ) As% String
{
Set result = ""
For i = 1: 1: $ ll ( List ) {
Set result = result _ $$$ quote ( $ s ( i > 1 = 0: "" , 1: ";" ) _ $ lg ( $ lg ( List , i )) _ "=" )
_ "_ $ g (" _ $ lg ( $ lg ( List , i )) _ "," _ $$$ quote (.. #Null ) _ ") _"
_ $ s ( i = $ ll ( List ) = 0: "" , 1: $$$ quote ( ";" ))
}
Return result
}
Now we define the
$$$ MethodArguments macro as:
#define MethodArguments ## Expression (## class (App.Log) .GetMethodArguments (% classname,% methodname))
Usage example
Create an App.Use class with the Test method to demonstrate the capabilities of the logging system
App.UseInclude App . Logmacro
Class App.Use [ CompileAfter = App.Log ]
{
/// Do ## class (App.Use) .Test ()
ClassMethod Test ( a As% Integer = 1 , ByRef b = 2 )
{
$$$ LogWarn ( "Text" )
}
}
As a result, the
$$$ LogWarn ( "Text" ) macro in int code is converted to a string:
Do ## class ( App .Log
) .
AddRecord ( "App.Use" , "Test" , $ st ( $ st ( -1),
"PLACE" ) ,
"WARN" , "a =" _ $ g ( a , "Null" ) _
"; b = " _ $ g ( b , " Null " ) _
"; " , " Text " )
After executing this code, a new object of class App.Log is created:

Improvements
Having written the basis of the logging system, we will think about how it can be improved.
First, let's add the ability to process object arguments, because at the moment only
oref objects will be written.
Secondly, we add the ability to open the context - in that case, from the stored values of the arguments, restore the context (arguments) of the method.
Handling Object Arguments
Generating a string that writes the value of the argument to the log occurs in the ArgumentsListToString method and looks like:
"_ $ g (" _ $ lg ( $ lg ( List , i )) _ "," _ $$$ quote (.. #Null ) _ ") _" . Let's refactor and select it into a separate method GetArgumentValue, which will take the input variable name and its class (and we know all this from FormalSpecParsed) and the output will be the code that converts the variable into a string. For data types, this will be the already existing code, the objects will be converted to JSON using the methods SerializeObject (to be called from client code) and WriteJSONFromObject (to convert the object to JSON):
The GetArgumentValue, SerializeObject, and WriteJSONFromObject methods:ClassMethod GetArgumentValue ( Name As% String , ClassName As% Dictionary.CacheClassname ) As% String
{
If $ ClassMethod ( ClassName , "% Extends" , "% RegisteredObject" ) {
// it's an object
Return "_ ## class (App.Log) .SerializeObject (" _ Name _ ") _"
} Else {
// it's a datatype
Return "_ $ g (" _ Name _ "," _ $$$ quote (.. #Null ) _ ") _"
}
}
ClassMethod SerializeObject ( Object ) As% String
{
Return : ' $ IsObject ( Object ) Object
Return .. WriteJSONFromObject ( Object )
}
ClassMethod WriteJSONFromObject ( Object ) As% String [ ProcedureBlock = 0]
{
// Create a string that we will redirect to
Set Str = ""
Use $ io : :( "^" _ $ ZNAME )
// Enable redirection
Do ## class ( % Device ). ReDirectIO (1)
// Any statements can be written below.
Do ## class ( % ZEN.Auxiliary.jsonProvider ). % ObjectToJSON ( Object )
// Disable redirection
Do ## class ( % Device ). ReDirectIO (0)
Return Str
// Labels that allow for IO redirection
// Read Character - we don't care
rchr Quit
// Read a string - we don't care
rstr (sz, to) Quit
// Write a character - call the output label
wchr (s) Do output ( $ char ( s )) Quit
// Write a form feed - call the output label
wff () Do output ( $ char (12)) Quit
// Write a newline - call the output label
wnl () Do output ( $ char (13,10)) Quit
// Write a string - call the output label
wstr (s) Do output ( s ) Quit
// Write a tab - call the output label
wtab (s) Do output ( $ char (9)) Quit
// Output label - you really want to do.
// in our case, we want to write to Str
output (s) Set Str = Str _ s Quit
}
Writing to the log with an object argument looks like this:

Opening context
The idea of this method is to make all the arguments available in the current context (mostly the terminal, for debugging the application). For this, the parameter of the
ProcedureBlock method is used; if set to 0, then all variables declared inside such a method will be available even after the output of their method. Our method, accordingly, will open an App.Log class object and deserialize the Arguments property.
Methods LoadContext and DeserializeObjectClassMethod LoadContext ( Id ) As% Status [ ProcedureBlock = 0]
{
Return : '.. % ExistsId ( Id ) $$$ OK
Set Obj = .. % OpenId ( Id )
Set Arguments = Obj . Arguments
Set List = .. GetMethodArgumentsList ( Obj . ClassName , Obj . MethodName )
For i = 1: 1: $ Length ( Arguments , ";" ) -1 {
Set Argument = $ Piece ( Arguments , ";" , i )
Set @ $ lg ( $ lg ( List , i )) = .. DeserializeObject ( $ Piece ( Argument , "=" , 2), $ lg ( $ lg ( List , i ), 2))
}
Kill Obj , Arguments , Argument , i , Id , List
}
ClassMethod DeserializeObject ( String , ClassName ) As% String
{
If $ ClassMethod ( ClassName , "% Extends" , "% RegisteredObject" ) {
// it's an object
Set st = ## class ( % ZEN.Auxiliary.jsonProvider ). % ConvertJSONToObject ( String ,,. Obj )
Return : $$$ ISOK ( st ) obj
}
Return string
}
In the terminal, it looks like this:zw > do ##class(App.Log).LoadContext(9) zw >a=3 >b=<OBJECT REFERENCE>[2@%ZEN.proxyObject] zw b > +----------------- general information --------------- | oref value: 2 | class name: %ZEN.proxyObject | reference count: 2 +----------------- attribute values ------------------ | %changed = 1 | %data("A") = 1 | %data("B") = "stringvalue" | %index = ""
What's next?
The main improvement that can be made here is the addition of another argument to the main macro with an arbitrary list of variables already created inside the method.
findings
Macros allow you to implement at the compilation stage a part of the application logic, thus reducing the load in runtime.
Links
Part I. MacrosGitHub logging repositoryLogging System ClassesThe author is grateful to Daimor
Habrawers ,
Greyder, and another very competent engineer who wished to remain anonymous, for help in writing the code.