Search code examples
c#tfsversion-controlazure-devopssystem-testing

How do I programmatically report the TFS/AzureDevOps changeset number in my logs


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?


Solution

  • 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...

    1. Create an empty build info file and check it in with your source code. This file is just a reference example--use placeholder data. It can be XML, JSON, or any other format (but should probably be text-based). Set the file's build action to "Content," so it will be included in the build output.
    2. Use your logging framework's template or telemetry initialization functionality to read the build info file and include its contents in log output. The exact implementation will depend on your logging framework.
    3. Add a Powershell script task to your Azure DevOps pipeline. The script should either update or overwrite the build info file. You can get some good info from build variables. The task should run before the MSBuild compilation step.

    Create and Parse a BuildInfo file

    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"];
        }
    }
    

    Add the Build Info to your Log Output

    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
    

    Get Build Details from CI

    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.