Azure Monitor & Application Insights — Deep
Companion to Azure Monitor & Application Insights. This topic goes deeper on sampling internals, KQL for incident analysis, profiler/snapshot debugger, distributed tracing semantics, and cost control — the operational levers seniors actually pull.
Key Points
- Workspace-based App Insights is the modern path. Resources unify under a Log Analytics workspace; cross-resource KQL is a first-class capability.
- Three sampling layers: SDK-side adaptive (default — drops at source to save cost), SDK-side fixed-rate (deterministic), and server-side ingestion sampling (after data lands — most flexible, but you've already paid).
- Tables are the API:
requests,dependencies,traces,exceptions,pageViews,customEvents,customMetrics,availabilityResults,browserTimings,performanceCounters. Joins onoperation_Idare the workhorse. - Distributed tracing uses W3C Trace Context (
traceparent+tracestateheaders). The legacy Request-Id / Request-Context headers are deprecated. - Connection string > instrumentation key. CS supports regional ingest endpoints, sovereign clouds, and Live Metrics auth.
- Cost = ingestion volume × retention. Daily cap, sampling, and basic-logs/archive tiers are the three knobs.
- Profiler + Snapshot Debugger give prod-grade post-mortem: sampled CPU profiles + call stack & locals on uncaught exceptions.
- Workbooks > legacy Dashboards for parameterized, share-able reports.
Concepts (deep dive)
The base Azure Monitor & Application Insights topic covers the "what does it do" surface. This file is the operations layer: how sampling actually decides which spans get dropped (and what that costs you in lost outliers), how distributed tracing propagates across services via W3C Trace Context, how to find the slow query when an incident is live, and how to keep ingestion costs under control once volume becomes real. Every section below answers a question a senior gets asked at 2 AM during an incident.
Workspace-based App Insights
Old model: each AI resource had its own data store. Cross-resource queries were painful.
Modern model:
┌─────────────── Log Analytics Workspace ───────────────┐
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ AppInsights │ │ AppInsights │ │ Diagnostics │ │
│ │ (Web API) │ │ (Worker) │ │ (KeyVault, │ │
│ │ │ │ │ │ Cosmos…) │ │
│ └──────────────┘ └──────────────┘ └─────────────┘ │
│ │
│ Cross-resource KQL queries work across all of these │
└───────────────────────────────────────────────────────┘
When you create AI in the portal, "workspace-based" is now the only option. Migrate classic resources via the portal (one-click).
Sampling — three layers, three trade-offs
[App Process] [Ingest Endpoint] [Workspace]
│ │ │
│ Adaptive sampling (SDK) │ Ingestion sampling │
│ Fixed-rate sampling (SDK) │ (server-side) │
│ │ │
●──────── send (or drop) ─────────▶●───── store (or drop) ───▶●
▲ ▲
│ Saves cost AND egress │ Saves cost only
│ Loses fidelity for outliers │ Full sample available pre-decision
| Layer | Where | Knob | Saves egress? | Pro | Con |
|---|---|---|---|---|---|
| Adaptive (default) | SDK | Target items/sec | ✅ | Auto-tunes under load | Non-deterministic — may drop items you want |
| Fixed-rate | SDK | SamplingRatio = 0.1 | ✅ | Predictable | Doesn't react to bursts |
| Ingestion | Service | Per-table % | ❌ | Most flexible; change after the fact | Already paid for egress + ingest |
// Adaptive (OTel distro default)
builder.Services.AddOpenTelemetry().UseAzureMonitor();
// Fixed-rate at 10%
builder.Services.AddOpenTelemetry().UseAzureMonitor(o =>
{
o.SamplingRatio = 0.1f;
});
💡 Errors and dependencies-with-failures are usually preserved by adaptive sampling — it correlates by operation_Id so you don't get half-traces.
Tables — what's where
| Table | Content | Key columns |
|---|---|---|
requests | Inbound HTTP | name, resultCode, duration, success, operation_Id |
dependencies | Outbound (HTTP/SQL/queue) | type, target, data, duration, success |
traces | Logs (ILogger) | severityLevel, message, customDimensions |
exceptions | Thrown exceptions | type, outerMessage, details (call stack) |
pageViews | Browser nav | name, url, duration |
customEvents | App-defined business events | name, customDimensions |
customMetrics | App-defined gauges/counters | name, value, valueCount, valueSum, percentiles |
availabilityResults | Synthetic ping results | name, success, location |
browserTimings | Real-user perf (RUM) | networkDuration, processingDuration |
performanceCounters | OS/CLR counters | category, counter, value |
KQL deep — incident-grade queries
Joins across tables by operation_Id:
// Failed requests with their exceptions and slowest dependency
let failures =
requests
| where timestamp > ago(1h) and success == false
| project timestamp, name, operation_Id, duration;
let exns =
exceptions
| project operation_Id, exType=type, exMsg=outerMessage;
let slowDeps =
dependencies
| summarize maxDepDur=max(duration), depTarget=any(target) by operation_Id;
failures
| join kind=leftouter exns on operation_Id
| join kind=leftouter slowDeps on operation_Id
| project timestamp, name, duration, exType, exMsg, depTarget, maxDepDur
| order by duration desc
make-series for visualization-friendly metrics:
requests
| where timestamp > ago(24h)
| make-series p95=percentile(duration, 95) default=0
on timestamp from ago(24h) to now() step 5m
by name
| render timechart
summarize percentiles():
requests
| where timestamp > ago(1h)
| summarize percentiles(duration, 50, 95, 99, 99.9) by name
| order by percentile_duration_99 desc
mv-expand to fan out arrays / nested fields:
exceptions
| where timestamp > ago(1d)
| extend frames = parse_json(details)
| mv-expand frame=frames
| extend method = tostring(frame.parsedStack[0].method)
| summarize count() by method
| top 10 by count_
parse_json for customDimensions:
traces
| where message has "OrderPlaced"
| extend props = todynamic(customDimensions)
| extend orderId = tostring(props.OrderId), amount = todouble(props.Amount)
| summarize sum(amount) by bin(timestamp, 1h)
Live Metrics — sub-second observability
Live Metrics streams un-sampled telemetry directly from the SDK to the portal pane — bypasses ingestion entirely. Use during deploys, incidents, load tests. Latency: ~1 second.
⚠️ Live Metrics requires the SDK to authenticate to the portal session. Connection string with regional endpoints handles this; bare instrumentation key may not.
Application Map
Auto-built from operation_Id correlation across services:
[Frontend SPA]
│
▼
[Web API (200ms p95, 0.2% errors)]
│
├──▶ [SQL: orders DB (45ms p95)]
├──▶ [Cosmos: catalog (12ms p95)]
└──▶ [Service Bus: orders queue]
│
▼
[Worker (worker-prod)]
│
└──▶ [SQL: orders DB]
The map is generated from dependencies + cross-component requests data — no manual config.
Profiler & Snapshot Debugger
Profiler = sampling CPU profiler in production.
// Web app: enable in App Service blade
// Self-host: install Microsoft.ApplicationInsights.Profiler.AspNetCore
builder.Services.AddServiceProfiler();
Triggers: scheduled (2 min every hour), CPU spike, memory spike, manual. Output: function-level CPU breakdown, ETW-based, low overhead (~5%).
Snapshot Debugger captures the call stack + local variables when an uncaught exception fires.
Inspect the snapshot in Visual Studio — like a post-mortem dump but with locals. Limited captures per app per day.
✅ Use both in prod: profiler for perf regression hunts; snapshot for "we can't reproduce this exception".
Workbooks vs Dashboards
| Dashboards | Workbooks | |
|---|---|---|
| Authoring | Pin tiles | Markdown + KQL + visualizations |
| Parameters | ❌ | ✅ (time range, env, service) |
| Drill-down | ❌ | ✅ (cell-level navigation) |
| Sharing | Static | Templated, version-controlled |
| Status | Legacy | Recommended |
Workbook structure:
─ Parameters (TimeRange, Service, Severity)
─ Markdown header
─ KQL: requests overview (chart)
─ KQL: top errors (grid → click → drill to next workbook)
─ KQL: dependencies map
Alerts deep
| Type | Source | Latency | Notes |
|---|---|---|---|
| Metric alert | Pre-aggregated metric | ~1 min | Fastest; scalar threshold |
| Log alert (scheduled) | KQL query | 5-15 min | Most flexible; can compute anything |
| Activity Log alert | ARM control plane | seconds | Resource lifecycle events |
| Smart Detection | ML on AppInsights | minutes | Failure anomaly, perf degradation |
Multi-resource log alerts: one rule scans many AppInsights / Log Analytics resources at once — essential for fleet ops.
// Multi-resource alert query
union workspace("ws-prod-*").requests
| where success == false
| summarize fails=count() by _ResourceId
| where fails > 100
Action groups: email, SMS, voice, webhook, Logic App, ITSM, Azure Function, Event Hubs.
Cost management
The two cost drivers:
Knobs:
- Sampling (SDK) — biggest single lever. 25% sampling = 75% cost cut.
- Daily cap — hard ceiling on workspace ingestion. Telemetry over the cap is dropped, not queued. Set with alarms.
- Retention — 30 days included; archive tier is ~10× cheaper for long-term.
- Basic Logs / Auxiliary Logs — cheaper ingestion for noisy tables (with query restrictions).
- Per-table retention — set short retention for chatty
dependencies, long forexceptions.
⚠️ A daily cap protects against runaway cost but creates an observability blackout when hit. Always alert at 80% of cap.
Distributed tracing — W3C Trace Context
Modern default. Two HTTP headers:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
││ ────────────trace-id───────────── ─parent-span-id── ││
│└─ trace flags (sampled)
└── version
tracestate: vendor1=value1,vendor2=value2
operation_Id = trace-id. operation_ParentId = parent span. AppInsights joins everything by these.
Legacy Request-Id / Request-Context (|guid.span_) is still understood for backward-compat but deprecated. New SDKs emit W3C only.
// OTel auto-propagates W3C across HttpClient, gRPC, Service Bus, EF, etc.
builder.Services.AddOpenTelemetry().UseAzureMonitor();
Connection string vs instrumentation key
Connection string (modern):
InstrumentationKey=<guid>;
IngestionEndpoint=https://<region>.in.applicationinsights.azure.com/;
LiveEndpoint=https://<region>.livediagnostics.monitor.azure.com/;
ApplicationId=<app-id>
Why CS over IKey-only: - ✅ Regional ingest endpoint (sovereign clouds, data residency). - ✅ Live Metrics endpoint baked in. - ✅ Application ID for app-level auth scenarios. - ✅ Azure roadmap — IKey-only is being phased out.
// Pull from Key Vault / config; never hardcode
.UseAzureMonitor(o => o.ConnectionString = builder.Configuration["AppInsights:ConnectionString"]);
How it works under the hood
┌──── App Process ────┐
│ OTel SDK │
│ - sampling │
│ - context propagat │
│ - exporters │
└──────────┬──────────┘
│ OTLP / AppInsights ingestion API
▼
┌──── Regional Ingest Endpoint ────┐
│ - auth / rate limit │
│ - server-side sampling (opt) │
└──────────┬───────────────────────┘
▼
┌──── Log Analytics Workspace ─────┐
│ Tables: requests / deps / etc │
│ Indexed for KQL │
└──────┬─────────────────┬─────────┘
│ │
▼ ▼
┌── KQL queries ──┐ ┌── Alerts ──┐
│ Workbooks │ │ rules │
│ Dashboards │ │ → Action │
│ Live Metrics ─►│ │ Groups │
└─────────────────┘ └────────────┘
Live Metrics is a separate channel — the SDK opens a long-poll to the Live endpoint and streams un-sampled samples directly. Bypasses Log Analytics ingestion entirely.
Code: correct vs wrong
❌ Wrong: instrumentation key only
Works, but no regional endpoint, no Live Metrics auth.
✅ Correct: full connection string
❌ Wrong: high-cardinality custom dimension
_logger.LogInformation("Request {RequestId} for {UserId}", requestId, userId);
// userId × requestId → millions of unique series in customMetrics if used as a metric tag
✅ Correct: bounded cardinality
❌ Wrong: no sampling at high traffic
Burst from 100 RPS to 10K RPS → 100× ingestion → bill spike → daily cap → blackout.
✅ Correct: adaptive sampling on
❌ Wrong: KQL without time bound on big workspace
Scans everything. Slow + expensive.
✅ Correct: time-bounded
Design patterns for this topic
Pattern 1 — "Workspace-based AI + diagnostic settings"
- Intent: unified KQL across app + Azure resources.
- How: route Cosmos/KV/Storage diag logs into the same workspace.
Pattern 2 — "Adaptive sampling + Live Metrics for incidents"
- Intent: save cost during normal ops; full fidelity during deploys.
- How: adaptive default; watch Live Metrics during rollouts.
Pattern 3 — "Multi-resource log alerts for fleets"
- Intent: one rule for many services.
- How:
union workspace("…").requestsquery.
Pattern 4 — "Workbooks as runbooks"
- Intent: parameterized incident-response notebook.
- How: workbook with TimeRange + Service params; version-controlled JSON.
Pattern 5 — "Profiler + Snapshot for hard-to-repro"
- Intent: prod post-mortems with locals.
- How: enable both; review snapshots in Visual Studio.
Pros & cons / trade-offs
| Aspect | Pros | Cons |
|---|---|---|
| Workspace-based AI | Cross-resource KQL | One-time migration |
| Adaptive sampling | Cost + perf | Non-deterministic drops |
| Ingestion sampling | Most flexible | Already paid egress |
| W3C Trace Context | Vendor-neutral | Some legacy systems still emit Request-Id |
| Profiler/Snapshot | Prod post-mortems | Limited captures/day |
| Workbooks | Rich, parameterized | KQL learning curve |
When to use / when to avoid
- Use workspace-based AI for new apps. Always.
- Use adaptive sampling unless you need 100% fidelity (compliance, low traffic).
- Use Workbooks for any dashboard that has parameters or drill-down.
- Use Profiler in prod for ongoing perf tracking.
- Avoid classic AI (non-workspace) for greenfield.
- Avoid tagging customMetrics with high-cardinality fields.
- Avoid unbounded KQL queries — always include
timestamp > ago(...).
Interview Q&A
Q1. Workspace-based vs classic App Insights? Workspace-based is the modern path: data lands in Log Analytics, enabling cross-resource KQL. Classic is single-resource and being deprecated.
Q2. Sampling layers? Adaptive (SDK, default, target rate, drops at source), fixed-rate (SDK, deterministic), ingestion (server-side, after data lands — most flexible but already paid egress).
Q3. Why connection string over instrumentation key? Regional ingest endpoints, Live Metrics endpoint, sovereign cloud support. IKey-only is legacy.
Q4. W3C Trace Context vs Request-Id? W3C is the modern open standard (traceparent/tracestate). Request-Id/Request-Context is the legacy AppInsights format. New SDKs emit W3C; both are accepted on ingest.
Q5. KQL join across tables? Common: requests | join (exceptions) on operation_Id — all signals for one logical operation correlate by operation_Id.
Q6. summarize vs make-series? summarize aggregates into rows; make-series produces evenly-spaced time-series suitable for render timechart.
Q7. Profiler vs Snapshot Debugger? Profiler = sampled CPU profile in prod. Snapshot = call stack + locals on uncaught exception. Different tools for different questions.
Q8. How to control AppInsights cost? Sampling (biggest), daily cap, retention tier (basic logs / archive), per-table retention.
Q9. Daily cap risk? Telemetry over cap is dropped — observability blackout. Alert at 80% of cap.
Q10. Live Metrics — how is it different from logs? Separate channel; un-sampled; sub-second; bypasses ingestion. Used during deploys/incidents.
Q11. Multi-resource alerts? One log alert can scan many workspaces / AppInsights resources via union workspace(...). Essential at scale.
Q12. Workbooks vs Dashboards? Workbooks: parameterized, drill-down, version-controlled JSON. Dashboards: pinned tiles, no parameters. Use Workbooks.
Gotchas / common mistakes
- ⚠️ Classic (non-workspace) AI — migrate; cross-resource KQL is worth it.
- ⚠️ High-cardinality dimensions — series explosion; metric becomes useless and expensive.
- ⚠️ No daily cap — runaway bill on traffic burst.
- ⚠️ Daily cap with no headroom alert — silent blackout.
- ⚠️ KQL without time bound — scans entire retention; slow + costs query units.
- ⚠️ Live Metrics off — you lose the best deploy-time tool.
- ⚠️ IKey-only connection — no regional endpoint, no Live Metrics auth.
- ⚠️ Sampling rate misunderstood — adaptive ≠ fixed. Don't assume "10%".