-
Notifications
You must be signed in to change notification settings - Fork 209
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Serilog for Dotnet Aspire #359
Comments
Did you configure Serilog to write to dashboard? (the configured OTLP server?). See this doc. It explains how the system currently works. To make serilog work with aspire, you need to tell to send serilog logs to the otlp endpoint. |
indeed, i forgot to configure the OTLP exporter.
|
I filed dotnet/aspire-samples#106 |
Hello @BartNetJS We attempted adding Serilog as well. @davidfowl is this expected? Created another static method in ServiceDefaults public static void AddCustomSerilog(this IHostApplicationBuilder builder)
} Note: Ignore the Seq bits! The App Name is configured in settings as shown below and finally the projects are added in the AppHost as shown below: var builder = DistributedApplication.CreateBuilder(args); builder.AddProject<Projects.Insights_Grpcs_Agents>("agentsgrpc") builder.AddProject<Projects.Insights_Grpcs_Analytics>("analyticsgrpc") builder.AddProject<Projects.Insights_Worker_Collector>("collectorgrpc") builder.Build().Run(); as we see AppName:"agents" and sidecare is registered with "agents". But seems, telemetry is identifying them as two different sources and appending random strings to ensure listing of the sources. Any clues? |
Did you remove the other logger provider that sends telemetry directly to the dashboard or are you logging with both serilog and the default setup? |
The only extra thing needed was to clear the providers, it was already there in our production code but I missed during customiztation of the code snippet. public static void AddCustomSerilog(this IHostApplicationBuilder builder) if (string.IsNullOrEmpty(appName)) var seqServerUrl = builder.Configuration["SeqServerUrl"]!; var logBuilder = new LoggerConfiguration() if(!string.IsNullOrEmpty(useOtlpExporter)) Seems like @TGrannen has beaten me for the sample. ;-) |
This used to work - until I updated to latest VS Preview. Now only the Console and File sinks create Serilog logs but I get nothing in Structured in Dashboard. I confirmed that OTEL_EXPORTER_OTLP_ENDPOINT is correct. And if I call ClearProviders before AddSerilog, I get no structured logs in any sink. Any ideas? |
I added in appsettings.json:
And now the code sends the configured api key to the OTLP collector endpoint in the extension method:
But still I don't see the logs in Structured in Dashboard. What else has to change? |
Any update? Should I open an issue for this? |
Easiest way to make this would is to read the official environment variables and passing them to serilog (https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/#otel_exporter_otlp_headers)
logBuilder.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");
}); PS: It would be good to file an issue on serilg to read the default OTLP environment variables. |
This works, thanks! The only thing is that it creates a second "apiservice" log in Structured. Even with I'll create an issue for the headers. |
Where are you clearing providers? Before or after the otlp logger? |
Tried both before and after, but neither remove the duplicate log:
|
Where is this called in relation to AddServiceDefaults? |
After builder.AddServiceDefaults(); |
I used the actual service name to register the Serilog log resource for the apiservice, e.g. |
@davidfowl Thanks for your reply. This is really helpful. |
I had the same duplication issue and it worked for me when I added the the var (otelResourceAttribute, otelResourceAttributeValue) = configuration["OTEL_RESOURCE_ATTRIBUTES"]?.Split('=') switch
{
[string k, string v] => (k, v),
_ => throw new Exception($"Invalid header format {configuration["OTEL_RESOURCE_ATTRIBUTES"]}")
};
options.ResourceAttributes.Add(otelResourceAttribute, otelResourceAttributeValue); |
I've been facing a similar issue with using Serilog alongside OpenTelemetry, particularly around logs either not being sent or being duplicated. After fixing the configuration using suggestions from @davidfowl, logs started showing up correctly but were duplicated. Following @islamkhattab advice, logs appeared under the same service, but duplication persisted. Both logs from OpenTelemetry and Serilog were appearing under the same service. For some reason, even after removing the OpenTelemetry logging provider, logs were still being sent from there. Using The solution I found was to use Serilog's When Serilog is registered using the public static IHostApplicationBuilder ConfigureSerilog(this IHostApplicationBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
builder.Logging.ClearProviders();
Action<LoggerConfiguration> configureLogger = config =>
{
config.ReadFrom.Configuration(builder.Configuration)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithProcessId()
.Enrich.WithProcessName()
.Enrich.WithThreadId()
.Enrich.WithSpan()
.Enrich.WithExceptionDetails(new DestructuringOptionsBuilder()
.WithDefaultDestructurers()
.WithDestructurers(new[] { new DbUpdateExceptionDestructurer() }))
.Enrich.WithProperty("Environment", builder.Environment.EnvironmentName)
.WriteTo.Console()
.WriteTo.OpenTelemetry(options =>
{
options.IncludedData = IncludedData.TraceIdField | IncludedData.SpanIdField;
options.Endpoint = builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"];
AddHeaders(options.Headers, builder.Configuration["OTEL_EXPORTER_OTLP_HEADERS"]);
AddResourceAttributes(options.ResourceAttributes, builder.Configuration["OTEL_RESOURCE_ATTRIBUTES"]);
void AddHeaders(IDictionary<string, string> headers, string headerConfig)
{
if (!string.IsNullOrEmpty(headerConfig))
{
foreach (var header in headerConfig.Split(','))
{
var parts = header.Split('=');
if (parts.Length == 2)
{
headers[parts[0]] = parts[1];
}
else
{
throw new Exception($"Invalid header format: {header}");
}
}
}
}
void AddResourceAttributes(IDictionary<string, object> attributes, string attributeConfig)
{
if (!string.IsNullOrEmpty(attributeConfig))
{
var parts = attributeConfig.Split('=');
if (parts.Length == 2)
{
attributes[parts[0]] = parts[1];
}
else
{
throw new Exception($"Invalid resource attribute format: {attributeConfig}");
}
}
}
});
};
switch (builder)
{
case WebApplicationBuilder webApplicationBuilder:
webApplicationBuilder.Host.UseSerilog((_, _, options) => configureLogger(options));
break;
case HostApplicationBuilder hostApplicationBuilder when
hostApplicationBuilder.GetType().GetMethod("AsHostBuilder", BindingFlags.Instance | BindingFlags.NonPublic) is { } asHostBuilderMethod:
{
if (asHostBuilderMethod.Invoke(hostApplicationBuilder, parameters: null) is not IHostBuilder hostBuilder)
{
throw new InvalidOperationException("Failed to get IHostBuilder from HostApplicationBuilder");
}
hostBuilder.UseSerilog((_, _, options) => configureLogger(options));
break;
}
default:
{
var loggerConfig = new LoggerConfiguration();
configureLogger(loggerConfig);
builder.Logging.AddSerilog(loggerConfig.CreateLogger());
break;
}
}
return builder;
} |
@Blind-Striker FWIW the |
Ok, it seems that
(P.S. to myself: read the fricking manual next time 🙃) I'm still not sure why public static IHostApplicationBuilder ConfigureSerilog(this IHostApplicationBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
builder.Services.AddSerilog(config =>
{
config.ReadFrom.Configuration(builder.Configuration)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithProcessId()
.Enrich.WithProcessName()
.Enrich.WithThreadId()
.Enrich.WithSpan()
.Enrich.WithExceptionDetails(new DestructuringOptionsBuilder()
.WithDefaultDestructurers()
.WithDestructurers(new[] { new DbUpdateExceptionDestructurer() }))
.Enrich.WithProperty("Environment", builder.Environment.EnvironmentName)
.WriteTo.Console()
.WriteTo.OpenTelemetry(options =>
{
options.IncludedData = IncludedData.TraceIdField | IncludedData.SpanIdField;
options.Endpoint = builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"];
AddHeaders(options.Headers, builder.Configuration["OTEL_EXPORTER_OTLP_HEADERS"]);
AddResourceAttributes(options.ResourceAttributes, builder.Configuration["OTEL_RESOURCE_ATTRIBUTES"]);
void AddHeaders(IDictionary<string, string> headers, string headerConfig)
{
if (!string.IsNullOrEmpty(headerConfig))
{
foreach (var header in headerConfig.Split(','))
{
var parts = header.Split('=');
if (parts.Length == 2)
{
headers[parts[0]] = parts[1];
}
else
{
throw new Exception($"Invalid header format: {header}");
}
}
}
}
void AddResourceAttributes(IDictionary<string, object> attributes, string attributeConfig)
{
if (!string.IsNullOrEmpty(attributeConfig))
{
var parts = attributeConfig.Split('=');
if (parts.Length == 2)
{
attributes[parts[0]] = parts[1];
}
else
{
throw new Exception($"Invalid resource attribute format: {attributeConfig}");
}
}
}
});
});
return builder;
} |
@nblumhardt, sorry, I had reached the same conclusion, just before saw your comment. Thank you very much! 🙏 |
… not functioning - Fixed a bug where Serilog's OpenTelemetry exporter was not properly exporting logs. Discovered related issue on GitHub (serilog/serilog-aspnetcore#359) and implemented the recommended solution. - Adjusted configuration settings and ensured compatibility with the current OpenTelemetry standards. - Added additional checks and balances to prevent similar issues in the future and enhanced logging to capture any related errors.
@nblumhardt Thanks for the clarification around @Blind-Striker Thanks a lot for providing a fix. I had the exact same problem. It has fixed the issue with structured logs, and I can also see the traces without duplicate names. However, with this fix, I am now seeing an My setup is like below. Please note that my apiService is in a different repository, and not part of the solution, so I am loading it via ProjectPath. AppHost: Program.csusing Microsoft.Extensions.Configuration;
using Portal.AppHost.Configuration;
var builder = DistributedApplication.CreateBuilder(args);
var configuration = builder.Configuration;
// get the configuration settings for the projects
var projectSettingsSection = configuration.GetSection(nameof(ProjectsSettings));
var projectSettings = projectSettingsSection.Get<ProjectsSettings>();
// get the configuration settings for api service
var apiServiceSettings = projectSettings.ApiServiceSettings;
// add the project reference
var apiService = builder.AddProject(apiServiceSettings.ProjectName, apiServiceSettings.ProjectPath);
// add the portal project with a dependency on
// apiService
builder
.AddProject<Projects._Portal>("xxx-xxx-portal")
.WithReference(apiService);
// run the host app
await builder.Build().RunAsync(); ServiceDefaults: Extensions.csThis is exactly the same as yours apart from my Serilog setup is a bit trimmed down as I am reading from file. public static IHostApplicationBuilder ConfigureSerilog(this IHostApplicationBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
builder.Services.AddSerilog(config =>
{
config
.ReadFrom.Configuration(builder.Configuration)
.WriteTo.OpenTelemetry(options =>
{
options.IncludedData = IncludedData.TraceIdField | IncludedData.SpanIdField;
options.Endpoint = builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"];
AddHeaders(options.Headers, builder.Configuration["OTEL_EXPORTER_OTLP_HEADERS"]);
AddResourceAttributes(options.ResourceAttributes, builder.Configuration["OTEL_RESOURCE_ATTRIBUTES"]);
void AddHeaders(IDictionary<string, string> headers, string headerConfig)
{
if (!string.IsNullOrEmpty(headerConfig))
{
foreach (var header in headerConfig.Split(','))
{
var parts = header.Split('=');
if (parts.Length == 2)
{
headers[parts[0]] = parts[1];
}
else
{
throw new InvalidOperationException($"Invalid header format: {header}");
}
}
}
}
void AddResourceAttributes(IDictionary<string, object> attributes, string attributeConfig)
{
if (!string.IsNullOrEmpty(attributeConfig))
{
var parts = attributeConfig.Split('=');
if (parts.Length == 2)
{
attributes[parts[0]] = parts[1];
}
else
{
throw new InvalidOperationException($"Invalid resource attribute format: {attributeConfig}");
}
}
}
});
});
return builder;
} |
Hello everyone! I have successfully set up the Aspire dashboard as a standalone dashboard to one of my web apis. I'm able to read metrics, logs and traces in the dashboard without problem. I'm spinning up the dashboard as a docker compose locally, but everytime I do so im getting this message in my dashboard: I have tried several methods, including the one @davidfowl mentioned with passing in them in the launchSettings. I have provided different variables to my docker compose - but still getting the message. Can someone point me in the right direction? Or does anyone have a solution? |
Did you click the more information link? It points to docs that explain how to set it up securely |
Hey @softmatters, I'm glad you managed to resolve the duplicate message issue. I'll take a closer look and try to respond in more detail. In the meantime, I released a demo using Aspire, OpenTelemetry, and Serilog yesterday. If you review the code and compare it with yours, you might fix the problem faster. https://github.com/Blind-Striker/dotnet-otel-aspire-localstack-demo |
Hi @Blind-Striker , thanks a lot. I'll take a look and will let you know if I managed to resolve the issue or not. |
Hehe... I had a typo in my api key :) But thanks! |
I do like to use Serilog configuration from appSettings. I changed the code shared here a little bit to make it work with that approach. So if you have given appSettings.Development.json "Serilog": {
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.OpenTelemetry" ],
"MinimumLevel": "Debug",
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3} {CorrelationId}] {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "OpenTelemetry",
"Args": {
"endpoint": "%OTEL_EXPORTER_OTLP_ENDPOINT%",
"includedData": "TraceIdField, SpanIdField"
}
}
],
"Enrich": [
{
"Name": "WithProperty",
"Args": {
"name": "ApplicationName",
"value": "PocRedisOtel.Api"
}
},
"WithMachineName",
"WithDemystifiedStackTraces",
"WithClientAgent",
"FromLogContext",
"WithCorrelationIdHeader"
]
} I've created the method void AddSerilogOtelSection(string config, string section)
{
foreach (var part in config?.Split(',') ?? [])
{
if (part.Split('=') is [var key, var value])
builder.Configuration[$"Serilog:WriteTo:1:Args:{section}:{key}"] = value;
else
throw new InvalidOperationException($"Invalid {section} format: {part}");
}
} Then I just call it var builder = WebApplication.CreateBuilder(args);
AddSerilogOtelSection(builder.Configuration["OTEL_EXPORTER_OTLP_HEADERS"], "headers");
AddSerilogOtelSection(builder.Configuration["OTEL_RESOURCE_ATTRIBUTES"], "resourceAttributes"); |
Did anyone file an issue to make sure the serilog otel extensions natively support these configuration options? |
I guess so @davidfowl |
The version 4.0.0-dev-00313 of the Serilog.Sinks.OpenTelemetry will be able to read the environment variables by default https://www.nuget.org/packages/Serilog.Sinks.OpenTelemetry/4.0.0-dev-00313 "Serilog": {
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.OpenTelemetry" ],
"MinimumLevel": "Debug",
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3} {CorrelationId}] {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "OpenTelemetry"
}
],
//....
} Thank you @nblumhardt for helping me!!! |
Great contribution @AlbertoMonteiro ! |
@AlbertoMonteiro |
@sbalocky serilog/serilog-sinks-opentelemetry#146 will resolve this; HTH |
这里面有个问题, 微软默认的日志记录器, 是可以实现,把日志输出到aspire,然后由aspire再转发到seq进行存储的,而应用的日志,只需要输出到console, 为什么serilog不使用相同的方式,而使用了open-telemetry这种方式呢? https://learn.microsoft.com/zh-cn/dotnet/aspire/logging/seq-component?tabs=dotnet-cli |
There is a problem here. Microsoft's default logger can output logs to Aspire, which then forwards them to Seq for storage. However, application logs only need to be output to the console. Why does Serilog use the same method instead of Open Strategy? |
I still have duplicate message logs and traces when I use both OpenTelemetry and Seq. Has anyone encountered the same issue? |
Hi folks! If you have an additional question or feature request, a new thread on Stack Overflow (usage questions) or a new ticket in the appropriate repo (feature requests) will get the right eyes on it and improve chances of finding a successful resolution. Thanks! |
I'm exploring the new Dotnet 8 Aspire dashboard.
Out of the box it is providing structured logs (from opentelemetry) and tracing as shown in the print screen:
As soon i add Serilog Use like this
builder.Host.UseSerilog();
I loose the structured logs.
For example here i added the UseSerilog in the web frontend:
The serilog instrumentation is like this:
Log.Logger = new LoggerConfiguration() .WriteTo.Console() .Enrich.FromLogContext() .Enrich.With(new TraceIdEnricher()) .CreateLogger();
How can i use Serilog and Aspire together?
The text was updated successfully, but these errors were encountered: