Application Insights is not including properties set by custom ITelemetryInitializer - c#

(Added UPDATE 1 below which I think answers this question)
In a fairly simple ASP.NET Core 2 web app I have initialized in Program like this:
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<Startup>()
.UseApplicationInsights()
.Build();
I haven't configured an instrumentation key in appSettings.json yet because for now I'm running locally. When I run my app and force an exception on load of my home page I can see the exception logged in Visual Studio's telemetry search for Application Insights.
I now want to capture some details about the request in every logged event. I've followed some guidance and created an implementation of ITelemetryInitializer:
public class CustomTelemetryInitializer : ITelemetryInitializer
{
private const string UserIdKey = "UserId";
private readonly IUserService _userService;
public CustomTelemetryInitializer(IUserService userService)
{
_userService = userService;
}
public void Initialize(ITelemetry telemetry)
{
if (!(telemetry is RequestTelemetry requestTelemetry)) return;
var props = requestTelemetry.Properties;
if (!props.ContainsKey(UserIdKey))
{
var user = _userService.GetCurrentUser();
if (user != null)
{
props.Add(UserIdKey, user.UserId);
}
}
}
}
This is largely following this guide. IUserService is just a service which uses IHttpContextAccessor to get the current ClaimsPrincipal. You can see I'm trying to add the user's ID to the custom telemetry properties.
I've registered this ITelemetryInitializer in Startup like this:
services.AddSingleton<ITelemetryInitializer, CustomTelemetryInitializer>();
When I run my app again, I can see the debugger running through CustomTelemetryInitializer and setting the property correctly. However, when I review the events logged in app insights, the custom property is not included. They look identical to the screenshot above.
I've tried moving the app insights initialization out of Program and instead initializing it in Startup after registering the ITelemetryInitializer using services.AddApplicationInsightsTelemetry(), but this makes no difference.
Anyone know what I'm doing wrong?
UPDATE 1
I've realised I made a mistake. My custom properties are being included for Request events after all. But not the Exception events. But I've now realised that these Exception events take a different implementation of ITelemetry in Initialize i.e. TraceTelemetry. So I hadn't realised I was excluding these events from my custom properties.

Glad you figured it out.
All ITelemetry implementations in the SDK implements, ISupportProperties which gives it Properties collection. If you want to attach properties to every telemetry, you can cast to ISupportProperties and set props.

Related

Using Options Pattern for settings .NET Core app not working; settings file always has null properties

I have a straight-forward .NET 6 app using full Program and Startup classes (nothing top-level). I have the following segment in Startup:
IConfigurationSection oauthSection = Configuration.GetSection("OAuth");
services.Configure<OAuthSettings>(oauthSection);
var oauthSettings = oauthSection.Get<OAuthSettings>();
This is OAuthSettings:
public class OAuthSettings
{
public OAuthSettings()
{
CorsAllowedOrigins = new List<string>();
}
public string BaseUrl { get; set; }
public string DefaultSchema { get; set; }
public IEnumerable<string> CorsAllowedOrigins { get; set; }
}
I'm injecting the IOptions<OAuthSettings> object in another class from an adjacent library (but still part of the DI container), like the below:
private readonly IOptions<OAuthSettings> _oAuthOptions;
public IdentityServerDataProtectionDbContext(
DbContextOptions<IdentityServerDataProtectionDbContext> options,
IOptions<OAuthSettings> oAuthOptions)
: base(options)
{
_oAuthOptions = oAuthOptions;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.HasDefaultSchema(_oAuthOptions.Value.DefaultSchema);
}
However, every single time I try to call _oAuthOptions.Value, all of the properties are null, and the implementation is of UnnamedOptionsManager. This is happening despite the fact that oauthSettings in the first snippet resolves the settings just fine, meaning it isn't a problem with deserialization.
Any ideas? Thanks in advance.
Use IOptionsSnapshot<> or IOptionsMonitor<> instead.
This took me way too many hours to figure out, so I wanted to answer my own question here to hopefully prevent someone else the pain.
The documentation for the various Options interfaces explains the issue better than I could (emphasis mine):
IOptions:
Does not support:
Reading of configuration data after the app has started.
Named options
Is registered as a Singleton and can be injected into any service lifetime.
IOptionsSnapshot:
Is useful in scenarios where options should be recomputed on every injection resolution, in scoped or transient lifetimes. For more information, see Use IOptionsSnapshot to read updated data.
Is registered as Scoped and therefore cannot be injected into a Singleton service.
Supports named options
IOptionsMonitor:
Is used to retrieve options and manage options notifications for TOptions instances.
Is registered as a Singleton and can be injected into any service lifetime.
Supports:
Change notifications
Named options
Reloadable configuration
Selective options invalidation (IOptionsMonitorCache)

Why is my EF Core DbContext not bound by DI?

I have an Azure Function App with a function that runs on a blob trigger. I've proven that this function can run through the Azure Portal and responds to this blob trigger without issues... or at least it did.
Now that I've added functionality which makes use of EF Core (2.2.4), it gives me the following error, both when debugging locally and when publishing to Azure:
Microsoft.Azure.WebJobs.Host: Error indexing method 'ParseThings'. Microsoft.Azure.WebJobs.Host: Cannot bind parameter 'context' to type AvastusContext. Make sure the parameter Type is supported by the binding. If you're using binding extensions (e.g. Azure Storage, ServiceBus, Timers, etc.) make sure you've called the registration method for the extension(s) in your startup code (e.g. builder.AddAzureStorage(), builder.AddServiceBus(), builder.AddTimers(), etc.).
I have a Startup class as instructed by Azure Function App documentation here, and have followed their example to the letter, aside from the following line in place of their configured example services:
[assembly: FunctionsStartup(typeof(AvstFunctionApp.Startup))]
namespace AvstFunctionApp
{
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddDbContext<AvastusContext>(options => options.UseSqlServer(Environment.GetEnvironmentVariable("AvastusDb")));
}
}
}
And the beginning of my function:
public static class ParseThings
{
[FunctionName("ParseThings")]
public static void Run([BlobTrigger("summaries/{name}", Connection = "StorageConnectionString")]Stream myBlob, string name, ILogger log, AvastusContext context)
I can confirm that the AddDbContext line is getting hit in a debugger, so presumably there's some bug happening behind the scenes here, or I'm doing something incredibly silly.
Things I've tried that haven't worked include:
Adding .BuildServiceProvider(true) to the AddDbContext line
Using WebJobsStartup instead of the more recently advertised FunctionsStartup
Downgrading to .NET Core 2.2.0
Changing the Function class and Run method from static to instance
Fixing incorrect namespace of the injected AvastusContext
It's also worth noting that there are two other functions in this Function App project which don't seem to have any serious issues, and I've been able to get dependency injection working using a similar method with EF Core for another (ASP.NET Core MVC) project in this solution.
Thank you in advance for any help anyone can provide!
P.S. I find it incredibly weird that there hasn't been anything describing this situation with the later versions of .NET Core, Azure Function Apps, and EF Core on the interwebs, which leads me to believe that this might be a simple mistake. Hopefully not.
perhaps one solution can be you can try injecting IServiceProvider in your function instead of AvastusContext like I have injected in the repository class below:
private readonly IServiceProvider serviceProvider;
public SomeRepository(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider;
}
using var context = this.serviceProvider.GetService<XYZDBContext>();
This will provide a context object to you. Also, Not sure why you are trying to access context in the function directly for good practice have a context class defined, and maintain repository to do any CRUD operation in the code.
Startup.cs you can add extra configurations like :
builder.Services.AddDbContext<XYZDBContext>(
options =>
{
options.UseSqlServer(
conn,
sqlServerOptionsAction:
sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(maxRetryCount: 3, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null);
});
}, ServiceLifetime.Transient);
this configuration works perfectly fine in my current solution. Try this out.
Function app can't resolve dbcontext in functions as it can only resolve BindingContext. You need to create custom bindings to use dbcontext directly in function app.
Other way to get dbcontext injected via DI is to pass it to constructor and using a class level variable in the function.
public class ParseThings
{
private AvastusContext _context;
public ParseThings(AvastusContext context){
_context = context;
}
[FunctionName("ParseThings")]
public void Run([BlobTrigger("summaries/{name}", Connection = "StorageConnectionString")]Stream myBlob, string name, ILogger log){
// use _context here
}
}
If it still doesn't resolve you might want to look it into whether the functionsStartup is configured properly

Using static variables in C# Web Application - Accessing Session variables in class properties

Forgive me for my lack of coding knowledge as well as ability to ask the right question.
I'm rather new to this ASP.Net Web Application thing (Core), yet I still wondered..
In my current application, I have a class that has a property in which it gets it from a static variable, set when a user requests a controller. So the flow is: User sends a request with a variable in body, if not specified in body, the StaticClass.StaticProperty (example) is then set to the variable the user specified in the body (or default = 0), data is returned based upon the variable.
Yet I wondered, since there is no thread guarantee on this variable, whether or not this could be changed or messed up when the web application gets 50,000 requests at once?
I looked into sessions and tried the following:
service.AddSession(); //Not sure this even does anything?
HttpContext.Session.SetString //Setting this works in the controller, but I cant access it elsewhere by GetString
System.Web.HttpContext.Current.Session["test"] // Cant even access System.Web.Httpcontext, doesn't seem to exist.
HttpContext.Current //doesn't exist either
Session["test"] //doesn't exist either
Can I send a session over somewhere? I'm pretty lost.
Not sure if any of this made sense, I'll try to elaborate if needed.
Thank you in advance.
EDIT: Updated info.
I have added this to my startup.cs:
services.AddSingleton();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromSeconds(10);
options.Cookie.HttpOnly = true;
});
and
app.UseSession();
Setting the Session variable:
https://i.imgur.com/CY8rcdk.png
Using the Session variable:
https://i.imgur.com/SuLJKzV.png
Variable is always null.
Thank you for trying to help.
HttpContext is accessible only from things that are request specific, since it's a context of one and only request. And new controller instances are created by the framework for each request, with injected HttpContext. It's the developers job to pass it further if the need arises.
I recommend reading this article about it: https://dotnetcoretutorials.com/2017/01/05/accessing-httpcontext-asp-net-core/
First in your startup.cs, you need to register IHttpContextAccessor as a service like so :
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}
When you create a helper/service class, you can then inject in the IHttpContextAccessor and use it. It would look like something not too dissimilar to this :
public class UserService : IUserService
{
private readonly IHttpContextAccessor _httpContextAccessor;
public UserService(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public bool IsUserLoggedIn()
{
var context = _httpContextAccessor.HttpContext;
return context.User.Identities.Any(x => x.IsAuthenticated);
}
}

HangFire dashboard not displaying in PROD

I am using HangFire to schedule jobs but when I deployed to PROD, the website/hangfire url is not working. I am getting The system cannot find the file specified error.
On localhost, I am able to open the URL.
I followed this URL: http://docs.hangfire.io/en/latest/quick-start.html
Anyone know what I am missing.
Thanks
Hangfire Dashboard exposes sensitive information about your background jobs, including method names and serialized arguments as well as gives you an opportunity to manage them by performing different actions – retry, delete, trigger, etc. So it is really important to restrict access to the Dashboard.
To make it secure by default, only local requests are allowed, however you can change this by passing your own implementations of the IAuthorizationFilter interface, whose Authorize method is used to allow or prohibit a request. The first step is to provide your own implementation.
http://docs.hangfire.io/en/latest/configuration/using-dashboard.html#configuring-authorization
As Hangfire dashboard exposes sensitive information about your job which includes method names and serialized arguments. Also user can perform different actions like retry, trigger, delete etc. So it is very important to authenticate access to Dashboard.
By default Hangfire allows access to Dashboard pages only for local requests. In order to give appropriate rights for production or testing or UAT users, add your own implementation of authorization using the IDashboardAuthorizationFilter interface for the hangfire dashboard.
http://docs.hangfire.io/en/latest/configuration/configuring-authorization.html
See my sample code below
public class HangfireAuthorizationFilter : IDashboardAuthorizationFilter
{
private readonly string[] _roles;
public HangfireAuthorizationFilter(params string[] roles)
{
_roles = roles;
}
public bool Authorize(DashboardContext context)
{
var httpContext = ((AspNetCoreDashboardContext)context).HttpContext;
//Your authorization logic goes here.
return true; //I'am returning true for simplicity
}
}
Asp.net core startup class changes in Configure(IApplicationBuilder app, IHostingEnvironment env) method
Configure(IApplicationBuilder app, IHostingEnvironment env){
......
app.UseHangfireServer();
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
DashboardTitle = "Sample Jobs",
Authorization = new[]
{
new HangfireAuthorizationFilter("admin")
}
});
......
}
Maybe a late answer but might be usefull.
In my case, I had this code :
public class Startup
{
public void Configuration(IAppBuilder app)
{
#if !DEBUG
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new[] { new HangfireAuthFilter() }
});
#endif
}
}
}
It wasn't working on prod.
I realized that when I copy the application dll from the bin folder, it takes the debug configuration and doesn't start the hangfire.
When I publish the application via visual studio and than copy the DLL from the bin folder of published folder, it works correctly.

TraceListener in OWIN Self Hosting

I am using Microsoft.Owin.Hosting to host the following, very simple web app.
Here is the call to start it:
WebApp.Start<PushServerStartup>("http://localhost:8080/events");
Here is the startup class I am using:
public class PushServerStartup
{
public void Configuration(IAppBuilder app)
{
app.MapHubs();
}
}
I am running this inside a console application that does a lot of other things including routing trace writing to certain files etc. But all of a sudden (when activating the OWIN hosting) I am seeing trace messages written to the console that are normally routed somewhere else.
Obviously there are some trace listeners active in the OWIN hosting framework. How can I switch them off?
I had the same issue, I was self hosting 4 instances in one process and for each request was getting 4 lots of messages traced to console.
I simply removed the TraceListener instance
Trace.Listeners.Remove("HostingTraceListener")
"HostingTraceListener" is defined in the owin source code so I guess could change
- http://katanaproject.codeplex.com/SourceControl/latest#src/Microsoft.Owin.Hosting/Engine/HostingEngine.cs
I did this after
WebApp.Start(...
An alternative to the answer from meilke that works with latest Katana self-host (2.1.0):
StartOptions options = new StartOptions("http://localhost:8080/events");
// disable built-in owin tracing by using a null traceoutput
options.Settings.Add(
typeof(Microsoft.Owin.Hosting.Tracing.ITraceOutputFactory).FullName,
typeof(NullTraceOutputFactory).AssemblyQualifiedName);
using (WebApp.Start<PushServerStartup>(options))
NullTraceOutputFactory is similar to DummyFactory but using StreamWriter.Null instead of StringWriter:
public class NullTraceOutputFactory : ITraceOutputFactory
{
public TextWriter Create(string outputFile)
{
return StreamWriter.Null;
}
}
I found a solution myself. After studying the Katana source code it seems like you need to register your own ITraceOutputFactory instance to overrule the default trace listener (which is writing to the console).
Here is the new start call:
var dummyFactory = new DummyFactory();
var provider = ServicesFactory.Create(
defaultServiceProvider => defaultServiceProvider.AddInstance<ITraceOutputFactory>(dummyFactory));
using (WebApp.Start<Startup>(provider, new StartOptions("http://localhost:8090")))
{
Console.ReadLine();
}
And here is a dummy trace factory (maybe not the best solution but you can replace it with something serving your purpose a little better):
public class DummyFactory : ITraceOutputFactory
{
public TextWriter Create(string outputFile)
{
return TextWriter.Null;
}
}

Categories

Resources