.NET Core 6 - How to get an ILogger instance without Dependency Injection in Program.cs during Startup
Asked Answered
R

4

39

I've updated the content of my original question as I was starting to cause some confusion amongst the people trying to help me.

I am using the library "Microsoft.ApplicationInsights.AspNetCore" to send logs to Azure. One of the challenges it seems in using this library is that it doesn't send any log events to Azure until a service has been created for the library.

The chicken and egg situation is that I need to write logs and send them to Azure at the very earliest stage of the startup process in the Net Core 6 web app i.e. before the service that App Insights needs to function has actually been created.

The reason I need to write logs at an early stage in the app startup process is to capture details of the user login, in which the Microsoft sign in page will popup as soon as the .Net Core app has started.

In the code example below, you can see that I create an instance of the logger factory so that i can write some logs locally in the program.cs file before the remaining services are built and started. Although using this methodology works for writing to the console, no events are sent to App Insights. I think this becuase App insights library isn't established until the required service is created which is at a later stage in the program.cs file.

var builder = WebApplication.CreateBuilder(args);

// I create an instance of logger factory
using var loggerFactory = LoggerFactory.Create(loggingBuilder => loggingBuilder
    .SetMinimumLevel(LogLevel.Trace)
    .AddConsole()
    .AddApplicationInsights(builder.Configuration["APPINSIGHTS_CONNECTIONSTRING"]));
    
// I use the logger factory to create an instance of Ilogger
ILogger logger = loggerFactory.CreateLogger<Program>();

// This code section here is related to Microsoft Identity Web library and is responsible for
// triggering methods based upon when a user signs into Microsoft (as well as signing out)
// When them methods are triggered in this service, I need to write logs and send them to Azure.
// The issue is this service runs before Application Insights service has been created/started, see code section below...
builder.Services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
    // The claim in the Jwt token where App roles are available.
    options.TokenValidationParameters.RoleClaimType = "roles";
    // Advanced config - capturing user events. See OpenIdEvents class.
    options.Events ??= new OpenIdConnectEvents();
    options.Events.OnTokenValidated += openIdEvents.OnTokenValidatedFunc;
    // This is event is fired when the user is redirected to the MS Signout Page (before they've physically signed out)
    options.Events.OnRedirectToIdentityProviderForSignOut += openIdEvents.OnRedirectToIdentityProviderForSignOutFunc;
    // DO NOT DELETE - May use in the future.
    // OnSignedOutCallbackRedirect doesn't produce any user claims to read from for the user after they have signed out.
    options.Events.OnSignedOutCallbackRedirect += openIdEvents.OnSignedOutCallbackRedirectFunc;
});

// --- IMPORTANT NOTE -----
This log event is successfully written to the console, BUT it does not get sent to Azure App Insights.
// --------------------------------------------------------------------------------------
The methods triggered in the code section above by Microsoft Identity Web are actually stored in a separate class,
// however being unbale to write a test log message here means that it wont work in a separate class either.
logger.LogInformation("This is test message");


// ----- Other general services being created required for my app -----
// Add the AuthorizationPolicies for the AppRoles
builder.Services.AddAuthorizationClaimPolicies();

builder.Services.AddAuthorization(options =>
{
    // By default, all incoming requests will be authorized according to the default policy.
    options.FallbackPolicy = options.DefaultPolicy;
});
builder.Services.AddRazorPages()
    .AddMicrosoftIdentityUI();
    
// HERE IS THE PART WHERE APPLICATION INSIGHTS SERVICE IS CREATED, 
// SO HAVING CREATED AN INSTANCE OF ILOGGER FACTORY BEFORE THIS STEP DOES NOT WORK
// ----- Configure Application Insights Telemetry -----
Microsoft.ApplicationInsights.AspNetCore.Extensions.ApplicationInsightsServiceOptions aiOptions = new();
aiOptions.ConnectionString = builder.Configuration["APPINSIGHTS_CONNECTIONSTRING"];
aiOptions.EnablePerformanceCounterCollectionModule = builder.Configuration.GetSection("ApplicationInsights").GetValue<bool>("EnablePerformanceCounterCollectionModule");
aiOptions.EnableRequestTrackingTelemetryModule = builder.Configuration.GetSection("ApplicationInsights").GetValue<bool>("EnableRequestTrackingTelemetryModule");
aiOptions.EnableEventCounterCollectionModule = builder.Configuration.GetSection("ApplicationInsights").GetValue<bool>("EnableEventCounterCollectionModule");
aiOptions.EnableDependencyTrackingTelemetryModule = builder.Configuration.GetSection("ApplicationInsights").GetValue<bool>("EnableDependencyTrackingTelemetryModule");
aiOptions.EnableAppServicesHeartbeatTelemetryModule = builder.Configuration.GetSection("ApplicationInsights").GetValue<bool>("EnableAppServicesHeartbeatTelemetryModule");
aiOptions.EnableAzureInstanceMetadataTelemetryModule = builder.Configuration.GetSection("ApplicationInsights").GetValue<bool>("EnableAzureInstanceMetadataTelemetryModule");
aiOptions.EnableQuickPulseMetricStream = builder.Configuration.GetSection("ApplicationInsights").GetValue<bool>("EnableQuickPulseMetricStream");
aiOptions.EnableAdaptiveSampling = builder.Configuration.GetSection("ApplicationInsights").GetValue<bool>("EnableAdaptiveSampling");
aiOptions.EnableHeartbeat = builder.Configuration.GetSection("ApplicationInsights").GetValue<bool>("EnableHeartbeat");
aiOptions.AddAutoCollectedMetricExtractor = builder.Configuration.GetSection("ApplicationInsights").GetValue<bool>("AddAutoCollectedMetricExtractor");
aiOptions.RequestCollectionOptions.TrackExceptions = builder.Configuration.GetSection("ApplicationInsights").GetValue<bool>("RequestCollectionOptions.TrackExceptions");
aiOptions.EnableDiagnosticsTelemetryModule = builder.Configuration.GetSection("ApplicationInsights").GetValue<bool>("EnableDiagnosticsTelemetryModule");
// Build the serive with the options from above.
builder.Services.AddApplicationInsightsTelemetry(aiOptions);

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();
//app.MapControllers(); // Default mapping, not in use, see below...
app.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
Refutation answered 8/1, 2022 at 17:41 Comment(4)
OpenIdEvents - what is this ? Is it your custom class?Mandeville
Yes, basically I'm trying to pass an instance if ILogger to this class but the issue is the methods in this class are called right at the beginning of the app startup process so I cant rely on dependency injection to acquire an instance of ILogger. I've followed Guru's suggestion below and have tried passing the instance of ILogger created in Progam.cs to this sperate class and am able to log to the console, but when I add ".AddApplicationInsights());" to his method, I still don't get any logs going to App Insights. Unfortunately I think this steering towards a slightly different issue now.Refutation
Why you can not create Logger manually in a constructor for example?Mandeville
You are "sending the logs" from where? Where is the process running which contains your code? I assume it is Azure App Svc hosted?Halpern
S
7

As you've discovered, using an ILogger backed by Azure Application Insights prior to the DI container being constructed doesn't actually send the logs to AppInsights.

Since .NET 6's new host builder really cements the move away from the Startup class with DI support (which effectively needed two DI containers and wasn't ideal, for a few reasons), there aren't great options. From what I've seen, it feels like Microsoft doesn't intend to support complex logging providers during startup out of the box.

That said, you could try to manually instantiate a TelemetryClient with the instrumentation key. From there, you can log anything you want. Anything fancy like auto-adding properties to your events, etc, most likely won't work as nothing has really been set up. But you should be able to fire off data to your AppInsights. This hasn't been tested, but try:

var client = new TelemetryClient(TelemetryConfiguration.CreateDefault());
client.InstrumentationKey = "YOUR_INSTRUMENTATION_KEY";
client.TrackTrace("Severe startup issue.", SeverityLevel.Critical);
Semidome answered 1/2, 2022 at 20:14 Comment(3)
Hi, I managed to get an instance of the Telemetry Client as you mentioned into my program.cs file which ultimately allows me to send data to App Insights before the service is created for DI. I only need to use this for a few things before I then resort back to the proper service created the logging. I also managed to serialize a class of properties instead of using just a string for the message so this should give me what I was looking for, thanksRefutation
Final note: I used the using statement when creating the instance of the telemtry client and then flushed it after use --> using var telemetryConfiguration = new TelemetryConfiguration("my_instrumentation_key"); telemetryClient = new TelemetryClient(telemetryConfiguration); telemetryClient.TrackTrace(JsonSerializer.Serialize(logEvent), SeverityLevel.Information); telemetryClient.Flush(); (where logEvent is a serialized list of properties)Refutation
@Refutation yup, I only use this technique for very specific scenarios and it should be avoided in any normal request context, etc. also smart to flush. As well, the serializing is a little tricky, I have a gist here I go back to frequently that runs through a few ways to do it and notes on how they appear in AppIns gist.github.com/NickSpag/036fab2f30563f23083005f8569ea003 if that helpsSemidome
M
37

This code tested using net 6

var builder = WebApplication.CreateBuilder(args);

using var loggerFactory = LoggerFactory.Create(loggingBuilder => loggingBuilder
    .SetMinimumLevel(LogLevel.Trace)
    .AddConsole());

ILogger<LogTest> logger = loggerFactory.CreateLogger<LogTest>();
var test = new LogTest(logger);

or maybe you can adopt this code

var builder = WebApplication.CreateBuilder(args);

builder.Logging.ClearProviders();
 builder.Logging.AddConsole();
....

var app = builder.Build();

ILogger <LogTest> logger = app.Services.GetRequiredService<ILogger<LogTest>>();
var test = new LogTest(logger);

test class

public class LogTest
{
    private readonly ILogger<LogTest>  _logger;

    public LogTest(ILogger<LogTest> logger)
    {
        _logger = logger;  
        var message = $" LogTest logger created at {DateTime.UtcNow.ToLongTimeString()}";
        _logger.LogInformation(message);
    }
}
Mandeville answered 8/1, 2022 at 20:17 Comment(4)
Hi, really appreciate you helping. Passing a logger instance to the seperate class does work OK for writing to the console. Im still struggling to be able to write to Application Insights though. The methods I'm calling in my custom class have to be called before before the service is created for "builder.Services.AddApplicationInsightsTelemetry" so its a chicken and egg . I'm sorry I've really messed up in the way I've approached this question.Refutation
I'm using Application Insights library with the built in MS Logger. The challenge is the App Insights library only works once a service has been created . I need to log events for some methods which are run long before any of the services are created i.e. right at the beginning of the app startup process. So it seems that even though an ILogger intance can be passed around at an early stage, the App insights connector isnt actually working until the service is created.Refutation
So I have to ask, if you can add a logger of your own at the time you need, can't you just store whatever data you need in an array as it's happening until the App Insights connection is done, then dump them to it and stop processing anymore in your own little logger?Rubierubiginous
There is no way to get the ILogger that would have been created by the configuration using dependency injection at this point in the pipeline?Shizukoshizuoka
F
8

You can manually create LoggerFactory and Logger:

using var loggerFactory = LoggerFactory.Create(loggingBuilder => loggingBuilder
    .SetMinimumLevel(LogLevel.Trace)
    .AddConsole());

ILogger logger = loggerFactory.CreateLogger<Program>();
logger.LogInformation("Example log message");

UPD

You need to duplicate your "ordinary" setup which works for you. You can try something along this lines:

var configuration = new ConfigurationBuilder() // add all configuration sources used
    .AddJsonFile("appsettings.json", false, true)
    .AddEnvironmentVariables()
    .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", true, true)
    .Build();
using var loggerFactory = LoggerFactory.Create(loggingBuilder => loggingBuilder
    .AddConfiguration(configuration)
    .AddApplicationInsights());
// create logger and use it ....
Footgear answered 8/1, 2022 at 18:15 Comment(7)
Hi, Will this work for App Insights as well? i.e. how would this be done without logging to the console only? thxRefutation
This instance of ILogger works in the Program.cs file, but I find when passing it to the constructor of the separate class (OpenIdEvents) that's called from Program.cs that it doesn't seem to do anything, but no compile errors are seen though.Refutation
@Refutation not sure what OpenIdEvents is. Have you set up the logging correctly for manual factory? Also I would say that OpenIdEvents should be part of DI process including logger registration/use.Footgear
Its a class with methods in it that are called from Program.cs file. These methods fire when a user logs in/out of the application. I need to write logs in these methods but i cant get an instance of the logger to work. I've tried your suggeston and this works for logging to the console, but I need to log to Azure Application Insights instead. I tried simply adding ".AddApplicationInsights());" to your loggerfactory method and passing this to the Ctor of the seperate class, but no logs are sent to App Insights so I'm missing somethig else which I cant get my head around.Refutation
@Refutation you need to copy the same setup you use to "ordinary " ApplicationInsights" logging. See the update - maybe it will give you some ideas what can be missing.Footgear
Updated my question, hopefully this makes more sense now :)Refutation
Second example, this is the way...Shizukoshizuoka
S
7

As you've discovered, using an ILogger backed by Azure Application Insights prior to the DI container being constructed doesn't actually send the logs to AppInsights.

Since .NET 6's new host builder really cements the move away from the Startup class with DI support (which effectively needed two DI containers and wasn't ideal, for a few reasons), there aren't great options. From what I've seen, it feels like Microsoft doesn't intend to support complex logging providers during startup out of the box.

That said, you could try to manually instantiate a TelemetryClient with the instrumentation key. From there, you can log anything you want. Anything fancy like auto-adding properties to your events, etc, most likely won't work as nothing has really been set up. But you should be able to fire off data to your AppInsights. This hasn't been tested, but try:

var client = new TelemetryClient(TelemetryConfiguration.CreateDefault());
client.InstrumentationKey = "YOUR_INSTRUMENTATION_KEY";
client.TrackTrace("Severe startup issue.", SeverityLevel.Critical);
Semidome answered 1/2, 2022 at 20:14 Comment(3)
Hi, I managed to get an instance of the Telemetry Client as you mentioned into my program.cs file which ultimately allows me to send data to App Insights before the service is created for DI. I only need to use this for a few things before I then resort back to the proper service created the logging. I also managed to serialize a class of properties instead of using just a string for the message so this should give me what I was looking for, thanksRefutation
Final note: I used the using statement when creating the instance of the telemtry client and then flushed it after use --> using var telemetryConfiguration = new TelemetryConfiguration("my_instrumentation_key"); telemetryClient = new TelemetryClient(telemetryConfiguration); telemetryClient.TrackTrace(JsonSerializer.Serialize(logEvent), SeverityLevel.Information); telemetryClient.Flush(); (where logEvent is a serialized list of properties)Refutation
@Refutation yup, I only use this technique for very specific scenarios and it should be avoided in any normal request context, etc. also smart to flush. As well, the serializing is a little tricky, I have a gist here I go back to frequently that runs through a few ways to do it and notes on how they appear in AppIns gist.github.com/NickSpag/036fab2f30563f23083005f8569ea003 if that helpsSemidome
C
0

This is a late answer but adding for posterity after seeing this question pop up as modified.

Our team had similar challenges trying to log startup-oriented behaviors and content into log providers that are not fully configured and available until after the host starts. For the reasons outlined in other answers, you either need to create a separate bootstrapping logger or wait until the service provider has been built.

However, by leveraging progressive actions against an IOptions implementation, it is possible to capture data at startup but defer the actual logging action until after the host is available. This doesn't help if the host fails to start, but it can be useful where you need to track startup configuration. I created a NuGet package/library called LogDeferred to encapsulate this into extension methods that make the familiar ILogger interfaces available directly to the IServiceCollection during startup, with several convenience methods for use.

https://www.nuget.org/packages/LogDeferred/

https://github.com/agertenbach/LogDeferred

All the familiar ILogger behavior is available directly against the IServiceCollection, with the option to specify a type parameter that will be used to set the logger category for the entry:

builder.Services.LogDebug("I will show up under the default Microsoft.Hosting.Startup logger");
builder.Services.LogInformation<MyTypeHere>("I will show up under the {type} logger", typeof(MyTypeHere).FullName);

One or more actions can be called against the resolved ILogger itself as an action, instead:

builder.Services.Log(logger =>
{
    logger.LogInformation("Here's one log");
    logger.LogDebug("Here's another!");
});

builder.Services.Log<MyTypeHere>(logger =>
{
    logger.LogInformation("Types are supported here too");
});

If your log actions require injected dependencies after startup, you may chain them using the following syntax for optional/nullable and required dependencies respectively:

builder.Services.Log<MyFeatures>()
    .For<FeatureOne>((logger, feature) =>
        {
            logger.LogInformation("Feature one is null? {isNull}", feature is null);
        })
    .ForRequired<FeatureTwo, IOptions<MyFeatureConfig>>((logger, f2, config) =>
        {
            logger.LogInformation("If either of these were null, you'd get an InvalidOperationException");
        });
Craniometer answered 26/1 at 18:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.