{"id":114,"date":"2008-12-06T18:21:00","date_gmt":"2008-12-06T18:21:00","guid":{"rendered":"\/lisa\/post\/2008\/12\/06\/Keeping-it-Simple-with-Log4Fox.aspx"},"modified":"2021-08-30T13:27:09","modified_gmt":"2021-08-30T20:27:09","slug":"keeping-it-simple-with-log4fox","status":"publish","type":"post","link":"https:\/\/spacefold.com\/lisa\/2008\/12\/06\/keeping-it-simple-with-log4fox\/","title":{"rendered":"Keeping it Simple with Log4Fox"},"content":{"rendered":"<p>David Le Mesurier writes:<\/p>\n<blockquote style=\"background-color: aliceblue;\"><p>Hi Lisa,<br \/>\nI have been looking at your <a title=\"Log4Fox article\" href=\"\/articles\/log4fox\">log4fox<\/a> code as I want to instrument my applications.<br \/>\nBasically I want to be able to instrument them at a client&#8217;s site by setting an ini file setting to turn on the code.<br \/>\nI don&#8217;t want the user to have to do anything other than tick a box in my application&#8217;s preference form.<br \/>\nTo log it to a table, what do I need to set in the application to:<\/p>\n<ul style=\"margin: 0px;\">\n<li>A. Instantiate the logging object to log to the table<\/li>\n<li>B. Make a call to the logging code.<\/li>\n<\/ul>\n<p>I have looked at the sample prg, but I&#8217;m afraid that I can&#8217;t seem to get my head around what is needed.<\/p><\/blockquote>\n<h4><\/h4>\n<h4>And it&#8217;s no wonder<\/h4>\n<p>Sorry David, this is entirely my fault.<\/p>\n<p>Not because the code I published had any bugs in it; it&#8217;s perfectly fine.\u00a0 But, when I wrote that <a title=\"Log4Fox article\" href=\"\/articles\/log4fox\/\" target=\"_blank\" rel=\"noopener\">article<\/a> and <a title=\"Get code for Log4Fox here\" href=\"\/downloads-index\/\" target=\"_blank\" rel=\"noopener\">published the test program<\/a>,\u00a0I was interested in\u00a0showing the depth and breadth of a complex API, and maybe this doesn&#8217;t show you how you&#8217;d call the logging API in real life.<\/p>\n<p>There were a couple of other details, now that I look at it, that could be improved or further explained, to help David use the API according to his specification:<\/p>\n<ul>\n<li>I didn&#8217;t include an Appender class that logs to a Fox table.\u00a0 I did include a SQL Appender, but it addresses an external\u00a0table using a handle to a SQL connection.<\/li>\n<li>The SQL Appender I wrote didn&#8217;t (and I really didn&#8217;t have any reason for this, other than performance\/efficiency) write individual rows to a table.\u00a0 It accumulated log events and then wrote the whole log to a text value (much like a stored version of the file you get from the File Appender) when you closed the log.<\/li>\n<\/ul>\n<p>Maybe it wasn&#8217;t clear in the article that writing other Appenders and Formatter objects, if necessary, is really easy.<\/p>\n<p>You&#8217;re not likely to need to write Formatters very often, but there isn&#8217;t much required if you do.\u00a0 To write an Appender, you really only have to implement one required method, as the article does explain: the .SendLog method that actually addresses your chosen output device.<\/p>\n<p>In many cases you will also need to implement .OpenLog and .CloseLog.\u00a0 For each of these methods you&#8217;re going to consider: what does it mean to &#8220;open&#8221;, &#8220;close&#8221; and &#8220;send&#8221; information to my output type? How do these ideas apply?<\/p>\n<p>The ideas of .Open and .Close don&#8217;t apply to every output type; for example an Appender that wrote to _SCREEN might decide to CLEAR the _VFP application window and set some font properties, or it might do nothing at all.\u00a0 But .Send applies to every output, because that is the point at which the log message gets rendered.<\/p>\n<p class=\"NB\">Hmmm. Anybody see a resemblance to ReportListener design questions here?<\/p>\n<p>You don&#8217;t need to worry about anything else.\u00a0 The Logger maintains your Appenders\u00a0and calls their .DoLog methods.\u00a0 You don&#8217;t need to touch the .DoLog method, which simply figures out whether the Appender is supposed to log this event, based on the level of log events it&#8217;s currently configured to handle.\u00a0\u00a0 The Appender base class is going to take care of all the\u00a0shared\u00a0details for you.\u00a0You only need to touch .SendLog, because your method of rendering the output device is specific to that device.<\/p>\n<p>I&#8217;ll write a quick Fox Cursor Appender in this post, as a kind of a walkthrough for you.<\/p>\n<p>The published test program instantiates many Appender objects to the Logger&#8217;s collection, to show you how they each handle log events according to their own\u00a0output types\u00a0and current logging level.\u00a0 This, by the way<em>, is<\/em> a real\u00a0life scenario: it is perfectly normal to configure a file Appender to log verbosely while only sending Fatal-level events to an email Appender.<\/p>\n<h4><\/h4>\n<h4>How does the Logger figure out what Appenders your user wants, and what their levels should be?<\/h4>\n<p>In the article, I addressed\u00a0configuration by deriving a specialized class from\u00a0Log4Fox, which I called Log4FoxConfigAlias.\u00a0 The idea here was that the logger would read a configuration table or cursor, to which you&#8217;d point it by telling it the alias containing this information.<\/p>\n<p>The published test program creates that cursor on the fly, with a bunch of records holding instructions about different Appenders.\u00a0 Then it creates a Log4FoxConfigAlias Logger instance, and tells\u00a0the new object\u00a0about the cursor.\u00a0 The Log4FoxConfigAlias instance reads the cursor, adds the Appenders to its collection, and configures them, based on the records in that cursor.<\/p>\n<p>David could use Log4FoxConfigAlias if he wants.\u00a0 In fact, he might even instantiate it on-the-fly, as I did, using information from his INI file.\u00a0 But he could also derive a different class, Log4FoxConfigFromINI, which would read his INI file directly.<\/p>\n<p>In this blog-walkthrough, I won&#8217;t do either one.\u00a0 I&#8217;ll just use the ancestor Logger class, Log4Fox, which doesn&#8217;t know how to do any configuration.\u00a0 I&#8217;ll do some work in the calling program that you&#8217;ll have to imagine is being done by your startup code, or your Application object, or however <em>you<\/em> normally do this type of thing.<\/p>\n<h5 class=\"NB\">Sidebar: VFP\u00a0Helper Object techniques<\/h5>\n<p class=\"NB\">Again, I don&#8217;t want to over-complicate things here, but you may want to remember that all these objects are derived from an ancestor called VFPHelperObject.<\/p>\n<p>This\u00a0class&#8217; raison d&#8217;\u00eatre\u00a0&#8212; which really has nothing to do with Log4Fox or log4j &#8212; is to help you integrate a library like this one into your overall application strategy.<br \/>\nVFPHelperObject expresses this principle by handing off error-handling behavior to its &#8220;owner object&#8221; &#8212; presumably your application object &#8212; so that its behavior matches the rest of your code when an error occurs.<\/p>\n<p>In the calling program here, and in the published test program, I have to explicitly turn off that behavior, since there is no external owner object.\u00a0 But I hope you can see how useful this practice can be, when you&#8217;re assembling a full-scale application from a bunch of pre-built and externally-designed components.<\/p>\n<h4>Now for that walkthrough<\/h4>\n<p>David wants to know how to log to a table.\u00a0 Well, first you need an Appender that knows how to log to a (presumably Fox) table.<\/p>\n<p>I didn&#8217;t write one of those before, so I&#8217;ll do one now, and this time it will log each event to a separate row in the table rather than writing the full log to a memo field, as I did in the SQL Appender before.<\/p>\n<p>For this walkthrough I&#8217;ll specify as follows:<\/p>\n<ul>\n<li>You tell the Appender where to log by telling it what alias to use. You also tell the Appender in what DataSession the Appender should find the LogAlias (I figure most people use an Application object with a private data session for work like this)<\/li>\n<li>Because this Appender is writing separate rows to the table for each event, you should be able to find the Open and Close events in the table easily.\u00a0 However, we&#8217;ll add a member property to the Appender called LogRunName, because multiple instances of the application may be writing to the same table, and we&#8217;ll include this value in a separate column of the LogAlias table.<br \/>\nFor this walkthrough, I&#8217;ll use GETENV(&#8220;UserName&#8221;) to distinguish rows between different users logging to a network table, which would work if the application was a singleton for each user &#8212; you could also generate a unique key for this purpose.<\/li>\n<li>The Appender will expect, and validate, a LogAlias structure that\u00a0looks like this:<br \/>\n<span style=\"color: #0000ff;\">CREATE CURSOR <\/span>(<span style=\"color: #0000ff;\">THIS<\/span>.LogAlias) ;<br \/>\n(event_run <span style=\"color: #0000ff;\">char<\/span>(50), event_date <span style=\"color: #0000ff;\">datetime<\/span>, event_log <span style=\"color: #0000ff;\">varchar<\/span>(250))<\/li>\n<\/ul>\n<p>Ready?<\/p>\n<h5>DEFINEing Class log4FoxCursorAppender AS log4FoxAbstractAppender<\/h5>\n<p>&nbsp;<\/p>\n<p>What will our new Appender class need to do?\u00a0 Well, it&#8217;s going to need a few member properties, matching our specification:<\/p>\n<p class=\"code\">LogDataSession = 1<br \/>\nLogAlias = &#8220;EventLog&#8221;<br \/>\nLogRunName = &#8220;Log Run Name&#8221;<\/p>\n<p>Remember what I said before? There&#8217;s definitely\u00a0one method we have to implement: .SendLog.\u00a0\u00a0 It will look like this:<\/p>\n<p class=\"code\"><span style=\"color: #0000ff;\">PROTECTED PROCEDURE <\/span>SendLog(whatever)<br \/>\n<span style=\"color: #0000ff;\">IF THIS<\/span>.LogOpen<br \/>\n<span style=\"color: #0000ff;\">\u00a0\u00a0\u00a0\u00a0\u00a0 LOCAL <\/span>iSession<br \/>\niSession = <span style=\"color: #0000ff;\">SET<\/span>(&#8220;DATASESSION&#8221;)<br \/>\n<span style=\"color: #0000ff;\">\u00a0\u00a0\u00a0\u00a0\u00a0 SET DATASESSION TO <\/span>(<span style=\"color: #0000ff;\">THIS<\/span>.LogDataSession)<br \/>\n<span style=\"color: #0000ff;\">INSERT INTO <\/span>(<span style=\"color: #0000ff;\">THIS<\/span>.LogAlias) <span style=\"color: #0000ff;\">VALUES <\/span>;<br \/>\n(<span style=\"color: #0000ff;\">THIS<\/span>.LogRunName, <span style=\"color: #0000ff;\">DATETIME<\/span>(), whatever)<br \/>\n<span style=\"color: #0000ff;\">SET DATASESSION TO <\/span>(iSession)<br \/>\n<span style=\"color: #0000ff;\">\u00a0\u00a0\u00a0ENDIF<\/span><span style=\"color: #0000ff;\"><br \/>\nENDPROC <\/span><\/p>\n<p>&#8230; really only a few obvious lines of code.<\/p>\n<p class=\"NB\">You may think the check for .LogOpen is unnecessary (why isn&#8217;t this behavior somewhere else, like .DoLog in the ancestor class?). But some output destinations don&#8217;t really have a need for the concept of a log being opened at all.\u00a0 SQL Appender, for example, doesn&#8217;t care whether you&#8217;ve previously opened a log by checking for a handle; it can accumulate contents and open the log later, if it wants.\u00a0 What a log being &#8220;open&#8221; means, and whether you care about it, as well as <em>at what <\/em>moments you care about it, is highly dependent on output device, and therefore is handled by each Appender.<\/p>\n<p>In <em>this<\/em> class, THIS.LogOpen means that the class has previously checked for its alias and either verified its structure or opened a cursor with the right structure, in the correct data session, of course.<\/p>\n<p>So how do you suppose it does\u00a0that?\u00a0 Let&#8217;s look at the rest of the class code, which is similarly straightforward:<\/p>\n<p class=\"code\"><span style=\"color: #0000ff;\">PROCEDURE <\/span>OpenLog()<br \/>\n<span style=\"color: #008000;\">* Let&#8217;s set some values here that probably<\/span><span style=\"color: #008000;\"><br \/>\n* aren&#8217;t useful to expose, since this<br \/>\n* class is always going to want them set<br \/>\n* a certain way.<br \/>\n* If you create a different Formatter (Layout)<br \/>\n* object you probably will have different need<br \/>\n<\/span><span style=\"color: #0000ff;\">THIS<\/span>.Layout.IncludeDateTimeStamp = .F. <span style=\"color: #008000;\">&amp;&amp; because this Appender<\/span><span style=\"color: #008000;\"><br \/>\n&amp;&amp; has a separate column for it<br \/>\n<\/span><span style=\"color: #0000ff;\">THIS<\/span>.Layout.IncludeLogEvent = .T.<\/p>\n<p>\u00a0\u00a0 <span style=\"color: #008000;\">* in real life you would probably expose *this*<\/span><span style=\"color: #008000;\"><br \/>\n* one, or else generate a unique key for the run here: <\/span><br \/>\n<span style=\"color: #0000ff;\">\u00a0\u00a0 THIS<\/span>.LogRunName = &#8220;Log run: &#8221; + <span style=\"color: #0000ff;\">GETENV<\/span>(&#8220;UserName&#8221;)<\/p>\n<p><span style=\"color: #0000ff;\">\u00a0\u00a0 IF THIS<\/span>.loglevel &lt; LOGLEVEL_NONE<br \/>\n<span style=\"color: #0000ff;\">\u00a0\u00a0\u00a0\u00a0\u00a0 LOCAL <\/span>iSession, iSelect,iArea<br \/>\niSession = <span style=\"color: #0000ff;\">SET<\/span>(&#8220;DATASESSION&#8221;)<br \/>\n<span style=\"color: #0000ff;\">\u00a0\u00a0\u00a0\u00a0\u00a0 SET DATASESSION TO <\/span>(<span style=\"color: #0000ff;\">THIS<\/span>.LogDataSession)<br \/>\niSelect = <span style=\"color: #0000ff;\">SELECT<\/span>(0)<br \/>\niArea = <span style=\"color: #0000ff;\">SELECT<\/span>(<span style=\"color: #0000ff;\">THIS<\/span>.LogAlias)<br \/>\n<span style=\"color: #0000ff;\">IF <\/span>iArea &gt; 0 AND <span style=\"color: #0000ff;\">THIS<\/span>.ValidateLogCursor(iArea)<br \/>\n<span style=\"color: #0000ff;\">\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 SELECT <\/span>(<span style=\"color: #0000ff;\">THIS<\/span>.LogAlias)<br \/>\n<span style=\"color: #0000ff;\">ELSE<\/span><span style=\"color: #0000ff;\"><br \/>\nIF iArea &gt; 0<br \/>\n<\/span><span style=\"color: #0000ff;\">USE IN <\/span>(iArea)<br \/>\n<span style=\"color: #0000ff;\">\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 ENDIF<br \/>\nSELECT <\/span>0<br \/>\n<span style=\"color: #0000ff;\">\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 CREATE CURSOR <\/span>(<span style=\"color: #0000ff;\">THIS<\/span>.LogAlias) ;<br \/>\n(event_run <span style=\"color: #0000ff;\">char<\/span>(50), ;<br \/>\nevent_date <span style=\"color: #0000ff;\">datetime<\/span>, ;<br \/>\nevent_log <span style=\"color: #0000ff;\">varchar<\/span>(250))<br \/>\n<span style=\"color: #0000ff;\">\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0ENDIF<br \/>\nTHIS<\/span> .LogOpen = .T.<br \/>\n<span style=\"color: #0000ff;\">\u00a0\u00a0\u00a0\u00a0\u00a0 SELECT <\/span>(iSelect)<br \/>\n<span style=\"color: #0000ff;\">SET DATASESSION TO <\/span>(iSession)<br \/>\n<span style=\"color: #0000ff;\">\u00a0\u00a0\u00a0\u00a0\u00a0 THIS<\/span>.DoLog(<span style=\"color: #0000ff;\">THIS<\/span>.GetStandardLogOpenedMessage(),LOGLEVEL_INFO)<br \/>\n<span style=\"color: #0000ff;\">ENDIF<\/span><span style=\"color: #0000ff;\"><br \/>\nENDPROC <\/span><\/p>\n<p><span style=\"color: #0000ff;\">PROTECTED PROCEDURE <\/span>ValidateLogCursor(iArea)<br \/>\n<span style=\"color: #008000;\">\u00a0\u00a0 * validate to suit your needs<\/span><br \/>\n<span style=\"color: #0000ff;\">RETURN <\/span>;<br \/>\n<span style=\"color: #0000ff;\">\u00a0\u00a0 LEN<\/span>(<span style=\"color: #0000ff;\">FIELD<\/span>(&#8220;event_run&#8221;,iArea)) &gt; 0 AND ;<br \/>\n<span style=\"color: #0000ff;\">LEN<\/span>(<span style=\"color: #0000ff;\">FIELD<\/span>(&#8220;event_date&#8221;,iArea)) &gt; 0 AND ;<br \/>\n<span style=\"color: #0000ff;\">\u00a0\u00a0\u00a0LEN<\/span>(<span style=\"color: #0000ff;\">FIELD<\/span>(&#8220;event_log&#8221;,iArea)) &gt; 0<br \/>\n<span style=\"color: #0000ff;\">ENDPROC<\/span><\/p>\n<p>PROCEDURE CloseLog()<br \/>\n<span style=\"color: #0000ff;\">\u00a0\u00a0 IF THIS<\/span>.LogOpen<br \/>\n<span style=\"color: #0000ff;\">\u00a0\u00a0\u00a0\u00a0\u00a0 THIS<\/span>.DoLog(<span style=\"color: #0000ff;\">THIS<\/span>.GetStandardLogClosedMessage(),LOGLEVEL_INFO)<br \/>\n<span style=\"color: #008000;\">\u00a0\u00a0\u00a0\u00a0\u00a0 * we won&#8217;t close the cursor, we&#8217;ll just indicate that<br \/>\n* something\u00a0in the calling code decided to close the<br \/>\n* log at this moment.\u00a0 It isn&#8217;t necessarily the end<br \/>\n* of an application run; perhaps there is going to be<br \/>\n* some activity that doesn&#8217;t need to be logged.<\/span><\/p>\n<p>* What will *your* class do when you decide it&#8217;s<br \/>\n* time to close the log?<br \/>\n* You might decide it&#8217;s a good time to<br \/>\n* run a report form on the log contents,<br \/>\n* if you are in LOGLEVEL_DEBUG&#8230;<br \/>\n* you might store the contents of the cursor<br \/>\n* to a SQL table&#8230;<\/p>\n<p>* Of course you could perform that activity<br \/>\n* at a different time, using a separate<br \/>\n* public method.<\/p>\n<p><span style=\"color: #0000ff;\">THIS<\/span>.LogOpen = .F.<br \/>\n<span style=\"color: #0000ff;\">\u00a0\u00a0 ENDIF<br \/>\nDODEFAULT<\/span> ()<br \/>\n<span style=\"color: #0000ff;\">ENDPROC\u00a0\u00a0\u00a0<\/span><\/p>\n<p>&#8230; and that&#8217;s the whole class.<\/p>\n<p class=\"NB\">Note: I will be adding this class to the log4fox.prg in the standard article download, and I will also add the code for the sample invocation of the class, that we&#8217;ll look at next.<\/p>\n<h5>How do we instantiate it at runtime?<\/h5>\n<p>David wants to know how to &#8220;instantiate the logging object to log to the table&#8221;.<\/p>\n<p>You do that by creating a Log4Fox object (or a derived class, such as Log4FoxConfigAlias) and ensuring that it has an Appender in its collection that knows how to log to a table.\u00a0(We have one of those now.)<\/p>\n<p>If we&#8217;re using Log4FoxConfigAlias as our Logger object, as I did for the article, we can put a row in Log4FoxConfigAlias&#8217;s configuration table that says we want our new Appender.\u00a0 Log4FoxConfigAlias does this work in its Configuration_Assign method.\u00a0 As I explained in the article, it just has to instantiate each Appender object in its table and pass the Appender a reference to itself.<\/p>\n<p>&nbsp;<\/p>\n<p class=\"code\">loX = <span style=\"color: #0000ff;\">CREATEOBJECT<\/span>(<span style=\"color: #0000ff;\">ALLTRIM<\/span>(logger), <span style=\"color: #0000ff;\">THIS<\/span>)<\/p>\n<p>&nbsp;<\/p>\n<p>&#8230; the Appenders know how to add themselves into the Logger&#8217;s collection.\u00a0 (Remember: these two object types are tightly coupled.)<\/p>\n<p>Log4FoxConfigAlias passes other information to each Appender it creates, too, using information in its configuration table: for example, it tells each\u00a0Appender what level of log events it should include.<\/p>\n<p>If David writes a Log4FoxConfigINI derived class that looks in an INI file, it will do pretty much the same thing, using the INI file instead of a table for its resource.<\/p>\n<p>However, David doesn&#8217;t really have to do that.\u00a0 He can use the ancestor\u00a0class Log4Fox if he wants, and that&#8217;s what I&#8217;ve done in the simple DAVID_LOG4FOX.PRG:<\/p>\n<p class=\"code\">loLogger = <span style=\"color: #0000ff;\">NEWOBJECT<\/span>(&#8220;Log4Fox&#8221;,&#8221;Log4Fox.PRG&#8221;,&#8221;&#8221;,<span style=\"color: #0000ff;\">NULL<\/span>,.T.)<br \/>\n<span style=\"color: #008000;\">*&amp;* but ordinarily this object would belong to an owner, like so:<br \/>\n*&amp;* loLogger = CREATEOBJECT(&#8220;Log4Fox&#8221;,THIS.Application,.F.) <\/span><\/p>\n<p>&#8230; and then he can (presumably, having read some information from his INI file externally) add the Appenders he&#8217;d like to use to his Logger object, and configure them as needed,\u00a0like this:<\/p>\n<p><span style=\"color: #0000ff;\">NEWOBJECT<span style=\"color: black;\">(&#8220;log4foxEmailAppender&#8221;,&#8221;Log4Fox.PRG&#8221;,&#8221;&#8221;,loLogger) <\/span><br \/>\n<span style=\"color: #0000ff;\">NEWOBJECT<\/span><span style=\"color: #000000;\">(&#8220;log4foxCursorAppender&#8221;,&#8221;Log4Fox.PRG&#8221;,&#8221;&#8221;,loLogger) <\/span><\/span><span style=\"color: #008000;\">&amp;&amp; new!<\/span><\/p>\n<p><span style=\"color: #008000;\">* read from configuration info here<\/span><\/p>\n<p>* for now I&#8217;ll suggest some useful values that<br \/>\n* aren&#8217;t actually being read from anywhere, but<br \/>\n* obviously could be<\/p>\n<p>* I am going to assume that David wants<br \/>\n* his application preference form<br \/>\n* to write to the INI with this or<br \/>\n* any other information this class needs, but<br \/>\n* I&#8217;ll skip that part too.<\/p>\n<p><span style=\"color: #0000ff;\">FOR EACH <span style=\"color: #000000;\">loX <\/span>AS FoxOBJECT IN <span style=\"color: #000000;\">loLogger.Appenders<br \/>\nloX.SetLogLevel(LOGLEVEL_DEBUG)<br \/>\n<\/span><span style=\"color: #0000ff;\">ENDFOR<\/span> <\/span><\/p>\n<h5><\/h5>\n<h5>How do I\u00a0 log stuff?<\/h5>\n<p>&nbsp;<\/p>\n<p>This was David&#8217;s last question, and it&#8217;s probably <em>so<\/em> simple he just didn&#8217;t believe it.<\/p>\n<p>You&#8217;ve got your Logger object, and you just tell it to log whenever you feel like it.\u00a0 You forget all about the mechanics about how each Appender will output the logging information, and how each Appender&#8217;s Formatter object will construct the information to be sent to the Appender&#8217;s output device.<\/p>\n<p>You just instrument your code with calls that look like this:<\/p>\n<p class=\"code\">loLogger.<span style=\"color: #0000ff;\"><span style=\"color: #0000ff;\">OPEN<\/span><\/span>() <span style=\"color: #008000;\">&amp;&amp; or MyApplicationObject.Logger.OPEN(), etc<\/span><span style=\"color: #008000;\"><br \/>\n<\/span>loLogger.<span style=\"color: #0000ff;\"><span style=\"color: #0000ff;\">LOG<\/span><\/span>(&#8220;This is a test at level LOGLEVEL_INFO&#8221;, LOGLEVEL_INFO)<br \/>\nloLogger.<span style=\"color: #0000ff;\"><span style=\"color: #0000ff;\">LOG<\/span><\/span>(&#8220;This is a test at level LOGLEVEL_FATAL&#8221;, LOGLEVEL_FATAL)<br \/>\nloLogger.<span style=\"color: #0000ff;\"><span style=\"color: #0000ff;\">CLOSE<\/span><\/span>()<\/p>\n<p>&#8230; honestly, that&#8217;s all you have to do.<\/p>\n<p>The .Open call will cause each of the Appenders to do whatever it needs to do to initialize a log run (some Appenders will need to do a lot of work, some will do none.)<\/p>\n<p>The first .Log call above will be logged by any Appender that successfully initialized its device and is set to log events all the way down to the &#8220;info&#8221; level, which is pretty verbose.<\/p>\n<p>The second .Log call above will be logged by pretty much any Appender that successfully initialized, considering that it&#8217;s hard to imagine wanting any Appender not wanting to include a &#8220;fatal&#8221; level event in its log.<\/p>\n<h5>Okay?<\/h5>\n<p>&nbsp;<\/p>\n<p>Did I leave anything out?<\/p>\n<p>I did tell David that I generally use XML configuration files, rather than INIs or configuration tables.\u00a0 In a follow-up post, he asked me, &#8220;What sort of stuff does the xml config file contain&#8221;?\u00a0 Here&#8217;s a simple example:<\/p>\n<p class=\"code\">&lt;log4j:configuration xmlns:log4j=&#8221;http:\/\/jakarta.apache.org\/log4j\/&#8221;&gt;<\/p>\n<p>&lt;appender name=&#8221;appender&#8221; class=&#8221;org.apache.log4j.FileAppender&#8221;&gt;<br \/>\n&lt;param name=&#8221;File&#8221; value=&#8221;Indentify-Log.txt&#8221;\/&gt;<br \/>\n&lt;param name=&#8221;Append&#8221; value=&#8221;false&#8221;\/&gt;<br \/>\n&lt;layout class=&#8221;org.apache.log4j.PatternLayout&#8221;&gt;<br \/>\n&lt;param name=&#8221;ConversionPattern&#8221; value=&#8221;%d [%t] %p &#8211; %m%n&#8221;\/&gt;<br \/>\n&lt;\/layout&gt;<br \/>\n&lt;\/appender&gt;<\/p>\n<p>&lt;root&gt;<br \/>\n&lt;priority value =&#8221;debug&#8221;\/&gt;<br \/>\n&lt;appender-ref ref=&#8221;appender&#8221;\/&gt;<br \/>\n&lt;\/root&gt;<\/p>\n<p>&lt;\/log4j:configuration&gt;<br \/>\nBut I don&#8217;t have to explain it.\u00a0 You can read many, many, beautifully detailed explanations of the architecture written by other people about log4j, log4net, or any of the other variants, and get an idea of how such a configuration file exposes the API&#8217;s features; I really did follow the log4j principles in my VFP implementation, albeit in a simplified form.<\/p>\n<p>Note: log4j also allows configuration programmatically, or by &#8220;property files&#8221;, which are similar to INIs.\u00a0 You can see an extensive example of a log4j property file <a title=\"some log4j docs\" href=\"http:\/\/www.vipan.com\/htdocs\/log4jhelp.html\" target=\"_blank\" rel=\"noopener\">here<\/a>.<\/p>\n<p>Have event-filled, and fully-loggable, fun.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>David Le Mesurier writes: Hi Lisa, I have been looking at your log4fox code as I want to instrument my applications. Basically I want to be able to instrument them at a client&#8217;s site by setting an ini file setting to turn on the code. I don&#8217;t want the user to have to do anything<a class=\"more-link\" href=\"https:\/\/spacefold.com\/lisa\/2008\/12\/06\/keeping-it-simple-with-log4fox\/\">Read more<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[9],"tags":[],"class_list":["post-114","post","type-post","status-publish","format-standard","hentry","category-visual-foxpro"],"_links":{"self":[{"href":"https:\/\/spacefold.com\/lisa\/wp-json\/wp\/v2\/posts\/114","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/spacefold.com\/lisa\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/spacefold.com\/lisa\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/spacefold.com\/lisa\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/spacefold.com\/lisa\/wp-json\/wp\/v2\/comments?post=114"}],"version-history":[{"count":2,"href":"https:\/\/spacefold.com\/lisa\/wp-json\/wp\/v2\/posts\/114\/revisions"}],"predecessor-version":[{"id":392,"href":"https:\/\/spacefold.com\/lisa\/wp-json\/wp\/v2\/posts\/114\/revisions\/392"}],"wp:attachment":[{"href":"https:\/\/spacefold.com\/lisa\/wp-json\/wp\/v2\/media?parent=114"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/spacefold.com\/lisa\/wp-json\/wp\/v2\/categories?post=114"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/spacefold.com\/lisa\/wp-json\/wp\/v2\/tags?post=114"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}