Skip to main content

Using NLog with BimlExpress for BimlScript Logging

Why Add Logging

When a BimlScript file becomes complex, debugging it through trial and error gets slow. Writing diagnostic messages from inside the script speeds the loop, and on Windows Server 2012 and later the older 'System.Diagnostics.Debug.WriteLine' plus DebugViewer combination no longer reliably surfaces those messages. Routing diagnostics to a log file works in any host and produces an artifact that can be opened in any text editor.

The NLog framework handles the file output cleanly. Adding it to a BimlScript project takes one shared logging file and an include directive in any other Biml file that wants to log.

Download and Stage NLog

Download the standard build of NLog and unpack it into a known directory, for example a folder named 'NLog-3.0' inside the SSIS solution. The full NLog distribution is unnecessary; the standard package is enough.

If the NLog assembly was downloaded from the internet, Windows marks it as blocked. The compiler refuses to load a blocked assembly through an assembly directive. Right click the .dll file, open Properties, and click Unblock. Without that step the include below fails to resolve at compile time.

The Logging File

Create a Biml file named 'BimlLogger.biml' next to the other Biml files. The file declares the NLog assembly through an 'assembly' directive, imports the namespaces it needs, and configures a single file target for the logger:

<#@ template language="C#" #>
<#@ assembly name="C:\Work\BimlSolution\NLog-3.0\net40\NLog.dll" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="NLog" #>
<#@ import namespace="NLog.Config" #>
<#@ import namespace="NLog.Targets" #>

<#
Logger logger = LogManager.GetLogger("BIML");

LoggingConfiguration config = new LoggingConfiguration();

FileTarget fileTarget = new FileTarget();
config.AddTarget("file", fileTarget);

fileTarget.FileName = @"${nlogdir}\..\..\biml.log";
fileTarget.Layout = "${longdate}|${level:uppercase=true}|${message}";
fileTarget.DeleteOldFileOnStartup = true;

LoggingRule loggingRule = new LoggingRule("*", LogLevel.Trace, fileTarget);
config.LoggingRules.Add(loggingRule);

LogManager.Configuration = config;
#>

Adjust the path on the 'assembly' line to match the actual location of NLog.dll on the machine. The 'FileName' value uses NLog's relative path token so the log file lands in the NLog folder by default; tighten it to whatever location suits the project.

Logging From a Generator Script

Any Biml file that wants to log includes 'BimlLogger.biml' and then calls 'logger.Info', 'logger.Warn', or any other NLog method. The shared file does the configuration once, and the variable 'logger' is in scope wherever the include is brought in:

<#@ template language="C#" #>
<#@ include file="BimlLogger.biml" #>

<#
logger.Info("Generating Package...");
#>

<Biml xmlns="http://schemas.varigence.com/biml.xsd">
<Packages>
<Package Name="DiagnosticsSample" ConstraintMode="Linear" ProtectionLevel="EncryptSensitiveWithUserKey" />
</Packages>
</Biml>

<#
logger.Info("Done.");
#>

When this file is generated, NLog appends a line per call to the configured log file. Switching the log level on the 'LoggingRule' from 'Trace' to 'Debug' or 'Info' filters which messages are written, which is useful when a script becomes chatty during development but should remain quieter once it is stable.

Where to Take It

The NLog documentation covers many other targets: rolling files, the event log, the console, network sinks, and structured destinations. Anything NLog can write to is reachable from a BimlScript file once the configuration step in 'BimlLogger.biml' is updated. For complex generation pipelines, log levels and target separation help slice script output by phase, table, or feature so the resulting log file remains useful when something goes wrong.