4. Logging to a Database
One of the most common requirements for logging, after Windows
Event Log and text files, is to store log entries in a database. The
Logging block contains the database trace listener that makes this easy. You configure the
database using a script provided with Enterprise Library, located in
the \Blocks\Logging\Src\DatabaseTraceListener\Scripts folder of the
source code.
The scripts assume that you will use the locally installed SQL
Server Express database, but you can edit the CreateLoggingDb.cmd file to change the target
to a different database server. The SQL script that the command file
executes creates a database named Logging, and adds the required tables and
stored procedures to it.
The
project contains a preconfigured database file named Logging.mdf
(located in the bin\Debug folder) that is auto-attached to your local
SQL Server Express instance. You can connect to this database using
Visual Studio Server Explorer to see the contents. The configuration
of the database trace listener contains the Database Instance property, which is a
reference to this database as configured in the settings section for
the Data Access application block (see Figure 2).
The database trace listener uses a text formatter to format the
output, and so you can edit the template used to generate the log
message to suit your requirements. You can also add extended
properties to the log entry if you wish. In addition, as with all
trace listeners, you can filter log entries based on
their severity if you like.
The Log table in the database
contains columns for only the commonly required values, such as the
message, event ID, priority, severity, title, timestamp, machine and
process details, and more. It also contains a column named FormattedMessage that contains the message
generated by the text formatter.
4.1. Using the Database Trace Listener
The example, Sending log entries to a
database, demonstrates the use of the database trace
listener. The code is relatively simple, following the same style as
the earlier example of creating a Dictionary of extended properties, and then
using the Write method of the LogWriter to write two log entries. The first
log entry is created by the LogWriter
from the parameter values provided to the Write method. The second is generated in code
as a new LogEntry instance by
specifying the values for the constructor parameters. Also notice
how easy it is to add additional information to a log entry using a
simple Dictionary as the ExtendedProperties of the log
entry.
// Check if logging is enabled before creating log entries.
if (defaultWriter.IsLoggingEnabled())
{
// Create a Dictionary of extended properties
Dictionary<string, object> exProperties = new Dictionary<string, object>();
exProperties.Add("Extra Information", "Some Special Value");
// Create a LogEntry using the constructor parameters.
defaultWriter.Write("Log entry with category, priority, event ID, severity, "
+ "title, and extended properties.", "Database",
5, 9008, TraceEventType.Warning,
"Logging Block Examples", exProperties);
// Create a LogEntry using the constructor parameters.
LogEntry entry = new LogEntry("LogEntry with category, priority, event ID, "
+ "severity, title, and extended properties.",
"Database", 8, 9009, TraceEventType.Error,
"Logging Block Examples", exProperties);
defaultWriter.Write(entry);
}
else
{
Console.WriteLine("Logging is disabled in the configuration.");
}
To see the two log messages created by this example, you can
open the Logging.mdf database from the bin\Debug folder using Visual
Studio Server Explorer. You will find that the FormattedMessage column of the second message
contains the following. You can see the extended property
information we added using a Dictionary at the end of the message.
Timestamp: 03/12/2009 17:14:02
Message: LogEntry with category, priority, event ID, severity, title, and extended
properties.
Category: Database
Priority: 8
EventId: 9009
Severity: Error
Title: Logging Block Examples
Activity ID: 00000000-0000-0000-0000-000000000000
Machine: BIGFOOT
App Domain: LoggingExample.vshost.exe
ProcessId: 5860
Process Name: E:\Logging\Logging\bin\Debug\LoggingExample.vshost.exe
Thread Name:
Win32 ThreadId:3208
Extended Properties: Extra Information - Some Special Value
Note that you cannot simply delete logged information due to
the references between the Log and
CategoryLog tables. However, the
database contains a stored procedure named ClearLogs that you can execute to remove all
log entries.
Note:
The connection string for the database we provide
with this example is:
Data
Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\Logging.
mdf;Integrated Security=True;User Instance=True
If you have configured a different database using
the scripts provided with Enterprise Library, you may find that
you get an error when you run this example. It is likely to be
that you have an invalid connection string in your App.config file
for your database. In addition, use the Services applet in your
Administrative Tools folder to check that the SQL Server
(SQLEXPRESS) database service (the service is named
MSSQL$SQLEXPRESS) is
running.
5. Testing Logging Filter Status
As you've seen in earlier examples, the Logging block allows you
to check if logging is enabled before you create and write a log
entry. You can avoid the additional load that this places on your
application if logging is not enabled. However, even when logging is
enabled, there is no guarantee that a specific log entry will be
written to the target log store. For example, it may be blocked by a
priority filter if the message priority is below a specified level, or
it may belong only to one or more categories where the relevant
category filter(s) have logging disabled (a common scenario in the
case of logging code specifically designed only for debugging
use).
The example, Checking filter status and adding context
information to the log entry, demonstrates how you can
check if a specific log entry will be written to its target before you
actually call the Write method. After
checking that logging is not globally disabled, the example creates
two LogEntry instances with different
categories and priorities. It passes each in turn to another method
named ShowDetailsAndAddExtraInfo. The
following is the code that creates the LogEntry instances.
// Check if logging is enabled before creating log entries.
if (defaultWriter.IsLoggingEnabled())
{
// Create a new LogEntry with two categories and priority 3.
string[] logCategories = new string[] {"General", "DiskFiles"};
LogEntry entry1 = new LogEntry("LogEntry with categories 'General' and "
+ "'DiskFiles' and Priority 3.", logCategories,
3, 9009, TraceEventType.Error,
"Logging Block Examples", null);
ShowDetailsAndAddExtraInfo(entry1);
// Create a new LogEntry with one category and priority 1.
logCategories = new string[] { "BlockedByFilter" };
LogEntry entry2 = new LogEntry("LogEntry with category 'BlockedByFilter' and "
+ "Priority 1.", logCategories, 1, 9010,
TraceEventType.Information,
"Logging Block Examples", null);
ShowDetailsAndAddExtraInfo(entry2);
}
else
{
Console.WriteLine("Logging is disabled in the configuration.");
}
The ShowDetailsAndAddExtraInfo
method takes a LogEntry instance and
does two different things. Firstly, it shows how you can obtain
information about the way that the Logging block will handle the log
entry. This may be useful in advanced scenarios where you need to be
able to programmatically determine if a specific log entry was
detected by a specific trace source, or will be written to a specific
target. Secondly, it demonstrates how you can check if specific
filters, or all filters, will block a log entry from being written to
its target.
5.1. Obtaining Information about Trace Sources and Trace
Listeners
The first section of the ShowDetailsAndAddExtraInfo method iterates
through the collection of trace sources (LogSource instances) exposed by the GetMatchingTraceSources method of the LogWriter class. Each LogSource instance exposes a Listeners collection that contains
information about the listeners (which specify the targets to which
the log entry will be sent).
void ShowDetailsAndAddExtraInfo(LogEntry entry)
{
// Display information about the Trace Sources and Listeners for this LogEntry.
IEnumerable<LogSource> sources = defaultWriter.GetMatchingTraceSources(entry);
foreach (LogSource source in sources)
{
Console.WriteLine("Log Source name: '{0}'", source.Name);
foreach (TraceListener listener in source.Listeners)
{
Console.WriteLine(" - Listener name: '{0}'", listener.Name);
}
}
...
5.2. Checking if Filters Will Block a Log Entry
Next, the ShowDetailsAndAddExtraInfo method checks if
any filters will block the current log entry. There are two ways you
can do this. You can query each filter type in turn, or just a
specific filter type, by using the GetFilter method of the LogWriter class to get a reference to that
type of filter. Then you can check if this filter is enabled, and
also use the ShouldLog method (to
which you pass the list of categories for the log entry) to see if
logging will succeed.
The following code shows this approach. It also shows the
simpler approach that you can use if you are not interested in the
result for a specific filter type. The LogWriter class also exposes the ShouldLog method, which indicates if any
filters will block this entry.
...
// Check if any filters will block this LogEntry.
// This approach allows you to check for specific types of filter.
// If there are no filters of the specified type configured, the GetFilter
// method returns null, so check this before calling the ShouldLog method.
CategoryFilter catFilter = defaultWriter.GetFilter<CategoryFilter>();
if (null == catFilter || catFilter.ShouldLog(entry.Categories))
{
Console.WriteLine("Category Filter(s) will not block this LogEntry.");
}
else
{
Console.WriteLine("A Category Filter will block this LogEntry.");
}
PriorityFilter priFilter = defaultWriter.GetFilter<PriorityFilter>();
if (null == priFilter || priFilter.ShouldLog(entry.Priority))
{
Console.WriteLine("Priority Filter(s) will not block this LogEntry.");
}
else
{
Console.WriteLine("A Priority Filter will block this LogEntry.");
}
// Alternatively, a simple approach can be used to check for any type of filter
if (defaultWriter.ShouldLog(entry))
{
Console.WriteLine("This LogEntry will not be blocked by config settings.");
....
// Add context information to log entries after checking that the log entry
// will not be blocked due to configuration settings. See the following
// section 'Adding Additional Context Information' for details.
....
}
else
{
Console.WriteLine("This LogEntry will be blocked by configuration settings.");
}
}
After you determine that logging will succeed, you can add extra context
information and write the log entry. You'll see the code to achieve
this shortly. In the meantime, this is the output generated by the
example. You can see that it contains details of the log (trace) sources and listeners for each of the two log
entries created by the earlier code, and the result of checking if any category filters will block each log
entry.
Created a LogEntry with categories 'General' and 'DiskFiles'.
Log Source name: 'General'
- Listener name: 'Formatted EventLog TraceListener'
Log Source name: 'DiskFiles'
- Listener name: 'FlatFile TraceListener'
- Listener name: 'XML Trace Listener'
Category Filter(s) will not block this LogEntry.
Priority Filter(s) will not block this LogEntry.
This LogEntry will not be blocked due to configuration settings.
...
Created a LogEntry with category 'BlockedByFilter', and Priority 1.
Log Source name: 'BlockedByFilter'
- Listener name: 'Formatted EventLog TraceListener'
A Category Filter will block this LogEntry.
A Priority Filter will block this LogEntry.
This LogEntry will be blocked due to configuration settings.