|
ASP.NET Log File - Felix John COLIBRI.
|
- abstract : an Asp.Net journaling CLASS for debugging and monitoring Web
Developments
- key words : ASP.NET, log, journal, utility, debugging, trace
- software used : Windows XP Home, Delphi 2005
- hardware used : Pentium 2.800 Mhz, 512 M memory, 250 G hard disc
- scope : Delphi 8 , Delphi 2005, Delphi 2006, Windows
- level : Delphi Web Developer
- plan :
1 - Goal of an Asp.Net log file
We explain how to create an Asp.Net log file.
Asp.Net is delivered with all kinds of debugging features. Without rejecting
all those tools, we are so used to indented log files that we started to
implement them as our first step in the Asp.Net world.
One of the reason is that the file log centralizes all debugging text during
the lifetime of a user visit:
- when the user requests a page, the Web Server starts a new AppDomain (a kind
of lightweight thread)
- when the user clicks on a link, this will start another AppDomain
So debugging messages tied to an AppDomain will not survive this execution. The
log file will remain on disk, as long as we want. And can be viewed in case of
crash, telling us the whole story which led to this sad happening.
2 - The Asp.Net log file CLASS
2.1 - Writing to a log file
Writing to the log file is quite easy:
- we create (or open) a FileStream
- we create a StreamWriter to write strings
- then we call StreamWriter.WriteLine( xxx );
Here is a code snippet:
var my_c_stream: System.Io.FileStream;
my_c_stream_writer: StreamWriter;
...
my_c_stream:= FileStream.Create('c:\log\my_log.txt',
FileMode.Open, FileAccess.Write);
my_c_stream_writer:= StreamWriter.Create(l_c_stream);
my_c_stream_writer.BaseStream.Seek(0, SeekOrigin.End);
my_c_stream_writer.WriteLine('hello World');
my_c_stream_writer.Close;
my_c_stream.Close;
|
2.2 - The UNIT
The CLASS definition is:
c_log= Class
m_full_file_name: System.String;
Constructor create_log(p_full_file_name: System.String);
procedure write_to_log(p_text: System.String);
end; // c_log
|
Here is the constructor:
Constructor c_log.create_log(p_full_file_name: System.String);
var l_c_stream: System.Io.FileStream;
begin
Inherited Create;
m_full_file_name:= p_full_file_name;
// -- create an empty file
if not &File.Exists(p_full_file_name)
then begin
l_c_stream:= FileStream.Create(p_full_file_name,
FileMode.Create, FileAccess.Write);
l_c_stream.Close();
end;
end; // create_log
|
with the writing method
procedure c_log.write_to_log(p_text: System.String);
var l_c_stream: System.Io.FileStream;
l_c_stream_writer: StreamWriter;
l_date_time: System.DateTime;
l_date: System.String;
begin
l_c_stream:= FileStream.Create(m_full_file_name,
FileMode.Open, FileAccess.Write);
l_c_stream_writer:= StreamWriter.Create(l_c_stream);
l_c_stream_writer.BaseStream.Seek(0, SeekOrigin.End);
l_date_time:= System.DateTime.Now;
l_date:= System.String.Format(' {0} ',
[l_date_time.ToString('g')]);
l_c_stream_writer.WriteLine(l_date+ ' '+ p_text);
l_c_stream_writer.Close;
l_c_stream.Close;
end; // write_to_log
|
Usually the project using the log declares a c_log variable, and uses this
variable to create and write to the log.
In the Asp.Net case, since each new page lives in a separate AppDomain, this is
not convenient. So we use a self sufficient log:
- on page request creates, and uses the log
- the other pages simply write to the log
This is achieved by using a global variable hidden in the IMPLEMENTATION of
the log UNIT, and a common fixed path and file name.
Here is the creation global procedure:
const k_default_name= 'log.txt';
var _m_c_log: c_log= Nil;
_m_log_name: System.String= '';
_do_log: Boolean= True;
_m_indentation: Integer= 0;
procedure create_asp_net_log(p_full_file_name: System.String);
begin
if _m_log_name= ''
then _m_log_name:= p_full_file_name;
if not _do_log
then Exit;
if _m_c_log<> Nil
then Exit;
if p_full_file_name= ''
then p_full_file_name:= k_default_name;
_m_c_log:= c_log.create_log(p_full_file_name);
end; // create_asp_net_log
|
and the log writing procedure is:
procedure write_to_asp_net_log(p_text: System.String);
begin
if not _do_log
then Exit;
if _m_c_log= Nil
then create_asp_net_log(_m_log_name);
// -- handle the ">", "<" parts
if Pos('<', p_text)> 0
then Dec(_m_indentation);
p_text:= _f_spaces(_m_indentation)+ p_text;
_m_c_log.write_to_log(p_text);
if Pos('>', p_text)> 0
then Inc(_m_indentation);
end; // write_to_asp_net_log
|
where the '>' and '<' are a way to indent / unindent the log output
2.3 - Where should we place the Log File ?
The log file is saved in a separate path, in order to be easily located in the
project. If our project is located in p_xxx\, the we decided to place the log
file in p_xxx\_log\asp_log.txt
Do NOT place the log in the p_xxx\BIN directory. For a complete explanation,
see the the BIN changes story below.
2.4 - When should the log be created ?
Many many papers can be found via Google about "the life of an Asp.Net page".
Oh yes. Could even be called "The secret life of an Asp.Net page", or maybe
"Everything you ever wanted to know about the life of an Asp.Net page".
The sad story is that the mere publishing of those pages demonstrate that
following the flow of control during the rendering of a page is not obvious.
We naturally wanted to create the log in the first event which requires a log
entry. So we first placed the creation in the main .Aspx tWebForm1.Page_Load
or tWebForm1.OnInit events. We later had to investigate Global.Asax, and
found out that events in this file were called before the main page. The
documentation confirms this in the "HTML Pipeline" descriptions. So we placed
the creation in the Global.Asax create event.
2.5 - log example
Here is a simple example of using the log file. First start and name a new
ASP.NET application:
|
start Delphi
|
|
select "file | new | asp.net application"
|
|
Delphi presents the path / file / server dialog
|
|
type the requested information. In our case the project will be located in:
c:\programs\us\web\asp_net\p_asp_net_log_file
so the dialog will look like:
|
|
then, in the top right tree project manager, right click the WebForm1.aspx
line, and rename the file name "a_asp_net_log_file.aspx"
|
|
the project folder will look like this:
|
|
compile and run to make sure that everyting in correct
|
|
Delphi starts Cassini, loads Internet Explorer which displays our
application
|
We have gathered all our helper files in a separate folder which is reachable
from all our projects. To compile our log unit, we tell Delphi where the folder
is located
We then prepare the path for the log
|
open a Windows Explorer, and create the log path. For instance:
c:\programs\us\web\asp_net\p_asp_net_log_file\_asp_log
|
Now we create the log in Global.Asax:
|
then, in the top right tree project manager, click Global.Asax:
|
|
Delphi opens the file in the Editor
|
|
add the unit in the USES after the implementation
|
|
add the log creation in the Global CONSTRUCTOR (there is a "// TODO: Add
any constructor code after InitializeComponent call" that we removed)
constructor TGlobal.Create;
begin
inherited;
InitializeComponent;
create_asp_net_log('c:\programs\us\web\asp_net\'
+ 'p_asp_net_log_file\_asp_log\the_log.txt');
write_to_asp_net_log('======');
write_to_asp_net_log('log_started');
end; // TGlobal.Create
|
|
|
compile and run
|
|
click on the the_log.txt file
|
|
NotePad is started and it displays the log content

|
Here is another example, with a ButtonClick and display of all predefined
events:
3 - Mini Howto
To use our CLASS:
- add the u_c_asp_net_log unit to your Asp.Net project. Also add the dependent
units (all in the zip below)
- add the USES clause to all the .PAS which will either create or write to
the log file
- create the log destination directory. If you want a new log, remove any
existing log.txt
- add the log creation in an event called before any log write
- add all the log write in your .PAS code
4 - Improvements
4.1 - The log in BIN saga
Usually, as other Delphi 6 projects available on this site demonstrate, we
place the .EXE, of a project in a separate directory. This directory also
houses the DCU'x, and a log\ sub directory
BIN is build by default by Asp.Net. So this is fixed. Then we decided to nest a
log\ sub directory in BIN
We then encountered random "Thread Timeout" exceptions, which could not be
explained.
Out of despair we went on Google searching for +"asp.net"+thread timeout".
After a couple of trials, we hit the following page:
Strange thread timeout
behavior again
This was my lucky day since I somehow browsed this 54 K long search and
anguished thread, instead of clicking away after one or two pages, to finally
stumble on the following paragraph (near the end, of course - hilite was
added):
Hi James,
I'm sorry for keeping you waiting for so long time. We've try debuging on
this issue, and finally found that the problem is caused by the log file we
put in the "log" subfolder under the "bin" sub folder of the web
application's root folder. In ASP.NET the asp.net runtime will monitor the
"bin" sub folder of the web app's root folder and the web.config or other
system configuration files. If any of them has been modified, the certain
web application's AppDomain will reboot. As for the situation in our issue,
the webmethod writes a log file in the "log" folder which is under the
"bin" sub directory of the application's root folder, that cause the
ASP.NET runtime detect the changes in the "bin" directory and then recycle
the application's AppDomain. I've tested changed the log file's destination
folder such as under the "log" folder directly under the application's root
folder. Such as :
strLogFileFullFilename = Server.MapPath("Log") +
Path.DirectorySeparatorChar + Format(Now(), "yyyyMMddHHmmss") + ".TXT"
After that, the webmethod will be executed correctly as long as you set
the "TimeOut" attribute in the config file to proper value.
Please check out the preceding result. If you have any questions on it,
please feel free to let me know.
Steven Cheng
Microsoft Online Support
Get Secure! www.microsoft.com/security
(This posting is provided "AS IS", with no warranties, and confers no
rights.)
=====
Re: Strange thread timeout behavior again
Hi! Steven,
Thanks so much you found out the reason finally! I've modified my original
application and proved that the problem is solved.
|
I had read about this automatic recompilation of .ASPX pages, whith all this
shadow copying business and all. But I was miles away from the diagnostic when
the exceptions started hitting the application.
I would certainly question Mr CHENG's postscript "Get Secure with
www.microsoft.com". Must be some kind of Redmond private joke, I guess. But he
did save my day, so, thank you very much, Mr CHENG.
In conclusion: NEVER place any custom file which is updated in the BIN
directory.
4.2 - Programming Notes
- Much could be done to improve this log. We could add a display on a web page
Label, concatenate the logs etc.
- I'm still not very efficient with the C# notation:
- System.Io.File.Exists instead of the FileExists function
- & File to avoid the reserved FILE
- FileAccess.Write because Java (and therefore C#) does not have decent
constants.
But whining and crying is no solution: either stick with Delphi 6, which is
fine, or for Asp.Net development, bite the bullet. In this area, the best
book still remains the Xavier PACHECO's "Delphi 8 for .Net", which is
better than most of the books on Delphi, even the recent Delphi 2006 books
where nearly all authors try to cater to both Vcl.Net and Windows Forms
developments. Fatal error.
- Using this old log file will certainly look outdated in our time and age of
steppers, symbolic debuggers and event logs, but, after over 20 years of
developments, I personally have never found a more efficient tool. I used
one with the Apple UCSD Pascal, then for 6502 assembler, Prolog, Postscript,
8086 assembler, Delphi, of course. And the first thing for .Net I developed
was, of all things, the log file.
5 - Download the Sources
Here are the source code files:
The .ZIP file(s) contain:
- the main program (.DPR, .DOF, .RES), the main form (.PAS, .DFM), and any
other auxiliary form
- any .TXT for parameters, samples, test data
- all units (.PAS) for units
Those .ZIP
- are self-contained: you will not need any other product (unless expressly
mentioned).
- for Delphi 6 projects, can be used from any folder (the pathes are RELATIVE)
- will not modify your PC in any way beyond the path where you placed the .ZIP
(no registry changes, no path creation etc).
To use the .ZIP:
- create or select any folder of your choice
- unzip the downloaded file
- using Delphi, compile and execute
To remove the .ZIP simply delete the folder.
The Pascal code uses the Alsacian notation, which prefixes identifier by
program area: K_onstant, T_ype, G_lobal, L_ocal, P_arametre,
F_unction, C_lasse etc. This notation is presented in the Alsacian Notation
paper.
As usual:
- please tell us at fcolibri@felix-colibri.com if you found some errors, mistakes, bugs or had
some problem downloading the file. Resulting corrections will be helpful
for other readers
- we welcome any comment, criticism, enhancement, other sources or reference
suggestion. Just send an e-mail to fcolibri@felix-colibri.com.
- or more simply, enter your (anonymous or with your e-mail if you want an
answer) comments below and clic the "send" button
- and if you liked this article, talk about this site to your fellow
developpers, add a link to your links page ou mention our articles in
your newsgroup posts when relevant. That's the way we operate: the more
traffic and Google references we get, the more articles we will write.
6 - The author
Felix John COLIBRI works at the Pascal
Institute. He programs in Pascal since 1979, and is mainly active in the area
of custom software
development and training, and is a frequent speaker at Borland
Developer Conferences. His web site features
tutorials, technical papers about programming with full downloadable source
code, and the description and calendar of forthcoming Delphi,
Interbase, Asp.Net, Ado.Net and OOP / UML training sessions.
|