Application Insights logging: ILogger, Serilog, severity levels and the traces table
In .NET, logs reach Application Insights through ILogger: each log becomes a row in traces (AppTraces), or in exceptions when you pass an exception, with its level mapped to severityLevel 0 (Verbose) through 4 (Critical). The classic 2.x provider sends only Warning and above unless you configure the ApplicationInsights log level; SDK 3.x and the Microsoft OpenTelemetry Distro log through OpenTelemetry's provider, configured under the OpenTelemetry alias, and drop BeginScope values unless IncludeScopes is on. Serilog can write through its Application Insights sink or through ILogger.
Logs are usually the largest and the least planned part of an Application Insights bill, and the part people most often can’t find. Both problems come from the same few settings: which provider sends logs, at what level, and with what context.
Where logs land
Every log written through ILogger becomes a trace item in traces (AppTraces in the workspace), with the rendered message in message, the level in severityLevel, and structured values (the {OrderId} in "Order {OrderId} placed") in customDimensions. A log that includes an exception becomes an item in exceptions instead. Logs written during a request share its operation_Id, so they appear in the request’s end-to-end view and can be joined to it in KQL.
Severity is a number, from the classic SeverityLevel enum:
ILogger level | severityLevel | Name |
|---|---|---|
| Trace, Debug | 0 | Verbose |
| Information | 1 | Information |
| Warning | 2 | Warning |
| Error | 3 | Error |
| Critical | 4 | Critical |
The mapping from LogLevel is the same in the classic provider and in Microsoft’s Azure Monitor exporter for OpenTelemetry, which sends both Trace and Debug as Verbose.
Warnings and above from the last hour, newest first:
traces
| where timestamp > ago(1h) and severityLevel >= 2 // 2 = Warning, 3 = Error, 4 = Critical
| project timestamp, severityLevel, message, cloud_RoleName, operation_Id, customDimensions
| order by timestamp desc
Write logs that query well
What you can do with logs later is decided when you write them. OpenTelemetry’s .NET logging guidance, which applies equally to the classic provider, comes down to a few habits:
- Use message templates, not string interpolation.
logger.LogInformation("Order {OrderId} placed", id)storesOrderIdas its own dimension you can filter and group by;$"Order {id} placed"buries it in the text. - Prefer compile-time logging. The
[LoggerMessage]source generator produces efficient logging methods and keeps templates consistent across call sites. - Name categories with dots. Categories such as
Contoso.Orders.Paymentsare what level filters match on, so a clear hierarchy lets you raise one area toDebugwithout flooding the rest. - Create loggers once. Take them from dependency injection rather than creating new ones in hot paths.
Personal data deserves the same care as in any other telemetry: log the order ID, not the customer’s email address.
Classic SDK 2.x
Microsoft.ApplicationInsights.AspNetCore includes the ApplicationInsightsLoggerProvider, so ASP.NET Core apps on the 2.x SDK collect ILogger output without extra setup. The Microsoft.Extensions.Logging.ApplicationInsights package alone gives log forwarding without the rest of the telemetry. The provider has one surprise: by default it sends only Warning and above, regardless of your general Logging:LogLevel. To send Information, configure the provider by its alias, ApplicationInsights.
appsettings.json for the 2.x provider, sending Information for your code and Warning for the framework:
{
"Logging": {
"LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" },
"ApplicationInsights": {
"LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" }
}
}
}
Settings under Logging:{provider}:LogLevel override Logging:LogLevel for that provider only. In code, the equivalent is builder.Logging.AddFilter<ApplicationInsightsLoggerProvider>("category", LogLevel.Information).
Scopes are on by default in 2.x: a scope of key-value pairs adds each pair as a custom property, and any other scope object is stored under a property named Scope. Classic ASP.NET apps don’t use ILogger this way; they forward System.Diagnostics.Trace, NLog or log4net through adapter packages, as the ASP.NET MVC guide describes.
Today: SDK 3.x and the Microsoft OpenTelemetry Distro
In SDK 3.x, Microsoft.Extensions.Logging.ApplicationInsights is unnecessary and has no 3.x version: the ASP.NET Core and Worker Service packages collect ILogger logs as traces themselves, and even TrackTrace, TrackException and TrackEvent now go through ILogger internally. The Microsoft OpenTelemetry Distro does the same through OpenTelemetry’s logger provider. In both cases the provider’s configuration alias is OpenTelemetry, so the per-provider override moves:
appsettings.json filtering for the OpenTelemetry logger provider (SDK 3.x and the Distro):
{
"Logging": {
"LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" },
"OpenTelemetry": {
"LogLevel": { "Default": "Warning", "Contoso.Orders": "Information" }
}
}
}
Without that section, the general Logging:LogLevel applies, which in the default ASP.NET Core templates is Information: more than a 2.x app sent. Check the volume after upgrading.
Scopes are off by default. Microsoft’s migration guide warns that values added with BeginScope are silently dropped in 3.x unless IncludeScopes is enabled, and the OpenTelemetry SDK has the same default. Turn it on, and use a list of key-value pairs as the scope state for performance; Azure Monitor adds the values to the log’s custom properties.
Enabling scopes for SDK 3.x and the Distro, and a scope that adds properties to every log inside it:
builder.Services.Configure<OpenTelemetryLoggerOptions>(o => o.IncludeScopes = true);
using (logger.BeginScope(new List<KeyValuePair<string, object?>> { new("orderId", order.Id) }))
{
logger.LogInformation("Payment authorised for {Amount}", order.Total);
}
Sampling applies to logs. In .NET, trace-based log sampling is on by default (EnableTraceBasedLogsSampler): logs written inside a request that wasn’t sampled are dropped with it. That keeps logs consistent with requests, and it’s the usual reason a log you’re sure was written isn’t in traces. The sampling guide covers turning it off.
Custom events also go through ILogger with the Distro: a log whose message template includes the microsoft.custom_event.name attribute becomes a row in customEvents.
Serilog
Serilog.Sinks.ApplicationInsights writes Serilog events to Application Insights as trace telemetry (TelemetryConverter.Traces) or event telemetry (TelemetryConverter.Events). It sends the rendered message, the severity, the message template and every log property as custom dimensions; takes the operation ID from an OperationId or TraceId property; and reads Version into the component version. Its README recommends reusing the app’s existing TelemetryConfiguration from dependency injection rather than the legacy TelemetryConfiguration.Active.
Serilog writing to Application Insights with the app’s own telemetry configuration:
builder.Services.AddSerilog((services, lc) => lc
.Enrich.FromLogContext()
.WriteTo.ApplicationInsights(services.GetRequiredService<TelemetryConfiguration>(), TelemetryConverter.Traces));
The sink is built on the classic TelemetryClient, and Microsoft’s migration page names it as a third-party package to check for SDK 3.x support before upgrading. With the Distro there’s no TelemetryConfiguration to reuse; the cleaner route is to keep ILogger as the logging API, or to send Serilog’s output over OTLP with the OpenTelemetry sink to a Collector that forwards to Azure Monitor.
What debug logs cost
Logs are billed like all ingested data, by the GB. A Debug level left on in production, or framework categories at Information, can multiply the volume of traces overnight. Three controls, in order: set levels per category so only your code logs at Information; let trace-based sampling drop logs with their requests; and, if you mostly read logs during incidents, move AppTraces to the Basic table plan, the only Application Insights table that supports it. The pricing guide has the prices and trade-offs.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Only warnings and errors arrive (2.x) | Provider default is Warning | Set Logging:ApplicationInsights:LogLevel |
| Log volume jumped after moving to 3.x | General Information level now applies | Add an OpenTelemetry log level section |
BeginScope properties missing (3.x, Distro) | Scopes off by default | IncludeScopes = true |
| Some request logs missing | Trace-based log sampling | Expected; disable only if every log matters |
Exceptions not in traces | Logged with an exception object | Query exceptions instead |
| Serilog logs not correlated | No OperationId/TraceId property | Enrich from the activity or log context |
Queries for this topic
-
Noisiest trace messages by volume and severity
traces cost
Returns: Up to 25 rows, one per message prefix and severity level, with the sampling-adjusted number of trace items, their billed megabytes and the services that wrote them, most expensive first.
-
Exceptions by type and operation
exceptions exceptions
Returns: Up to 50 rows, one per exception type and operation, with the sampling-adjusted count, the number of distinct problem IDs, one outer message and one innermost exception type.
Frequently asked questions
Why don't my Information logs show up in Application Insights?
With the classic 2.x provider, only Warning and above are sent by default; add an ApplicationInsights section with its own LogLevel in appsettings.json. With SDK 3.x or the Distro, check the OpenTelemetry provider's level and the category filters, then sampling: logs from unsampled requests are dropped in .NET by default.
How do I see logs in Application Insights?
Open Logs and query traces (AppTraces in the workspace), filtering by severityLevel, message or cloud_RoleName. Logs written during a request share its operation_Id, so the request's end-to-end transaction view lists them too. Logged exceptions are in the exceptions table.
Should I use Serilog's sink or ILogger?
If the app already uses Microsoft.Extensions.Logging, log through ILogger and let the SDK or Distro export it: one pipeline, sampling-aware, correlated. Use the Serilog sink when Serilog is your logging API; it needs the classic TelemetryClient, so check its support before upgrading to SDK 3.x.
Sources, checked
- Monitor .NET and Node.js applications with Application Insights (Classic API 2.x, traces and ILogger)
- Logging in .NET and ASP.NET Core (configuration and provider aliases)
- Customizing the OpenTelemetry .NET SDK for logs (IncludeScopes, filtering)
- Migration guidance from 2.x to 3.x (ILogger scopes, logging packages)
- Application Insights SDK 3.x for ASP.NET Core (log scopes)
- Azure Monitor exporter LogsHelper (LogLevel to SeverityLevel mapping, release 1.10.0)
- SeverityLevel enum (Microsoft.ApplicationInsights.DataContracts)
- Serilog.Sinks.ApplicationInsights (README)