Application Insights for desktop and console apps
A console, desktop or background app sends telemetry the same way a web app does, but nothing creates requests for you and the process can end before the buffer is sent. With the Microsoft OpenTelemetry Distro, create the SDK once with OpenTelemetrySdk.Create, wrap each unit of work in an Activity, and dispose the SDK only at shutdown, which flushes it. With the classic 2.x SDK, use the Worker Service package, wrap work in StartOperation, and call Flush() followed by a short delay before the process exits.
Web apps get a lot for free: the framework starts a request for every HTTP call, the host keeps the process alive, and shutdown is orderly. A console tool, a scheduled job, a worker service or a WPF app has none of that. The three things to add are a long-lived pipeline, an explicit unit of work, and a flush before exit.
What changes outside a web host
- No automatic requests. Requests come from server frameworks. In a console app, nothing appears in
requestsunless you start an operation yourself, and every dependency or log then has no parent to group under. - Short lives. Telemetry is batched. The classic in-memory channel sends every 30 seconds or every 500 items; a job that runs for ten seconds and exits loses everything still in the buffer.
- Unreliable networks. Desktop apps run on laptops that sleep and change networks, so offline storage and retry matter more than on a server.
- End-user machines. The connection string ships with the app, and the telemetry describes people, not servers.
Classic SDK 2.x
The .NET Classic API 2.x is deprecated and retires on March 31, 2027, but plenty of tools still run on it. The package Microsoft documents for console and background apps is Microsoft.ApplicationInsights.WorkerService. It targets netstandard2.0 (so it works on .NET and .NET Framework) and brings the dependency collector, performance counters and the ILogger provider with it.
A 2.x console app with dependency tracking, a request per unit of work, and a flush at the end:
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.WorkerService;
using Microsoft.Extensions.DependencyInjection;
IServiceCollection services = new ServiceCollection();
services.AddApplicationInsightsTelemetryWorkerService(
(ApplicationInsightsServiceOptions o) => o.ConnectionString = "<connection string>");
using var provider = services.BuildServiceProvider();
var telemetry = provider.GetRequiredService<TelemetryClient>();
var http = new HttpClient();
using (telemetry.StartOperation<RequestTelemetry>("NightlyExport"))
{
await http.GetAsync("https://example.com/export"); // tracked as a dependency of NightlyExport
telemetry.TrackEvent("ExportCompleted");
}
telemetry.Flush();
await Task.Delay(5000); // Microsoft's console sample waits after Flush()
The delay matters because the channel decides whether Flush() blocks. The in-memory channel (the SDK’s default when nothing else is configured) flushes synchronously, but it never retries and never writes to disk. The server channel retries and keeps a storage folder, and its Flush() returns before the send completes. Microsoft’s console sample sleeps after Flush() for that reason; FlushAsync() is the documented alternative that needs no sleep.
Dependencies without a web host
The original way to get dependency tracking into a bare console app was to install Microsoft.ApplicationInsights.DependencyCollector and initialize DependencyTrackingTelemetryModule against TelemetryConfiguration.Active. That still describes the mechanism, but TelemetryConfiguration.Active is obsolete on .NET Core; the Worker Service package does the same wiring for you. The module tracks HTTP, SQL (SqlClient), Azure Storage, Event Hubs, Service Bus and Cosmos DB calls over HTTP; anything else needs TrackDependency.
WPF and WinForms
Microsoft’s current docs give no WPF- or WinForms-specific guidance, so treat a desktop app as a long-running console app. The Worker Service pattern works: build the client in App.OnStartup (WPF) or Program.Main (WinForms), keep one TelemetryClient, start an operation around each user action you care about, and flush in OnExit or FormClosing. Hook AppDomain.CurrentDomain.UnhandledException and call TrackException there, then flush, because the process is about to end.
Today: SDK 3.x and the Microsoft OpenTelemetry Distro
The Microsoft OpenTelemetry Distro
For console and other non-hosted .NET apps, Microsoft’s instructions are to create the SDK before your code runs, keep it alive for the life of the app, and dispose it only at shutdown, which flushes pending telemetry. Microsoft.OpenTelemetry targets net8.0 and netstandard2.0, and it includes HttpClient, SqlClient and Azure SDK instrumentation, so dependency tracking needs no extra package.
A console app on the Distro, with one request per job and a flush on exit:
using System.Diagnostics;
using Microsoft.OpenTelemetry;
using OpenTelemetry;
using OpenTelemetry.Trace;
var jobs = new ActivitySource("Contoso.NightlyExport");
// Reads APPLICATIONINSIGHTS_CONNECTION_STRING; disposing the SDK flushes it.
using var sdk = OpenTelemetrySdk.Create(telemetry =>
{
telemetry.UseMicrosoftOpenTelemetry(o => o.Exporters = ExportTarget.AzureMonitor)
.WithTracing(tracing => tracing.AddSource("Contoso.NightlyExport"));
});
using (var job = jobs.StartActivity("NightlyExport", ActivityKind.Server))
{
using var http = new HttpClient();
await http.GetAsync("https://example.com/export"); // a dependency under NightlyExport
}
Two details make this work. Every ActivitySource must be registered with AddSource(), or its activities are never recorded. And the kind decides the table: Server and Consumer activities land in requests, while Client, Producer and the default Internal land in dependencies. Name activities after the kind of work, not the item: NightlyExport, not Export 2026-10-21. The request naming guide covers why.
For a hosted Worker Service (Host.CreateApplicationBuilder), register the Distro on the host’s OpenTelemetry builder instead; the host disposes it on shutdown:
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddOpenTelemetry()
.UseMicrosoftOpenTelemetry(o => o.Exporters = ExportTarget.AzureMonitor);
builder.Services.AddHostedService<ExportWorker>();
await builder.Build().RunAsync();
For WPF and WinForms, the same rule applies as for 2.x: create the SDK at startup, keep the reference, dispose it on exit.
Offline storage
When the exporter can’t reach Application Insights, it caches telemetry to disk and retries for up to 48 hours, preferring recent events if it has to drop some. On Windows the default folder is %LOCALAPPDATA%\Microsoft\AzureMonitor, then %TEMP%\Microsoft\AzureMonitor; on Linux and macOS it’s under %TMPDIR%, /var/tmp or /tmp. Set options.AzureMonitor.StorageDirectory to move it, or options.AzureMonitor.DisableOfflineStorage = true to turn it off. For a desktop app, the per-user default is usually what you want. The channels and exporters guide compares this with the classic channels.
SDK 3.x
If the app already uses TelemetryClient, SDK 3.x keeps it: upgrade Microsoft.ApplicationInsights.WorkerService to 3.x, remove the 2.x-only packages (DependencyCollector, PerfCounterCollector, WindowsServer.TelemetryChannel and the rest), and replace TelemetryConfiguration.Active or new TelemetryClient() with TelemetryConfiguration.CreateDefault(). SDK 3.x requires a connection string and can fail at startup without one. The channels are gone (the Azure Monitor exporter does the batching and keeps StorageDirectory and DisableOfflineStorage), DeveloperMode has no replacement, and TelemetryClient.Flush() still flushes.
A 3.x client in a bare console app:
var config = TelemetryConfiguration.CreateDefault();
config.ConnectionString = Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING");
var telemetry = new TelemetryClient(config);
using (telemetry.StartOperation<RequestTelemetry>("NightlyExport"))
{
telemetry.TrackEvent("ExportCompleted");
}
telemetry.Flush();
Desktop apps and the people using them
A connection string inside a desktop installer is readable by anyone who has the installer. Microsoft states that the instrumentation key in it isn’t a security token: knowing it lets someone send data to your resource, not read from it. If junk data is a real risk, Microsoft Entra authentication with local authentication disabled limits ingestion to identities holding the Monitoring Metrics Publisher role, which in practice means a credential on every machine, so it suits managed fleets more than public apps.
Telemetry from end-user machines is also personal data in many jurisdictions. Ask for consent where the law requires it, and keep user names, file paths and machine names out of custom properties. The client IP guide explains what happens to addresses by default, and disabling telemetry in development shows how to switch it off per build.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Nothing arrives from a short job | Process exits before the batch is sent | Dispose the SDK (Distro) or Flush() plus a delay (2.x) |
| Dependencies but no requests | No operation started | Wrap work in a Server activity or StartOperation<RequestTelemetry> |
| Custom activities missing | ActivitySource not registered | Add .AddSource("<name>") with the exact source name |
| Data arrives hours late | Offline storage replaying after reconnect | Expected; data older than 48 hours is dropped |
| 3.x app fails at startup | No connection string | Set APPLICATIONINSIGHTS_CONNECTION_STRING or ConnectionString |
| Duplicate telemetry | Two pipelines (for example SDK 3.x and the Distro) | Keep one; Microsoft says not to combine them |
Frequently asked questions
Why does my console app send nothing?
Usually because it exits before the batch goes out. The classic in-memory channel sends every 30 seconds or 500 items, and the Distro batches too. Call Flush() and wait (2.x), or dispose the OpenTelemetry SDK at the end of Main, which flushes it.
Is it safe to put the connection string in a desktop app?
Users can read it, but Microsoft states that the instrumentation key in it isn't a security token. The risk is unwanted data in your resource, not access to your data. Microsoft Entra authentication blocks unauthenticated ingestion, but every machine then needs a credential.
What happens to telemetry when a laptop is offline?
The Distro's exporter stores it on disk (by default under %LOCALAPPDATA%\Microsoft\AzureMonitor on Windows) and retries for up to 48 hours. The classic in-memory channel keeps nothing on disk; only the server channel retries from a storage folder.
Sources, checked
- Enable OpenTelemetry in Application Insights (console and non-hosted apps)
- Add and modify OpenTelemetry in Application Insights (custom activities)
- Configure OpenTelemetry in Application Insights (offline storage)
- Monitor .NET and Node.js applications with Application Insights (Classic API 2.x)
- Migrate Application Insights Classic API SDKs to OpenTelemetry (SDK 3.x)
- Migration guidance from 2.x to 3.x (ApplicationInsights-dotnet)
- Connection strings in Application Insights
- Microsoft Entra authentication for Application Insights