I sometimes find myself binary searching through changesets and running tests to determine when a defect was introduced. This leaves me with a lot of very similar logs to sift through, and sometimes I have trouble remembering which diagnostic file came from which changeset run.
I would like to include the changeset number in the diagnostic files somehow. Is there any way to do this with Visual Studio/C#/AzureDevOps?
Microsoft used to support a file called BuildInfo.config
that was useful for this exact sort of thing, but I think support for it was dropped after Visual Studio 2015.
Basically, it was a simple XML file that was created by MSBuild. When deployed alongside the app binaries, a logging framework could read the file and include the details in log output.
There are a bunch of ways you might create similar functionality yourself. Here's what I'd do...
For this example, I'm going to implement my build info file as simple key/value pairs in a text file. I chose simple text because it allows a clear example without requiring any third-party libraries or complex parsing. In your app, you might want to use JSON, XML, or something else more standardized.
First, create a placeholder build info file. This file will be used in your
local dev environment, and will serve as a reference for your build info schema.
I called mine BuildInfo.txt
, and put it in my project's root directory.
COMMIT=commit not set
BUILD=build not set
Next, write a helper to parse the build info file. Mine's very rudimentary. It would be wise to add some defenses against missing or malformed build info, but I chose to omit any defensive code to keep the example focused.
class BuildInfo
{
// Singleton instance backing field
static readonly Lazy<BuildInfo> instance = new Lazy<BuildInfo>(() => new BuildInfo());
// Singleton instance public accessor
public static BuildInfo Instance => instance.Value;
public string Commit { get; }
public string Build { get; }
private BuildInfo()
{
// This is a very rudimentary example of parsing the info file. It
// will fail loudly on malformed input. Consider:
// 1) Using a standard file format (JSON, XML). I rolled my own
// here to avoid adding a dependency on a parsing library.
// 2) If you want your app to fail when no build info is
// available, add a more descriptive exception. If you don't
// want it to fail, add some devensive code or fallback logic.
var info = File.ReadAllLines("BuildInfo.txt")
.Select(l => l.Split('=', 2))
.ToDictionary(key => key[0], val => val[1]);
Commit = info["COMMIT"];
Build = info["BUILD"];
}
}
I'm using log4net here, but any logging framework should have some similar mechanism to customise the log output. Consult the framework's documentation for more info.
First, add your build info to the logging context. This should go somewhere in your app's startup code--as early as possible.
log4net.GlobalContext.Properties["Build"] = BuildInfo.Instance.Build;
log4net.GlobalContext.Properties["Commit"] = BuildInfo.Instance.Commit;
Then, update your log appender's configuration to include the custom fields.
Here's an example configuration for the the console appender. The important bits
are %property{BuildId}
and %property{Commit}
.
<?xml version="1.0" encoding="utf-8" ?>
<log4net>
<appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
<target value="Console.Error" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%-5level [%property{BuildId}] [%property{Commit}] - %message%newline" />
</layout>
</appender>
<root>
<level value="ALL"/>
<appender-ref ref="ConsoleAppender" />
</root>
</log4net>
Now, when you call log.Warn("Some log mesage")
, you'll see the following
console output:
WARN [build not set] [commit not set] - Some log mesage
Finally, you need to get the real build details from your CI environment. I did this with a very simple PowerShell script task. Make sure the task runs before your build step!
@(
"COMMIT=$($Env:BUILD_SOURCEVERSION)",
"BUILD=$($Env:BUILD_BUILDID)"
) | Out-File BuildInfo.txt
(Tip: You can see all of the environment variables available by running Get-ChildItem Env: | Sort Name
in a PowerShell task)
(Another tip: if you want to use JSON instead of text, take a look at the ConvertTo-Json
cmdlet)
Now, if all of the pieces are in place, the CI server should overwrite the checked-in build info file. The new file should then be packages with your deployable artifacts, and copied to your server. On startup, your app should read the build info, and then the info should be included in every log message.
There are a lot of little things to get lined up between your build and deploy process, so be prepared for some trial and error. CI/CD setup tends to be tedious, in my experience.