How to log an error when Azure App Configuration reload fails - c#

I'm using Azure App Configuration with the dynamic configuration using poll model enabled, as described here. My goal is to log an error message when the configuration reload fails for any reason, like an incorrect key-vault reference. Here's the method which adds Azure App Configuration as the configuration source.
public static IConfigurationBuilder AddAppConfiguration(IConfigurationBuilder configurationBuilder, IEnumerable<string> filters)
{
if (!configurationBuilder.Sources.Any())
{
configurationBuilder.AddDefaultConfigurationSources();
}
var configuration = configurationBuilder.Build();
var appConfiguration = configuration.GetSection(AppConfiguration.ConfigurationKey)
.Get<AppConfiguration>();
if (appConfiguration?.IsEnabled == true)
{
if (appConfiguration.IsRedundancyEnabled)
configurationBuilder.AddAzureAppConfiguration(
options =>
{
ConfigureAppConfiguration(options, appConfiguration, appConfiguration.UrlSecondary, filters, appConfiguration.Label);
}, true);
configurationBuilder.AddAzureAppConfiguration(
options => { ConfigureAppConfiguration(options, appConfiguration, appConfiguration.Url, filters, appConfiguration.Label); },
true);
}
return configurationBuilder;
}
Please note that the optional parameter in AddAzureAppConfiguration is set to true. This means that the application won't crash if the configuration refresh fails and will just keep running with the old settings, I'd like to keep that behavior.
I've tried to use the ChangeToken to log an error if the configuration hasn't changed like this:
configuration.GetReloadToken().RegisterChangeCallback(state =>
{
var reloadToken = state as IChangeToken;
if (reloadToken.HasChanged)
{
logger.Information("Configuration refresh successful");
}
else
{
logger.Error("Configuration refresh failed");
}
RegisterCallback(logger, configuration);
}, configuration.GetReloadToken());
But the callback only actually gets called if the configuration did in fact change 😅, at least I didn't manage to get it to fail and log the error.

As explained here, the default ILoggerFactory will be added automatically when services.AddAzureAppConfiguration() is invoked in your ConfigureServices() method through DI. I was not seeing logs because the Microsoft minimum log level was set to Error, while the AzureAppConfigurationProvider logs Warning if the TryRefreshAsync() method fails.

Related

Why is IConfiguration null when integration testing my Azure Functions?

The current version of the Microsoft.Azure.Functions.Extensions package exposes an additional property that allows you easy access to the IConfiguration provided to the function. Previously this required manually building a service provider, which was obviously problematic.
Using that package my FunctionsStartup.cs looks like this:
public override void Configure(IFunctionsHostBuilder builder)
{
base.Configure(builder);
var config = builder.GetContext().Configuration; // new in v1.1.0 of Microsoft.Azure.Functions.Extensions
var mySetting = config["MySetting"];
int.Parse(mySetting, out var mySetting);
// ... use mySetting...
}
In order to test my HTTP-triggered functions I've used this article as a base, which details how to manually build and start a host to execute my function as if it was running in Azure, similar to how TestServer works in ASP.NET Core:
var host = new HostBuilder()
.ConfigureWebJobs(new FunctionsStartup().Configure)
.Build();
var functionsInstance = ActivatorUtilities.CreateInstance<MyFunctions>(host.Services);
I can then execute the function methods defined on MyFunctions to test their responses:
var request = new DefaultHttpRequest(new DefaultHttpContext());
var response = (OkObjectResult)functionsInstance.HttpTriggerMethod(request);
... assert that response is valid
The problem is that when I run my tests, builder.GetContext().Configuration is returning null in FunctionsStartup.Configure, which of course causes those tests to fail. How can I work around this?
The article I linked to hasn't been updated to take into account the existence of builder.GetContext().Configuration, but you can make this work for testing purposes with a little tweaking. Instead of using:
var host = new HostBuilder()
.ConfigureWebJobs(new FunctionsStartup().Configure)
.Build();
you need to explicitly copy the host's settings into a new WebJobsBuilderContext that you then pass to your function's startup:
var host = new HostBuilder()
.ConfigureWebJobs((context, builder) => new FunctionsStartup().Configure(new WebJobsBuilderContext
{
ApplicationRootPath = context.HostingEnvironment.ContentRootPath,
Configuration = context.Configuration,
EnvironmentName = context.HostingEnvironment.EnvironmentName,
}, builder))
.Build();
I'm not sure if this is the completely correct way to achieve this, but it has worked well for me.

MSAL.Net No account or login hint was passed to the AcquireTokenSilent call

I have seen many same or similar questions, and tried all their answers if there was one, but none of those works for me.
I'm using this example from Microsoft's Github account as my project base.
It works well for just signing in users.
The project has 1 WebApi, 1 Angular App.
Then I followed this Microsoft example to add code to call Graph API.
Here is the controller code:
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class BillsController : ControllerBase
{
static readonly string[] scopeRequiredByApi = new string[] { "access_as_user" };
readonly ITokenAcquisition tokenAcquisition;
readonly WebOptions webOptions;
public BillsController(ITokenAcquisition tokenAcquisition,
IOptions<WebOptions> webOptionValue)
{
this.tokenAcquisition = tokenAcquisition;
this.webOptions = webOptionValue.Value;
}
[HttpGet]
[AuthorizeForScopes(Scopes = new[] { Constants.ScopeUserRead, Constants.ScopeMailRead })]
public async Task<IActionResult> Profile()
{
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
var subject = string.Empty;
try
{
// Initialize the GraphServiceClient.
Graph::GraphServiceClient graphClient = GetGraphServiceClient(new[] { Constants.ScopeUserRead, Constants.ScopeMailRead });
var me = await graphClient.Me.Request().GetAsync();
// Get user photo
var messages = await graphClient.Me.MailFolders.Inbox.Messages.Request().GetAsync();
subject = messages.First().Subject;
return Ok(subject);
}
catch (System.Exception ex)
{
throw ex;
}
}
private Graph::GraphServiceClient GetGraphServiceClient(string[] scopes)
{
return GraphServiceClientFactory.GetAuthenticatedGraphClient(async () =>
{
string result = await tokenAcquisition.GetAccessTokenForUserAsync(scopes);
return result;
}, webOptions.GraphApiUrl);
}
}
For Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Setting configuration for protected web api
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddProtectedWebApi(Configuration);
services.AddWebAppCallsProtectedWebApi(Configuration, new string[] { Constants.ScopeUserRead, Constants.ScopeMailRead })
.AddInMemoryTokenCaches();
services.AddOptions();
services.AddGraphService(Configuration);
// Creating policies that wraps the authorization requirements
services.AddAuthorization();
services.AddDbContext<TodoContext>(opt => opt.UseInMemoryDatabase("TodoList"));
services.AddControllers();
// Allowing CORS for all domains and methods for the purpose of sample
services.AddCors(o => o.AddPolicy("default", builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
}));
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
// Since IdentityModel version 5.2.1 (or since Microsoft.AspNetCore.Authentication.JwtBearer version 2.2.0),
// Personal Identifiable Information is not written to the logs by default, to be compliant with GDPR.
// For debugging/development purposes, one can enable additional detail in exceptions by setting IdentityModelEventSource.ShowPII to true.
// Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
app.UseDeveloperExceptionPage();
}
else
{
// 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.UseExceptionHandler("/error");
app.UseCors("default");
app.UseHttpsRedirection();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
On the Angular App, I added one button to call this Profile() controller action.
todo-view.component.ts
getEmails(): void {
this.service.getEmails().subscribe({
next: (emails: any) => {
alert(emails);
},
error: (err: any) => {
console.log("error happened~!");
console.log(err);
}
});
}
todo-view.component.html
<button (click)="getEmails();">Get Emails</button>
I added the below code into my Startup.cs and removed the AddWebAppCallsProtectedWebApi.
services.AddProtectedWebApiCallsProtectedWebApi(Configuration).AddInMemoryTokenCaches();
Now it throws me a different error message:
I was having the same issue with a react app. Since the AuthorizeForScopes is for returning views, it does not work for API solutions. I was able to add some configuration options to get it working.
The first thing I did was use a SQL cache. This helped stop the "No login hint" error when the site restarted. After that, the token would work fine until timeout, after which the token would get removed from the cache and the error would reappear.
For that, I started looking at the configuration settings. I changed my configuration to the following.
services
.AddWebAppCallsProtectedWebApi(new string[] { "User.Read" }, idOps =>
{
Configuration.Bind("AzureAd", idOps);
idOps.SaveTokens = true;
idOps.RefreshOnIssuerKeyNotFound = true;
idOps.SingletonTokenAcquisition = true;
idOps.UseTokenLifetime = true;
},
ops => Configuration.Bind("AzureAd", ops))
.AddDistributedTokenCaches()
.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = Configuration.GetConnectionString("Site_DbContext");
options.SchemaName = "dbo";
options.TableName = "_TokenCache";
});
I haven't done much testing on it to find out the magic combination, but the sweet spot seems to be SingletonTokenAcquisition. With that set, it seems to be behaving like a hybrid cache. When first set, it pulls the token into memory and holds it so if it is removed from the database cache, it still has access to it.
The other settings may be necessary for the refreshing but I haven't tested that yet.
The one thing I did notice is the token doesn't get added back to the SQL cache until it refreshes so if something happens where the token is removed and the site goes down clearing the memory, the error may reappear, but this is the best solution I found so far. I was able to have my SPA running for 24 hours and was still able to pull new data.

Heartbeat functionality from AppInsights does not appear in azure portal

I'm trying to get the heartbeat feature of AppInsights sdk to work but I'm having some trouble.
I have a simple app (just the default ASP.net core 2.2 project created by using dotnet new webapp) running on a k8 cluster inside Azure and is configured with the following settings:
public void ConfigureServices(IServiceCollection services)
{
ApplicationInsightsServiceOptions aiOptions
= new ApplicationInsightsServiceOptions();
// Disables adaptive sampling.
aiOptions.EnableAdaptiveSampling = false;
// Disables QuickPulse (Live Metrics stream).
aiOptions.EnableQuickPulseMetricStream = false;
aiOptions.InstrumentationKey = InstrumentationKey;
aiOptions.EnableHeartbeat=true;
services.AddApplicationInsightsTelemetry(aiOptions);
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddApplicationInsightsKubernetesEnricher();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
However, I can't see any properties in Application Insights related to the heartbeat functionality. I can see other stuff like the kubernetes pod name, etc.
Am I missing some configuration?
Thank you.
The Heartbeat feature is enabled by default as of base SDK 2.5.0 and the ability to configure the Heartbeat was added in 2.3.0-beta1.
I would suggest you to modify your startup file like below:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
ApplicationInsightsServiceOptions aiOpts =
new ApplicationInsightsServiceOptions();
aiOpts.EnableHeartbeat = true; // false to disable
services.AddApplicationInsightsTelemetry(aiOpts);
...
}
Also add using Microsoft.ApplicationInsights.AspNetCore.Extensions; in the top of your file.
Configure the Heartbeat feature in code by modifying the IHeartbeatPropertyManager directly. You can do this when you first obtain the property manager via the TelemetryModules.Instance singleton.
foreach (var md in TelemetryModules.Instance.Modules)
{
if (md is IHeartbeatPropertyManager heartbeatPropertyMan)
{
heartbeatPropertyMan.HeartbeatInterval = TimeSpan.FromMinutes(5.0);
heartbeatPropertyMan.ExcludedHeartbeatProperties.Add("osType");
...
Try this and see if it helps.
To query heartbeat events you can use the following KQL query:
customMetrics
| where name == "HeartbeatState"

Why is ILogger not logging to Azure app insights?

I am testing out the code directly out of here for a console app: https://learn.microsoft.com/en-us/azure/azure-monitor/app/ilogger#
I basically copied the code and pointed it to a new azure app insights instance. However, none of the logs are showing up in app insights. Am I missing anything?
static void Main(string[] args)
{
// Create DI container.
IServiceCollection services = new ServiceCollection();
// Add the logging pipelines to use. We are using Application Insights only here.
services.AddLogging(loggingBuilder =>
{
// Optional: Apply filters to configure LogLevel Trace or above is sent to ApplicationInsights for all
// categories.
loggingBuilder.AddFilter<ApplicationInsightsLoggerProvider>("", LogLevel.Trace);
loggingBuilder.AddApplicationInsights(******);
});
// Build ServiceProvider.
IServiceProvider serviceProvider = services.BuildServiceProvider();
ILogger<Program> logger = serviceProvider.GetRequiredService<ILogger<Program>>();
logger.LogCritical("critical message working");
// Begin a new scope. This is optional. Epecially in case of AspNetCore request info is already
// present in scope.
using (logger.BeginScope(new Dictionary<string, object> { { "Method", nameof(Main) } }))
{
logger.LogWarning("Logger is working - warning"); // this will be captured by Application Insights.
}
}
The code is correct, but you are hitting a known issue with ApplicationInsights and Console apps - the app is dying before ApplicationInsights can send the data to the backend. (data is not sent immediately, but batched and sent at intervals.)
Adding a sleep of ~30 secs should help your case.
Thread.Sleep(31000);
In regular console apps, docs suggest doing an explicit flush.
https://learn.microsoft.com/en-us/azure/azure-monitor/app/console#full-example
But in the ILogger case, you don't control the TelemetryClient instance. So your best alternative is to control the channel, and call flush on the channel followed by a small sleep. Modified code is given below.
class Program
{
static void Main(string[] args)
{
// Create DI container.
IServiceCollection services = new ServiceCollection();
var channel = new InMemoryChannel();
services.Configure<TelemetryConfiguration>(
(config) =>
{
config.TelemetryChannel = channel;
}
);
// Add the logging pipelines to use. We are using Application Insights only here.
services.AddLogging(loggingBuilder =>
{
// Optional: Apply filters to configure LogLevel Trace or above is sent to ApplicationInsights for all
// categories.
loggingBuilder.AddFilter<ApplicationInsightsLoggerProvider>("", LogLevel.Trace);
loggingBuilder.AddApplicationInsights("***");
});
// Build ServiceProvider.
IServiceProvider serviceProvider = services.BuildServiceProvider();
ILogger<Program> logger = serviceProvider.GetRequiredService<ILogger<Program>>();
logger.LogCritical("critical message working");
// Begin a new scope. This is optional. Epecially in case of AspNetCore request info is already
// present in scope.
using (logger.BeginScope(new Dictionary<string, object> { { "Method", nameof(Main) } }))
{
logger.LogWarning("Logger is working - warning"); // this will be captured by Application Insights.
}
channel.Flush();
Thread.Sleep(1000);
}
}

IdentityServer3 - rejected because invalid CORS path

We have an ASP.NET MVC application that is authenticating without issue against IdentityServer3, however the web API part of the application using ApiController's start to fail if the user waits before proceeding with AJAX functionality after about 3 minutes (before 3 mins everything seems fine).
The errors seen in Chrome are:
XMLHttpRequest cannot load
https://test-auth.myauthapp.com/auth/connect/authorize?client_id=ecan-farmda…gwLTk5ZjMtN2QxZjUyMjgxNGE4MDg2NjFhZTAtOTEzNi00MDE3LTkzNGQtNTc5ODAzZTE1Mzgw.
No 'Access-Control-Allow-Origin' header is present on the requested
resource. Origin 'http://test.myapp.com' is therefore not allowed
access.
On IE I get the following errors:
SCRIPT7002: XMLHttpRequest: Network Error 0x4c7, The operation was
canceled by the user.
Looking at IdentityServer3's logs I'm seeing entries like so:
2015-08-10 16:42 [Warning]
(Thinktecture.IdentityServer.Core.Configuration.Hosting.CorsPolicyProvider)
CORS request made for path: /connect/authorize from origin:
http://test.myapp.com but rejected because invalid CORS path
In the IdentityServer3 web application I'm giving clients AllowedCorsOrigins:
Thinktecture.IdentityServer.Core.Models.Client client = new Thinktecture.IdentityServer.Core.Models.Client()
{
Enabled = configClient.Enabled,
ClientId = configClient.Id,
ClientName = configClient.Name,
RedirectUris = new List<string>(),
PostLogoutRedirectUris = new List<string>(),
AllowedCorsOrigins = new List<string>(),
RequireConsent = false, // Don't show consents screen to user
RefreshTokenExpiration = Thinktecture.IdentityServer.Core.Models.TokenExpiration.Sliding
};
foreach (Configuration.RegisteredUri uri in configClient.RedirectUris)
{
client.RedirectUris.Add(uri.Uri);
}
foreach (Configuration.RegisteredUri uri in configClient.PostLogoutRedirectUris)
{
client.PostLogoutRedirectUris.Add(uri.Uri);
}
// Quick hack to try and get CORS working
client.AllowedCorsOrigins.Add("http://test.myapp.com");
client.AllowedCorsOrigins.Add("http://test.myapp.com/"); // Don't think trailing / needed, but added just in case
clients.Add(client);
And when registering the service I add a InMemoryCorsPolicyService:
app.Map("/auth", idsrvApp =>
{
var factory = new IdentityServerServiceFactory();
factory.Register(new Registration<AuthContext>(resolver => AuthObjects.AuthContext));
factory.Register(new Registration<AuthUserStore>());
factory.Register(new Registration<AuthRoleStore>());
factory.Register(new Registration<AuthUserManager>());
factory.Register(new Registration<AuthRoleManager>());
// Custom user service used to inject custom registration workflow
factory.UserService = new Registration<IUserService>(resolver => AuthObjects.AuthUserService);
var scopeStore = new InMemoryScopeStore(Scopes.Get());
factory.ScopeStore = new Registration<IScopeStore>(scopeStore);
var clientStore = new InMemoryClientStore(Clients.Get());
factory.ClientStore = new Registration<IClientStore>(clientStore);
var cors = new InMemoryCorsPolicyService(Clients.Get());
factory.CorsPolicyService = new Registration<ICorsPolicyService>(cors);
...
var options = new IdentityServerOptions
{
SiteName = "Authentication",
SigningCertificate = LoadCertificate(),
Factory = factory,
AuthenticationOptions = authOptions
};
...
});
I do note that the IdentityServer3 log entries say "CORS request made for path: /connect/authorize" rather than "CORS request made for path: /auth/connect/authorize". But looking through the IdentityServer3 source code suggests this probably isn't the issue.
Perhaps the InMemoryCorsPolicyService isn't being picked up?
Any ideas of why things aren't working for the AJAX called ApiController?
Thinktecture.IdevtityServer3 v1.6.2 has been installed using NuGet.
Update
I'm having a conversation with the IdentityServer3 developer, but am still having an issue reaching a resolution. In case it helps:
https://github.com/IdentityServer/IdentityServer3/issues/1697
Did you try adding https url also?- client.AllowedCorsOrigins.Add("https://test.myapp.com");
The documentation of IdentityServer says you should configure it on the client:
AllowedCorsOrigins = ... // Defaults to the discovery, user info, token, and revocation endpoints.
https://docs.duendesoftware.com/identityserver/v6/reference/options/#cors
CORS is a nightmare!
It's a browser thing which is why you're witnessing different behaviour in IE than in Chrome.
There are (at least) two ways that CORS is configured on the server. When a client makes a request with the Origin header you have to tell the server whether or not to accept it -- if accepted then the server adds the Access-Control-Allow-Origin header to the response for the browser.
In MVC / webAPI you have to add CORS services, set a CORS policy, and then .UseCors something like this:
builder.Services.AddCors((options =>
{
if (settings.AllowedCorsOrigins.Length > 0)
{
options.AddDefaultPolicy(builder =>
{
builder.SetIsOriginAllowedToAllowWildcardSubdomains();
builder.AllowAnyHeader().AllowAnyMethod().WithOrigins(settings.AllowedCorsOrigins);
});
}
if (isDevelopment)
{
options.AddPolicy("localhost", builder =>
{
builder.SetIsOriginAllowedToAllowWildcardSubdomains();
builder.AllowAnyHeader().AllowAnyMethod().SetIsOriginAllowed((string origin) => { return origin.Contains("localhost"); }); });
}
});
and
app.UseCors();
if (app.Environment.IsDevelopment())
{
app.UseCors("localhost");
}
Typically, you want the list of allowed hosts as an array of strings in your appsettings.json. And watch out for the boobytrap with SetIsOriginAllowedToAllowWildcardSubdomains.
As well as this, IdentityServer has its own additional CORS settings which are applied in addition to the standard MVC/webAPI settings. These are in the ClientCorsOrigin table and this doesn't support wildcard subdomains. You can sidestep this whole boobytrap by implementing your own ICorsPolicyService to use the same settings from your appsettings.json something like this
public class CorsPolicyService : ICorsPolicyService
{
private readonly CorsOptions _options;
public CorsPolicyService(IOptions<CorsOptions> options)
{
_options = options.Value;
}
private bool CheckHost(string host)
{
foreach (string p in _options.AllowedCorsOrigins)
{
if (Regex.IsMatch(host, Regex.Escape(p).Replace("\\*", "[a-zA-Z0-9]+"))) // Hyphen?
{
return true;
}
}
return false;
}
public Task<bool> IsOriginAllowedAsync(string origin)
{
return Task.FromResult(CheckHost(origin));
}
}

Categories