Azure Kusto
Produce clear, performant KQL queries for diagnostics, reporting, operational insights, and incident investigations.
Use This Skill When
- The user asks to write a KQL query for diagnostics or reporting
- The user needs query optimization or performance tuning
- The user is investigating incidents using Azure logs
- The user wants to aggregate telemetry, analyze trends, or build alerts
Context: Query Maturity
Immature: Ad hoc queries, unbounded scans, timeouts
Developing: Filtered queries, basic aggregation
Managed: Optimized execution, good performance, reusable queries → Target
Optimized: Query compilation caching, materialized views, automated insights
Required Inputs
- Data source: Azure Monitor (Log Analytics), Application Insights, Data Explorer?
- Table names: Exact names (AzureActivity, SecurityEvent, CustomEvents)?
- Time range: Last hour? Last 7 days? Specific dates?
- Filters: Specific resources, servers, error codes, user IDs?
- Output needed: Row count? Aggregation? Time series chart? Alert condition?
- Performance constraints: Must complete in 5 seconds? 30 seconds OK?
Decision Tree
What's the primary objective?
├─ Count/summarize → Use summarize operator, aggregate early
├─ Find specific records → Use where filter first, then project
├─ Time-series analysis → Use bin(timestamp), render timechart
├─ Correlation/join → Join multiple tables, but use small left table
└─ Alert condition → Simple condition with threshold
What's the expected data volume?
├─ < 1 million rows → Simple query, no optimization needed
├─ 1-100 million rows → Filter early, use summarize
└─ > 100 million rows → Partition by time, use external tools
How fast does this need to run?
├─ <5 seconds (dashboard) → Aggressive filtering, pre-aggregation
├─ <30 seconds (report) → Standard optimization
└─ <5 minutes (batch analysis) → Can be less optimized
Workflow
Phase 1: Start with Schema Understanding
Know your tables:
// Azure Monitor / Log Analytics tables
AzureActivity // Azure Control Plane events (Create, Delete, Update)
SecurityEvent // Windows Security logs (logins, privilege escalation)
Syslog // Linux syslog entries
CommonSecurityLog // Third-party firewall, IDS logs
StorageBlob // Azure Storage access logs
AppServiceHTTPLogs // App Service HTTP requests
// Application Insights tables
customEvents // App-generated events
pageViews // Web page navigation
requests // HTTP requests (API calls)
dependencies // External service calls (DB, HTTP)
exceptions // Application exceptions
traces // Custom telemetry (logging)
performance // Performance counters (CPU, memory)
Inspect schema:
// See columns and types
SecurityEvent | getschema
// Output
ColumnName ColumnType
TimeGenerated datetime
Computer string
Account string
EventID int
Activity string
LogonType int
Phase 2: Query Construction (Simple → Complex)
Basic query (single table, filter):
// Find all failed logins in last 24 hours
SecurityEvent
| where TimeGenerated > ago(24h)
| where Activity == "Failed"
| project TimeGenerated, Computer, Account, LogonType
Add aggregation:
// Count failed logins by computer
SecurityEvent
| where TimeGenerated > ago(24h)
| where Activity == "Failed"
| summarize FailureCount = count() by Computer
| sort by FailureCount desc
Add time-series:
// Failed logins over time (hourly)
SecurityEvent
| where TimeGenerated > ago(7d)
| where Activity == "Failed"
| summarize FailureCount = count() by bin(TimeGenerated, 1h), Computer
| render timechart
Join multiple tables:
// Correlate failed logins with failed API requests
SecurityEvent
| where Activity == "Failed"
| join (
AppServiceHTTPLogs
| where HttpStatus >= 400
| project TimeGenerated, UserId, HttpStatus
) on $left.Account == $right.UserId
| project TimeGenerated, Account, HttpStatus
Phase 3: Performance Optimization
Filter early (most important):
// ✓ GOOD: Filter first (scans 1000 rows)
SecurityEvent
| where TimeGenerated > ago(1h) // Filters by time (fast)
| where Computer == "server-prod-1" // Filters by resource
| where Activity == "Failed" // Filters by value
| summarize count()
// ✗ BAD: Filter late (scans 10 million rows, then filters)
SecurityEvent
| summarize count() by Activity // Aggregates all rows first
| where Activity == "Failed" // Then filters (slow!)
Project only needed columns:
// ✓ GOOD: Get 3 columns (small result set)
SecurityEvent | project TimeGenerated, Account, Activity | count
// ✗ BAD: Get all columns (large result set)
SecurityEvent | count
Avoid N:N joins:
// ✗ BAD: Cartesian product (10K × 10K = 100M rows)
SecurityEvent
| join AppServiceHTTPLogs on Computer == ServerName // Many matches per row
// ✓ GOOD: One-to-many join (dimension table small)
SecurityEvent
| join kind=leftouter (
Resources // Small reference table
| project ResourceId, Owner
) on Computer == ResourceId
Use summarize instead of distinct:
// ✗ SLOWER: Get all rows, then dedup
SecurityEvent | distinct Account
// ✓ FASTER: Aggregate by key
SecurityEvent | summarize by Account
Phase 4: Common Query Patterns
Failed authentication analysis:
// Find brute-force attacks (5+ failures in 5 min per account)
SecurityEvent
| where TimeGenerated > ago(7d)
| where Activity == "Failed"
| summarize FailureCount = count() by Account, bin(TimeGenerated, 5m)
| where FailureCount >= 5
| project TimeGenerated, Account, FailureCount
| sort by FailureCount desc
API performance analysis:
// Response time percentiles by endpoint
requests
| where TimeGenerated > ago(24h)
| summarize
p50 = percentile(duration, 50),
p95 = percentile(duration, 95),
p99 = percentile(duration, 99),
failureRate = todouble(sum(resultCode >= 400)) / count() * 100
by name
| project
EndPoint = name,
P50_ms = p50,
P95_ms = p95,
P99_ms = p99,
FailureRate_pct = failureRate
| sort by P99_ms desc
Error spike detection:
// Errors today vs. yesterday (baseline comparison)
let today = exceptions
| where TimeGenerated > ago(24h)
| summarize ErrorCount = count() by bin(TimeGenerated, 1h), type;
let yesterday = exceptions
| where TimeGenerated between(ago(48h) .. ago(24h))
| summarize ErrorCount = count() by bin(TimeGenerated, 1h), type;
today
| join yesterday on type
| extend Spike = (ErrorCount < ErrorCount1) ? "Normal" : "SPIKE"
| where Spike == "SPIKE"
Dependency failures (DB, external API):
// Calls to external service failing
dependencies
| where TimeGenerated > ago(1h)
| where type == "HTTP" or type == "SQL"
| summarize
TotalCalls = count(),
FailedCalls = sum(success == false),
P95_ms = percentile(duration, 95)
by target, type
| extend FailureRate_pct = (FailedCalls / TotalCalls) * 100
| where FailureRate_pct > 5 // Alert if >5% failure
Cost analysis (query efficiency):
// Data ingestion volume by table (for cost management)
union withsource=TableName *
| where TimeGenerated > ago(1d)
| summarize bytes = sum(estimate_data_size(*)) by TableName
| extend GB = bytes / (1024 * 1024 * 1024)
| sort by GB desc
| render barchart
Phase 5: Query Testing & Validation
Test with time range:
// Check query on small time window first
SecurityEvent
| where TimeGenerated > ago(1h) // Test 1 hour
| summarize count()
// Expand after validation
| where TimeGenerated > ago(30d) // Now query 30 days
Check for null values:
// Missing or unexpected data
SecurityEvent
| where TimeGenerated > ago(24h)
| where isempty(Account) or isnan(LogonType)
| count
Validate result reasonableness:
Query: Failed logins per hour
Expected: 100-500 per hour on typical day
Got: 50M rows
→ Issue: Likely a join explosion or missing filter
Phase 6: Advanced Patterns
Anomaly detection (using ML):
// Automatic anomaly in request latency
requests
| where TimeGenerated > ago(7d)
| where isnotempty(duration)
| summarize RequestCount = count(), AvgDuration = avg(duration)
by bin(TimeGenerated, 1h), name
| render anomalychart
Materialized views (pre-aggregate for speed):
// Define view once
.create materialized-view HourlyErrors as
SecurityEvent
| where Activity == "Failed"
| summarize FailureCount = count() by bin(TimeGenerated, 1h), Computer
// Query pre-aggregated view (very fast)
HourlyErrors
| where TimeGenerated > ago(7d)
Output Contract
Query Code
- KQL syntax (ready to copy-paste into Azure Portal)
- Documented with comments explaining each step
Query Explanation
- What data is queried?
- What filters are applied?
- What output is returned?
- Expected result size?
Performance Notes
- Query execution time estimate
- Data volume scanned
- Optimization tips if applicable
Optional Variants
- Time range variations (1 day, 7 days, 30 days)
- Alternative filters (by resource, error code, user)
- Alert condition (if/then thresholds)
Guardrails
- State assumptions when schema is unknown: "I'm assuming table X has column Y; please verify."
- Prefer readable composition over dense one-liners: Use clear variable names, line breaks.
- Avoid unbounded scans: Always include time filter (TimeGenerated > ago(X)).
- Test on small time ranges first: Verify query before running on 90-day data.
- Project early: Remove unnecessary columns to reduce result size and latency.
- Filter before join: Minimize left table size to prevent memory issues.
- Monitor for cost: Large data volumes in Log Analytics cost money per GB ingested.
1---2name: azure-kusto3description: Write and optimize Kusto Query Language (KQL) for Azure Data Explorer, Log Analytics, and monitoring workflows. Use this skill when users ask for KQL queries, telemetry analysis, incident investigation, or query optimization.4---56# Azure Kusto78Produce clear, performant KQL queries for diagnostics, reporting, operational insights, and incident investigations.910## Use This Skill When1112- The user asks to write a KQL query for diagnostics or reporting13- The user needs query optimization or performance tuning14- The user is investigating incidents using Azure logs15- The user wants to aggregate telemetry, analyze trends, or build alerts1617## Context: Query Maturity1819**Immature**: Ad hoc queries, unbounded scans, timeouts 20**Developing**: Filtered queries, basic aggregation 21**Managed**: Optimized execution, good performance, reusable queries → **Target** 22**Optimized**: Query compilation caching, materialized views, automated insights2324## Required Inputs2526- **Data source**: Azure Monitor (Log Analytics), Application Insights, Data Explorer?27- **Table names**: Exact names (AzureActivity, SecurityEvent, CustomEvents)?28- **Time range**: Last hour? Last 7 days? Specific dates?29- **Filters**: Specific resources, servers, error codes, user IDs?30- **Output needed**: Row count? Aggregation? Time series chart? Alert condition?31- **Performance constraints**: Must complete in 5 seconds? 30 seconds OK?3233## Decision Tree3435```36What's the primary objective?37├─ Count/summarize → Use summarize operator, aggregate early38├─ Find specific records → Use where filter first, then project39├─ Time-series analysis → Use bin(timestamp), render timechart40├─ Correlation/join → Join multiple tables, but use small left table41└─ Alert condition → Simple condition with threshold4243What's the expected data volume?44├─ < 1 million rows → Simple query, no optimization needed45├─ 1-100 million rows → Filter early, use summarize46└─ > 100 million rows → Partition by time, use external tools4748How fast does this need to run?49├─ <5 seconds (dashboard) → Aggressive filtering, pre-aggregation50├─ <30 seconds (report) → Standard optimization51└─ <5 minutes (batch analysis) → Can be less optimized52```5354## Workflow5556### Phase 1: Start with Schema Understanding57581. **Know your tables**:59 ```kql60 // Azure Monitor / Log Analytics tables61 AzureActivity // Azure Control Plane events (Create, Delete, Update)62 SecurityEvent // Windows Security logs (logins, privilege escalation)63 Syslog // Linux syslog entries64 CommonSecurityLog // Third-party firewall, IDS logs65 StorageBlob // Azure Storage access logs66 AppServiceHTTPLogs // App Service HTTP requests67 68 // Application Insights tables69 customEvents // App-generated events70 pageViews // Web page navigation71 requests // HTTP requests (API calls)72 dependencies // External service calls (DB, HTTP)73 exceptions // Application exceptions74 traces // Custom telemetry (logging)75 performance // Performance counters (CPU, memory)76 ```77782. **Inspect schema**:79 ```kql80 // See columns and types81 SecurityEvent | getschema82 83 // Output84 ColumnName ColumnType85 TimeGenerated datetime86 Computer string87 Account string88 EventID int89 Activity string90 LogonType int91 ```9293### Phase 2: Query Construction (Simple → Complex)94951. **Basic query** (single table, filter):96 ```kql97 // Find all failed logins in last 24 hours98 SecurityEvent99 | where TimeGenerated > ago(24h)100 | where Activity == "Failed"101 | project TimeGenerated, Computer, Account, LogonType102 ```1031042. **Add aggregation**:105 ```kql106 // Count failed logins by computer107 SecurityEvent108 | where TimeGenerated > ago(24h)109 | where Activity == "Failed"110 | summarize FailureCount = count() by Computer111 | sort by FailureCount desc112 ```1131143. **Add time-series**:115 ```kql116 // Failed logins over time (hourly)117 SecurityEvent118 | where TimeGenerated > ago(7d)119 | where Activity == "Failed"120 | summarize FailureCount = count() by bin(TimeGenerated, 1h), Computer121 | render timechart122 ```1231244. **Join multiple tables**:125 ```kql126 // Correlate failed logins with failed API requests127 SecurityEvent128 | where Activity == "Failed"129 | join (130 AppServiceHTTPLogs131 | where HttpStatus >= 400132 | project TimeGenerated, UserId, HttpStatus133 ) on $left.Account == $right.UserId134 | project TimeGenerated, Account, HttpStatus135 ```136137### Phase 3: Performance Optimization1381391. **Filter early** (most important):140 ```kql141 // ✓ GOOD: Filter first (scans 1000 rows)142 SecurityEvent143 | where TimeGenerated > ago(1h) // Filters by time (fast)144 | where Computer == "server-prod-1" // Filters by resource145 | where Activity == "Failed" // Filters by value146 | summarize count()147 148 // ✗ BAD: Filter late (scans 10 million rows, then filters)149 SecurityEvent150 | summarize count() by Activity // Aggregates all rows first151 | where Activity == "Failed" // Then filters (slow!)152 ```1531542. **Project only needed columns**:155 ```kql156 // ✓ GOOD: Get 3 columns (small result set)157 SecurityEvent | project TimeGenerated, Account, Activity | count158 159 // ✗ BAD: Get all columns (large result set)160 SecurityEvent | count161 ```1621633. **Avoid N:N joins**:164 ```kql165 // ✗ BAD: Cartesian product (10K × 10K = 100M rows)166 SecurityEvent167 | join AppServiceHTTPLogs on Computer == ServerName // Many matches per row168 169 // ✓ GOOD: One-to-many join (dimension table small)170 SecurityEvent171 | join kind=leftouter (172 Resources // Small reference table173 | project ResourceId, Owner174 ) on Computer == ResourceId175 ```1761774. **Use summarize instead of distinct**:178 ```kql179 // ✗ SLOWER: Get all rows, then dedup180 SecurityEvent | distinct Account181 182 // ✓ FASTER: Aggregate by key183 SecurityEvent | summarize by Account184 ```185186### Phase 4: Common Query Patterns1871881. **Failed authentication analysis**:189 ```kql190 // Find brute-force attacks (5+ failures in 5 min per account)191 SecurityEvent192 | where TimeGenerated > ago(7d)193 | where Activity == "Failed"194 | summarize FailureCount = count() by Account, bin(TimeGenerated, 5m)195 | where FailureCount >= 5196 | project TimeGenerated, Account, FailureCount197 | sort by FailureCount desc198 ```1992002. **API performance analysis**:201 ```kql202 // Response time percentiles by endpoint203 requests204 | where TimeGenerated > ago(24h)205 | summarize 206 p50 = percentile(duration, 50),207 p95 = percentile(duration, 95),208 p99 = percentile(duration, 99),209 failureRate = todouble(sum(resultCode >= 400)) / count() * 100210 by name211 | project 212 EndPoint = name,213 P50_ms = p50,214 P95_ms = p95,215 P99_ms = p99,216 FailureRate_pct = failureRate217 | sort by P99_ms desc218 ```2192203. **Error spike detection**:221 ```kql222 // Errors today vs. yesterday (baseline comparison)223 let today = exceptions224 | where TimeGenerated > ago(24h)225 | summarize ErrorCount = count() by bin(TimeGenerated, 1h), type;226 let yesterday = exceptions227 | where TimeGenerated between(ago(48h) .. ago(24h))228 | summarize ErrorCount = count() by bin(TimeGenerated, 1h), type;229 today230 | join yesterday on type231 | extend Spike = (ErrorCount < ErrorCount1) ? "Normal" : "SPIKE"232 | where Spike == "SPIKE"233 ```2342354. **Dependency failures (DB, external API)**:236 ```kql237 // Calls to external service failing238 dependencies239 | where TimeGenerated > ago(1h)240 | where type == "HTTP" or type == "SQL"241 | summarize 242 TotalCalls = count(),243 FailedCalls = sum(success == false),244 P95_ms = percentile(duration, 95)245 by target, type246 | extend FailureRate_pct = (FailedCalls / TotalCalls) * 100247 | where FailureRate_pct > 5 // Alert if >5% failure248 ```2492505. **Cost analysis** (query efficiency):251 ```kql252 // Data ingestion volume by table (for cost management)253 union withsource=TableName *254 | where TimeGenerated > ago(1d)255 | summarize bytes = sum(estimate_data_size(*)) by TableName256 | extend GB = bytes / (1024 * 1024 * 1024)257 | sort by GB desc258 | render barchart259 ```260261### Phase 5: Query Testing & Validation2622631. **Test with time range**:264 ```kql265 // Check query on small time window first266 SecurityEvent267 | where TimeGenerated > ago(1h) // Test 1 hour268 | summarize count()269 270 // Expand after validation271 | where TimeGenerated > ago(30d) // Now query 30 days272 ```2732742. **Check for null values**:275 ```kql276 // Missing or unexpected data277 SecurityEvent278 | where TimeGenerated > ago(24h)279 | where isempty(Account) or isnan(LogonType)280 | count281 ```2822833. **Validate result reasonableness**:284 ```285 Query: Failed logins per hour286 Expected: 100-500 per hour on typical day287 Got: 50M rows288 → Issue: Likely a join explosion or missing filter289 ```290291### Phase 6: Advanced Patterns2922931. **Anomaly detection** (using ML):294 ```kql295 // Automatic anomaly in request latency296 requests297 | where TimeGenerated > ago(7d)298 | where isnotempty(duration)299 | summarize RequestCount = count(), AvgDuration = avg(duration) 300 by bin(TimeGenerated, 1h), name301 | render anomalychart302 ```3033042. **Materialized views** (pre-aggregate for speed):305 ```kql306 // Define view once307 .create materialized-view HourlyErrors as308 SecurityEvent309 | where Activity == "Failed"310 | summarize FailureCount = count() by bin(TimeGenerated, 1h), Computer311 312 // Query pre-aggregated view (very fast)313 HourlyErrors314 | where TimeGenerated > ago(7d)315 ```316317## Output Contract3183191. **Query Code**320 - KQL syntax (ready to copy-paste into Azure Portal)321 - Documented with comments explaining each step3223232. **Query Explanation**324 - What data is queried?325 - What filters are applied?326 - What output is returned?327 - Expected result size?3283293. **Performance Notes**330 - Query execution time estimate331 - Data volume scanned332 - Optimization tips if applicable3333344. **Optional Variants**335 - Time range variations (1 day, 7 days, 30 days)336 - Alternative filters (by resource, error code, user)337 - Alert condition (if/then thresholds)338339## Guardrails340341- **State assumptions when schema is unknown**: "I'm assuming table X has column Y; please verify."342- **Prefer readable composition over dense one-liners**: Use clear variable names, line breaks.343- **Avoid unbounded scans**: Always include time filter (TimeGenerated > ago(X)).344- **Test on small time ranges first**: Verify query before running on 90-day data.345- **Project early**: Remove unnecessary columns to reduce result size and latency.346- **Filter before join**: Minimize left table size to prevent memory issues.347- **Monitor for cost**: Large data volumes in Log Analytics cost money per GB ingested.