📜 ⬆️ ⬇️

We write logging system in InterSystems Caché

Monet impression sunrise

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


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:
InformationTime of receipt
Event typeCompilation
Class nameCompilation
Method nameCompilation
Arguments passed to the methodCompilation
Line number of ref. CodaPerformance
UsernamePerformance
Date TimePerformance
MessagePerformance
IP addressPerformance
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.Log
Class 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:

Add the appropriate methods to the App.Log class:
Methods GetMethodArguments, GetMethodArgumentsList, ArgumentsListToString
ClassMethod 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.Use
Include 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 DeserializeObject
ClassMethod 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. Macros
GitHub logging repository
Logging System Classes

The author is grateful to Daimor Habrawers , Greyder, and another very competent engineer who wished to remain anonymous, for help in writing the code.

Source: https://habr.com/ru/post/258805/


All Articles