Search code examples
functiongoogle-cloud-platformserilogstructured-data

Serilog structured data doesn't work in Google Cloud functions with C# / .NET


We're using Serilog (wrapped in a .NET ILogger) to perform logging in Google Cloud functions written using .NET Core, and also in ASP.NET Core APIs that are in Kubernetes containers in Google Cloud. When we try to add structured data to show up in Google's jsonPayload in each logged event, it works for the ASP.NET Core services, but doesn't work for the .NET Core functions.

Here is the code we're using. The first three batches of code are how we initialize Serilog, separately for APIs and functions, plus their shared config dependency. After those examples is how we add Serilog LogContext information that should appear in Google's jsonPayload for APIs and functions. After those examples is the appsettings.json content for logging, which is pretty standard.

Short version of "what we're expecting" is for our arbitrary DTO to show up in properties after our message in jsonPayload when we look in Google Cloud Logs Explorer and examine any logging from the function.

Our thinking has focused on the fact that the APIs have different "host builder" objects than the .NET Core objects, even though both host builders implement IHostBuilder. But that could easily be wrong. Someone let us know!

And yes, we do have all the prescribed NuGet packages installed.

Initialization code: APIs

    public static WebApplicationBuilder AddStructuredLoggerToCloudApiService(this WebApplicationBuilder builder)
    {
        var definition = DefineLoggingSetup(builder.Configuration);
        var logger = definition.CreateLogger();

        builder.Host.ConfigureLogging(logging => logging.AddSerilog(logger));

        // Chainable.
        return builder;
    }

Initialization code: functions

    public static IHostBuilder AddStructuredLoggerToCloudFunction(this IHostBuilder builder, WebHostBuilderContext context)
    {
        var definition = DefineLoggingSetup(context.Configuration);
        var logger = definition.CreateLogger();

        builder.ConfigureLogging(logging => logging.AddSerilog(logger));

        // Chainable.
        return builder;
    }

Initialization code: shared config dependency

    private static LoggerConfiguration DefineLoggingSetup(IConfiguration config)
    {
        var definition = new LoggerConfiguration()
            .ReadFrom.Configuration(config)   // Scoped to calling code: sinks, message template.
            .Enrich.FromLogContext();         // Supports later AddLogData, etc.

        return definition;
    }

Using LogContext

    public static IDisposable AddLogData<TLogData>(this ILogger _, TLogData data)
    {
        return LogContext.PushProperty(typeof(TLogData).Name, data, true);
    }

Logging config in appsettings.json

  "Serilog": {
    "Using": [ "Serilog.Sinks.GoogleCloudLogging" ],
    "MinimumLevel": "Information",
    "WriteTo": [
      {
        "Name": "GoogleCloudLogging",
        "Args": {
          "projectID": "************",
          "restrictedToMinimumLevel": "Information",
          "outputTemplate": "****=> {Timestamp:HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}"
        }
      }
    ]
  }

(The exact GCP project ID has been redacted.)


Solution

  • In case this problem ever arises for anyone else, I'm posting the answer here, though I hate ending up having to answer my own questions in StackOverflow.

    Short answer: When you're using a Google Cloud function, you have to set up logging in an override of a different predefined method, ConfigureLogging(). The builder in this method is an ILoggingBuilder, irregularly named logging by default, and the context here is a WebHostBuilderContext.

    Our final code there looks like this:

    logging.AddStructuredLoggerToCloudFunction(context);
    

    The definition of AddStructured___() had to change slightly to use the ILoggingBuilder, but that was the only change.

    That's all there is to it.