Unified logging in Azure Functions and Azure Cloud Services

In a recent IoT project, I've written an Azure Cloud Service and an Azure Function.

They share a few functions. The shared code is implemented as a shared project in Visual Studio. Inside these functions, I needed to log information.

The Azure Function uses .NET Core 2.1
The Cloud Service uses .NET Framework 4.6.1

For Cloud Services, the recommended way to log is to use static calls to System.Diagnostics.Trace. Reference: https://docs.microsoft.com/en-us/azure/azure-monitor/app/cloudservices

Trace.TraceInformation("This is an informational message");
Trace.TraceError("ERROR!");

For Azure Functions, the recommended way to is to use dependency injection of Microsoft.Extensions.Logging.ILogger. Reference: https://docs.microsoft.com/en-us/azure/azure-functions/functions-monitoring#enable-application-insights-integration

public static async Task Run(ILogger log)
{
    log.LogInformation("This is an informational message");
    log.LogError("ERROR!");
}

If I choose to log the Trace. way in the shared code, the project won't compile for the Azure function.
If I choose to log the ILogger way in the shared code, the project won't compile for the Cloud Service.

I posted a question in the Microsoft forum, but the MVPs there were not aware of any solution. They did suggest multiple times that I should close the question with the answer "not possible" though.

Solution

The solution I selected uses preprocessor directives (called compilation symbols in Visual Studio), which are used by the the C# preprocessor. In the Solution Explorer, right-click your project (not the solution) and choose Properties. Then click on "Build".

I then created a logging utility class:

public class MyLogger
{
    private List<ILogger> iLoggers = new List<ILogger>();
    public void AddILogger(ILogger logger)
    {
        LogInformation($"MyLogger is adding ILogger {logger}. {iLoggers.Count} ILoggers have already been added.");
        iLoggers.Add(logger);
        LogInformation($"MyLogger added ILogger {logger}. {iLoggers.Count} ILoggers are now registered.");
    }
    public void LogInformation(string message)
    {
        bool hasLogged = false;
        foreach (ILogger l in iLoggers)
        {
            l.LogInformation(message);
            hasLogged = true;
        }
#if AZURECLOUDSERVICE
        Trace.TraceInformation(message);
        hasLogged = true;
#endif
        if (!hasLogged)
        {
            // Fallback to console logging
            Console.WriteLine(message);
        }
    }

    public void LogError(string message)
    {
        foreach (ILogger l in iLoggers)
        {
            l.LogError(message);
            hasLogged = true;
        }
#if AZURECLOUDSERVICE
        Trace.TraceError(message);
        hasLogged = true;
#endif
        if (!hasLogged)
        {
            // Fallback to console logging
            Console.Error.WriteLine(message);
        }
    }
}

The Cloud Service code needs to be modified to look like this:

MyLogger log = new MyLogger();
log.LogInformation("This is an informational message");
log.LogError("ERROR!");

The Azure function needs to be modified to look like this:

public static async Task Run(ILogger injected_log){
    MyLogger log = new MyLogger();
    log.AddILogger(injected_log);
    log.LogInformation("This is an informational message");
    log.LogError("ERROR!");
}

There might be a better way, maybe using C# conditionals, or using some compilation symbol that are defined automatically by the build environment. If you have suggestions on how to improve, please post a comment.