WBuck
WBuck

Reputation: 5503

How to wire up Serilog to .NET Aspire

I've been struggling trying to get Serilog to send telemetry over to the .NET Aspire dashboard. Once the Aspire dashboard opens, the Structured logs tab is always empty.

If I comment out the Serilog configuration in Program.cs (thus using the default ILogger) then I start seeing structured logs on the dashboard.

There are currently two issues on the Aspire repo which didn't seem to help me:

This is where our Serilog configuration stands at the moment:

// This extension is auto generated when adding Aspire. More on that below..
builder.AddServiceDefaults();

builder.Host.UseSerilog((hostContext, services, loggerConfiguration) => loggerConfiguration
    .ReadFrom.Configuration(builder.Configuration)
    .Enrich.FromLogContext()
    .Enrich.WithPropertiesMasking(builder.Configuration)
    .Enrich.WithProperty("ServiceName", DaprConstants.API_SERVICE_NAME)
    .Enrich.WithThreadId()
    .Enrich.WithCorrelationIdHeader("X-Correlation-ID")
    .WriteTo.ApplicationInsights
    (        
        new TelemetryConfiguration() { ConnectionString = builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"] },
        TelemetryConverter.Traces
    )
    .WriteTo.OpenTelemetry(opts =>
    {
        opts.Endpoint = builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]!;
        opts.ResourceAttributes.Add("service.name", builder.Configuration["OTEL_SERVICE_NAME"] ?? "Unknown");
    })
);

If you're unfamiliar with some of those environment variables you can read more about it here: .NET Aspire telemetry

From the GitHub issues I linked to above I've also modified the Extensions within the ServiceDefaults project that gets auto generated when you add Aspire to your application. I should note that I also tried keeping all the configuration in the Extensions class the same as well.

public static class Extension
{
    public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
    {
        builder.ConfigureOpenTelemetry();

        /* Other configuration... */

        return builder;
    }

    public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)
    {
        // Tried commenting this out.
        /*builder.Logging.AddOpenTelemetry(logging =>
        {
            logging.IncludeFormattedMessage = true;
            logging.IncludeScopes = true;
        });*/

        builder.Services.AddOpenTelemetry()
            .WithMetrics(metrics =>
            {
                metrics.AddAspNetCoreInstrumentation()
                    .AddHttpClientInstrumentation()
                    .AddRuntimeInstrumentation();
            })
            .WithTracing(tracing =>
            {
                if (builder.Environment.IsDevelopment())
                {
                    // We want to view all traces in development
                    tracing.SetSampler(new AlwaysOnSampler());
                }

                tracing.AddAspNetCoreInstrumentation()
                    .AddHttpClientInstrumentation();
            });

        builder.AddOpenTelemetryExporters();

        return builder;
    }

    private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)
    {
        var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);

        if (useOtlpExporter)
        {
            // In one of the GitHub issues David Fowler suggested commenting this line out.
            //builder.Services.Configure<OpenTelemetryLoggerOptions>(logging => logging.AddOtlpExporter());
            builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter());
            builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter());
        }

        if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
        {
            builder.Services.AddOpenTelemetry()
               .UseAzureMonitor();
        }

        return builder;
    }
}

I didn't show the AppHost here as I didn't think it mattered to the aforementioned issue.

Has anyone who is currently trying out Aspire with Serilog been able to get this to work correctly?

If so, any pointers?

Upvotes: 5

Views: 3765

Answers (2)

M. Dommer
M. Dommer

Reputation: 31

This is automatically supported now with little effort:

dotnet add package Serilog.Sinks.OpenTelemetry

Setup your serilog configuration in your appsettings.json file or appsettings.development.json file. The key here is to configure to WriteTo OpenTelemetry sink.

"Serilog": {
  "MinimumLevel": {
    "Default": "Information"
  },
  "WriteTo": [
    {
      "Name": "Console"
    },
    {
      "Name": "File",
      "Args": {
        "path": "log.log",
        "rollingInterval": "Day"
      }
    },
    {
      "Name": "OpenTelemetry"
    }
  ]
},

Finally, add this to the WebApplicationBuilder

builder.Host.UseSerilog((_, config) => config.ReadFrom.Configuration(builder.Configuration));

Profit Aspire Structured Logs Screenshot

Upvotes: 3

Chinmay T
Chinmay T

Reputation: 1133

I was facing the same issue, after digging GitHub posts, I am able to wire up Serilog with Aspire dashboard's Structured Logs tab. I found this at here on GitHub and thanks to David Fowler's reply I am able to wired it up properly.

builder.Host.UseSerilog((ctx, lc ) => lc
        .Enrich.FromLogContext()        
        .WriteTo.OpenTelemetry(options =>
        {
options.Endpoint = builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"];
            var headers = builder.Configuration["OTEL_EXPORTER_OTLP_HEADERS"]?.Split(',') ?? [];
            foreach (var header in headers)
            {
                var (key, value) = header.Split('=') switch
                {
                [string k, string v] => (k, v),
                    var v => throw new Exception($"Invalid header format {v}")
                };

                options.Headers.Add(key, value);
            }
            options.ResourceAttributes.Add("service.name", "apiservice");

            //To remove the duplicate issue, we can use the below code to get the key and value from the configuration

            var (otelResourceAttribute, otelResourceAttributeValue) = builder.Configuration["OTEL_RESOURCE_ATTRIBUTES"]?.Split('=') switch
            {
            [string k, string v] => (k, v),
                _ => throw new Exception($"Invalid header format {builder.Configuration["OTEL_RESOURCE_ATTRIBUTES"]}")
            };

            options.ResourceAttributes.Add(otelResourceAttribute, otelResourceAttributeValue);

           
          
        })
        .ReadFrom.Configuration(ctx.Configuration)
);

This is how it looks on Aspire Dashboard under Structured Logs tab.

aspire dashboard

After updating code to remove duplicate. This is how it looks under the Aspire dashboard.

Aspire dashboard with no duplicates

Upvotes: 5

Related Questions