
Kql
- 47 installs
- 6 repo stars
- Updated July 22, 2026
- julianobarbosa/claude-code-skills
Query and analyze data using Kusto Query Language.
About
Kusto Query Language authoring, debugging, optimization, translation, and tooling for Azure Monitor, Sentinel, ADX, and Application Insights.
- Log Analytics: No management commands
- Sentinel: Extends Log Analytics with SecurityEvent and SecurityAlert
Kql by the numbers
- 47 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #424 of 911 Databases skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/julianobarbosa/claude-code-skills --skill kqlAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 47 |
|---|---|
| repo stars | ★ 6 |
| Last updated | July 22, 2026 |
| Repository | julianobarbosa/claude-code-skills ↗ |
What it does
Query and analyze data using Kusto Query Language.
Files
KQL Skill
Write, debug, optimize, translate, and automate KQL queries across Azure data platforms.
Workflow Routing
| Workflow | Trigger | File |
|---|---|---|
| WriteQuery | "write a query", "create KQL", "query for", "find events where", "show me" | workflows/WriteQuery.md |
| DebugOptimize | "optimize", "slow query", "fix this KQL", "improve performance", "debug query" | workflows/DebugOptimize.md |
| Translate | "SQL to KQL", "Splunk to KQL", "SPL to KQL", "convert this query" | workflows/Translate.md |
| Tooling | "validate KQL", "run query via CLI", "automate query", "schedule alert", "REST API" | workflows/Tooling.md |
If no specific workflow matches, default to WriteQuery.
Reference Files
Read these as needed — don't load everything upfront:
| Reference | When to Read | File |
|---|---|---|
| Operators & Functions | Writing or reviewing any query | references/operators.md |
| Service Tables | Need to know available tables for a specific service | references/service-tables.md |
| Patterns & Anti-patterns | Optimizing queries or reviewing for best practices | references/patterns.md |
| SQL-to-KQL Map | Translating from SQL | references/sql-to-kql.md |
Sample Queries
The samples/ directory contains production-ready .kql files organized by service. Reference these when writing similar queries — they demonstrate the expected file format and conventions.
Output Format
Every generated query MUST use this .kql file format:
// ============================================================
// Title: <descriptive title>
// Service: <Log Analytics | Sentinel | ADX | App Insights>
// Tables: <comma-separated list of tables used>
// Description: <what this query does and when to use it>
// Parameters: <any variables the user should customize>
// Complexity: <Beginner | Intermediate | Advanced>
// ============================================================
// <the query, with inline comments for non-obvious logic>File Naming
Use kebab-case: failed-sign-ins-by-location.kql, high-cpu-vms-last-24h.kql
Core Principles
1. Always specify the target service — KQL varies across Azure services. A query for Sentinel won't necessarily work in ADX. 2. Time-bound by default — Include TimeGenerated filters (or equivalent) to prevent full-table scans. Default to last 24 hours unless the user specifies otherwise. 3. Performance first — Filter early (where before join/summarize), use has over contains for string matching, avoid * projections on wide tables. 4. Parameterize — Use let statements for values the user will customize (time ranges, thresholds, resource names). 5. Explain the query — Add inline comments for non-trivial logic, especially mv-expand, parse, regex, and complex summarize expressions.
Service-Specific Notes
- Log Analytics: No management commands (
.create,.alter). Tables likeHeartbeat,Perf,Event,Syslog,AzureActivity. - Sentinel: Extends Log Analytics with
SecurityEvent,SecurityAlert,SigninLogs,ThreatIntelligenceIndicator, plus custom analytics rule functions. - ADX: Full KQL engine — supports management commands, materialized views, continuous exports, external tables. Most powerful but queries may not be portable.
- Application Insights: Shares Log Analytics engine. Key tables:
requests,dependencies,exceptions,traces,customEvents,performanceCounters.
Examples
Example 1 — Write Query:
"Write a KQL query to find failed sign-ins from outside the US in the last 7 days"
Routes to:workflows/WriteQuery.md→ targets Sentinel/Log Analytics, usesSigninLogs
Example 2 — Optimize:
"This query takes forever to run, can you make it faster?" (pastes KQL)
Routes to: workflows/DebugOptimize.mdExample 3 — Translate:
"Convert this SQL query to KQL: SELECT * FROM events WHERE severity > 3 GROUP BY source"
Routes to: workflows/Translate.mdExample 4 — Tooling:
"How do I run this query from Azure CLI and export to CSV?"
Routes to: workflows/Tooling.md---
Gotchas
- `ago()` is evaluated at query parse time, not row time —
where TimeGenerated > ago(1h)and| extend Age = now() - TimeGenerateduse differentnow()snapshots by milliseconds. Cache the value in alet now_ = now();if comparison matters. - `contains` is case-insensitive AND non-indexed; `has` is indexed but token-boundary only:
where Message has "error"won't matcherrors(different token). For substring matches usecontains_cs/containsknowing they full-scan. - `summarize` without `by` returns one row, hiding all grouping bugs — if you forgot the
byclause and got 1 row, that's why. Always project at least one dimension during dev. - Cross-cluster `join` is fine; cross-workspace `join` silently truncates to the first workspace's data set if the table name collides. Use
workspace("foo").Tablealiases on both sides. - `extend` is evaluated lazily — a
whereafter it filters BEFORE the extend computes, which is fast but means columns referenced inwheremust already exist. Reorder:wherefirst, thenextend. - `SigninLogs` and `AADSignInEventsBeta` are different tables with different schemas in Sentinel — queries built for one fail on the other with cryptic "column not found" errors. Check
print Tables = "<expected>"againstgetschema. - Sentinel analytics rules cap at 10,000 results per run silently — your hunting query that returned 50K rows interactively will alert on only 10K. Use
| take 10000explicitly to surface the limit during testing.
{
"skill_name": "kql-skill",
"evals": [
{
"id": 1,
"prompt": "I need to find all Azure VMs that had more than 90% memory usage in the past 3 days, and I want to see which ones are consistently running hot versus just spiking. This is for a capacity planning review we're doing next week. Target: Log Analytics.",
"expected_output": "A .kql file targeting the Perf table with the standard comment header, using let statements for the time range and threshold, binned aggregation to distinguish sustained vs spike patterns, sorted by severity.",
"files": []
},
{
"id": 2,
"prompt": "We got this SQL query from the old SIEM team and need to convert it to KQL for Sentinel:\n\nSELECT src_ip, dst_ip, COUNT(*) as conn_count, COUNT(DISTINCT dst_port) as port_count\nFROM firewall_logs\nWHERE timestamp > NOW() - INTERVAL 1 HOUR\nAND action = 'denied'\nGROUP BY src_ip, dst_ip\nHAVING port_count > 20\nORDER BY port_count DESC\nLIMIT 50;\n\nThis is for detecting port scanning activity.",
"expected_output": "A .kql file with idiomatic KQL using CommonSecurityLog or AzureDiagnostics (firewall), proper pipe-based flow (not nested SQL style), let parameters, has/== instead of LIKE, summarize with dcount, and the standard file header noting it's a SQL translation.",
"files": []
},
{
"id": 3,
"prompt": "This Sentinel query is taking forever — can you optimize it?\n\nSecurityEvent\n| where EventID == 4625\n| where tolower(Account) contains \"admin\"\n| join kind=inner SigninLogs on $left.Account == $right.UserPrincipalName\n| summarize count() by Account, Computer\n| where count_ > 5\n| sort by count_ desc",
"expected_output": "An optimized version that: adds TimeGenerated filter, replaces tolower()+contains with has or =~ (case-insensitive), filters both sides before join, projects needed columns, and explains each optimization with before/after comparison.",
"files": []
},
{
"id": 4,
"prompt": "I'm a threat hunter and I need to detect lateral movement in our environment. Specifically, I want to find cases where an account authenticated to more than 5 different machines within a 1-hour window in the last 48 hours — this could indicate pass-the-hash or stolen credential use. We use Sentinel with M365 Defender tables.",
"expected_output": "A .kql file targeting DeviceLogonEvents or SecurityEvent (Event 4624), using session windowing or time bins to group authentications per account per hour, filtering for accounts hitting >5 distinct devices, with the standard header and risk context.",
"files": []
},
{
"id": 5,
"prompt": "Our API response times on the orders service spiked around 3am last night — around 2026-04-01T03:00:00Z. Can you write me a KQL query for Application Insights that shows what dependencies degraded during that window? I want to see if it was the database, an external API, or something else. The app name is 'orders-api'.",
"expected_output": "A .kql file targeting dependencies and requests tables, filtered around the 3am window, grouped by dependency type and target, showing duration percentiles and failure rates, with correlation to the orders-api cloud_RoleName.",
"files": []
},
{
"id": 6,
"prompt": "We're migrating from Splunk and need this correlation search converted to KQL for Sentinel:\n\nindex=auth sourcetype=linux_secure\n| search \"Failed password\"\n| stats count as failures dc(src_ip) as unique_sources values(src_ip) as sources by user\n| where failures > 20 AND unique_sources > 3\n| sort -failures\n| join type=inner user [search index=auth sourcetype=linux_secure \"Accepted password\" | stats count as successes latest(_time) as last_success by user]\n| eval success_after_bruteforce=if(last_success > relative_time(now(), \"-1h\"), \"YES\", \"NO\")\n| table user failures unique_sources sources successes success_after_bruteforce",
"expected_output": "Idiomatic KQL targeting Syslog table, using has instead of search, summarize with dcount/make_set, let statements for subqueries, join for success correlation, and the SPL-to-KQL mapping applied correctly.",
"files": []
},
{
"id": 7,
"prompt": "I need to query Azure reservations consumption data — show me all reservations alongside their actual consumed usage using the ARM/Azure Resource Graph tables. I want to see reservation name, utilization percentage, and which resources are consuming each reservation. Also show me how to run this via Azure CLI.",
"expected_output": "A .kql file targeting Azure Resource Graph (resources or advisorresources) or AzureDiagnostics, plus Azure CLI commands to run the query via az graph query. Should cover reservation utilization metrics and consumed resource mapping.",
"files": []
}
]
}
KQL Operators & Functions Reference
Table of Contents
- Tabular Operators
- Scalar Functions
- Aggregation Functions
- String Operators
- DateTime Functions
- Dynamic / JSON Functions
- Rendering
Tabular Operators
| Operator | Purpose | Example |
|---|---|---|
where | Filter rows | `T \ |
extend | Add computed columns | `T \ |
project | Select/rename columns | `T \ |
project-away | Remove columns | `T \ |
project-reorder | Reorder columns | `T \ |
summarize | Aggregate | `T \ |
sort by / order by | Sort rows | `T \ |
top | Top N rows | `T \ |
take / limit | Sample N rows | `T \ |
join | Combine tables | `T1 \ |
union | Concatenate tables | union Table1, Table2 |
distinct | Unique rows | `T \ |
count | Row count | `T \ |
search | Full-text search | search "error" in (Table1, Table2) |
parse | Extract from string | `T \ |
mv-expand | Expand arrays | `T \ |
mv-apply | Apply per element | `T \ |
evaluate | Plugin invocation | `T \ |
render | Visualize | `T \ |
serialize | Row numbering | `T \ |
lookup | Dimension lookup | `T \ |
as | Name subexpression | `T \ |
fork | Parallel branches | `T \ |
facet | Auto-group | `T \ |
sample | Random sample | `T \ |
sample-distinct | Distinct random sample | `T \ |
getschema | Column metadata | `T \ |
Join Kinds
| Kind | Behavior |
|---|---|
inner | Only matching rows from both |
leftouter | All left + matching right (nulls for non-matches) |
rightouter | All right + matching left |
fullouter | All from both |
leftanti | Left rows with NO match in right |
rightanti | Right rows with NO match in left |
leftsemi | Left rows with a match in right (no right columns) |
rightsemi | Right rows with a match in left |
Scalar Functions
Type Conversion
tostring(),toint(),tolong(),todouble(),toreal(),todecimal()tobool(),todatetime(),totimespan(),toguid()parse_json()/todynamic()— parse JSON string to dynamic
Conditional
iff(condition, ifTrue, ifFalse)— ternaryiif(condition, ifTrue, ifFalse)— alias for iffcase(cond1, val1, cond2, val2, ..., default)— multi-branchcoalesce(a, b, c)— first non-null valuemax_of(a, b),min_of(a, b)— scalar min/max
Null Handling
isempty()/isnotempty()— empty string or nullisnull()/isnotnull()— null checkcoalesce()— first non-null
Aggregation Functions
| Function | Purpose |
|---|---|
count() | Row count |
countif(predicate) | Conditional count |
dcount(column) | Approximate distinct count |
dcountif(column, predicate) | Conditional distinct count |
sum(column) | Sum |
sumif(column, predicate) | Conditional sum |
avg(column) | Average |
avgif(column, predicate) | Conditional average |
min(column), max(column) | Min/max |
percentile(column, N) | Nth percentile |
percentiles(column, N1, N2) | Multiple percentiles |
stdev(column) | Standard deviation |
variance(column) | Variance |
make_list(column) | Collect into array |
make_set(column) | Collect unique into array |
make_bag(column) | Merge dynamic objects |
arg_min(column, *) | Row with min value |
arg_max(column, *) | Row with max value |
take_any(column) | Any value (non-deterministic) |
binary_all_and(), binary_all_or(), binary_all_xor() | Bitwise aggregates |
hll(column) | HyperLogLog sketch |
tdigest(column) | T-Digest sketch |
String Operators
| Operator | Case | Description |
|---|---|---|
== | Sensitive | Exact match |
=~ | Insensitive | Exact match |
!= | Sensitive | Not equal |
!~ | Insensitive | Not equal |
has | Insensitive | Contains whole term |
!has | Insensitive | Doesn't contain term |
has_cs | Sensitive | Contains whole term |
has_any | Insensitive | Contains any term |
has_all | Insensitive | Contains all terms |
contains | Insensitive | Substring match (slow) |
!contains | Insensitive | No substring |
contains_cs | Sensitive | Substring match |
startswith | Insensitive | Prefix |
endswith | Insensitive | Suffix |
matches regex | Sensitive | Regex match |
in | Sensitive | Value in set |
in~ | Insensitive | Value in set |
!in | Sensitive | Value not in set |
between | — | Range (inclusive) |
String Functions
strcat(a, b, ...)— concatenatestrlen(s)— lengthsubstring(s, start, length)— extractsplit(s, delimiter)— split to arrayreplace_string(s, old, new)— replacereplace_regex(s, pattern, rewrite)— regex replaceextract(regex, captureGroup, s)— regex extractextract_all(regex, s)— all regex matchestrim(regex, s)— trim characterstolower(s),toupper(s)— case conversionurl_encode(),url_decode()— URL encodingbase64_encode_tostring(),base64_decode_tostring()— Base64hash_sha256(s)— SHA-256 hash
DateTime Functions
ago(timespan)— relative past (ago(1h),ago(7d))now()— current UTC timedatetime(2024-01-15)— literal datetimebin(datetime, timespan)— round down to bin (bin(TimeGenerated, 1h))startofday(),startofweek(),startofmonth(),startofyear()— period startendofday(),endofweek(),endofmonth(),endofyear()— period enddatetime_diff(unit, dt1, dt2)— difference in unitsdatetime_add(unit, amount, dt)— add to datetimeformat_datetime(dt, format)— format as stringdayofweek(dt)— day of week (timespan)getmonth(dt),getyear(dt)— extract parts
Timespan Literals
1d (day), 1h (hour), 1m (minute), 1s (second), 1ms, 1tick
Dynamic / JSON Functions
parse_json(s)/todynamic(s)— parse JSON stringbag_unpack(dynamic)— expand bag to columns (use withevaluate)bag_keys(dynamic)— get keysbag_has_key(dynamic, key)— check key existsbag_merge(bag1, bag2)— merge bagspack(k1, v1, k2, v2)— create bagpack_all()— all columns into one bagarray_length(arr)— array sizearray_index_of(arr, value)— find indexarray_slice(arr, start, end)— slicearray_concat(arr1, arr2)— concatenatearray_sort_asc(arr),array_sort_desc(arr)— sortset_difference(arr1, arr2)— set operationsset_intersect(arr1, arr2),set_union(arr1, arr2)treepath(dynamic)— all paths in JSON
Rendering
| render timechart // line chart over time
| render barchart // bar chart
| render piechart // pie chart
| render scatterchart // scatter plot
| render areachart // area chart
| render stackedareachart // stacked area
| render columnchart // column chart
| render anomalychart // with anomaly detection
| render ladderchart // ladder/waterfall
| render table // explicit table (default)Rendering properties:
| render timechart with (title="CPU Usage", xtitle="Time", ytitle="Percent")KQL Patterns & Anti-patterns
Table of Contents
- Performance Patterns
- Anti-patterns
- Security Hunting Patterns
- Time-series Patterns
- Data Enrichment Patterns
---
Performance Patterns
1. Filter Early, Filter Hard
// GOOD — time filter + equality first, then expensive operations
SecurityEvent
| where TimeGenerated > ago(1h)
| where EventID == 4625
| where AccountType == "User"
| summarize FailCount=count() by Account, Computer
// BAD — summarize all, then filter
SecurityEvent
| summarize FailCount=count() by Account, Computer, EventID
| where EventID == 46252. Use has Over contains
has uses the term index (O(1) lookup). contains does a substring scan (O(n)).
// GOOD — 10x faster for whole-word matches
Syslog | where SyslogMessage has "error"
// BAD — substring scan, no index
Syslog | where SyslogMessage contains "error"Use contains only when you need true substring matching (e.g., "err" matching "error").
3. Materialize Shared Subqueries
// GOOD — computed once, reused twice
let activeUsers = materialize(
SigninLogs
| where TimeGenerated > ago(1d)
| distinct UserPrincipalName
);
let userCount = activeUsers | count;
let topUsers = activeUsers
| join kind=inner SigninLogs on UserPrincipalName
| summarize count() by UserPrincipalName
| top 10 by count_;4. Efficient Joins
// GOOD — smaller table on the right, specific join kind
LargeTable
| where TimeGenerated > ago(1h)
| join kind=leftsemi SmallLookup on CommonKey
// BAD — no kind specified (defaults to innerunique, which deduplicates left), large table on right
SmallLookup
| join LargeTable on CommonKey5. Use in for Multiple Values
// GOOD
SecurityEvent | where EventID in (4624, 4625, 4634, 4648)
// BAD
SecurityEvent | where EventID == 4624 or EventID == 4625 or EventID == 4634 or EventID == 46486. Limit Columns Early
// GOOD — project before expensive operations
SecurityEvent
| where TimeGenerated > ago(1d)
| project TimeGenerated, Account, Computer, EventID
| summarize count() by Account
// BAD — carries all 50+ columns through the pipeline
SecurityEvent
| where TimeGenerated > ago(1d)
| summarize count() by Account---
Anti-patterns
1. Missing Time Filter
// BAD — scans entire retention (90 days+)
SecurityEvent | where EventID == 4625 | count
// GOOD
SecurityEvent | where TimeGenerated > ago(24h) | where EventID == 4625 | count2. Unnecessary Case Conversion
// BAD — tolower is expensive and unnecessary
SigninLogs | where tolower(UserPrincipalName) == "user@company.com"
// GOOD — =~ is case-insensitive
SigninLogs | where UserPrincipalName =~ "user@company.com"3. search * for Known Tables
// BAD — searches every table in the workspace
search "malware"
// GOOD — target specific tables
union SecurityEvent, SecurityAlert
| where * has "malware"4. distinct + count Instead of dcount
// BAD — materializes all distinct values, then counts
SigninLogs | distinct UserPrincipalName | count
// GOOD — approximate count, much faster
SigninLogs | summarize dcount(UserPrincipalName)5. Regex When Simpler Operators Work
// BAD — regex is expensive
Syslog | where SyslogMessage matches regex "^Failed"
// GOOD — startswith uses the index
Syslog | where SyslogMessage startswith "Failed"6. Joining Without Filtering First
// BAD — joins full tables
SigninLogs | join AuditLogs on CorrelationId
// GOOD — filter both sides first
SigninLogs
| where TimeGenerated > ago(1h)
| where ResultType != 0
| join kind=inner (
AuditLogs
| where TimeGenerated > ago(1h)
| where OperationName has "password"
) on CorrelationId---
Security Hunting Patterns
Anomaly Detection — Rare Events
// Find processes that ran on only 1 machine (potential lateral movement)
let timeRange = 7d;
DeviceProcessEvents
| where Timestamp > ago(timeRange)
| summarize MachineCount=dcount(DeviceName) by FileName
| where MachineCount == 1
| join kind=inner (
DeviceProcessEvents | where Timestamp > ago(timeRange)
) on FileName
| project Timestamp, DeviceName, FileName, ProcessCommandLineBaseline Deviation
// Alert when login count exceeds 3x the 7-day average
let baseline = SigninLogs
| where TimeGenerated between (ago(8d) .. ago(1d))
| summarize AvgLogins=avg(count_) by UserPrincipalName
| summarize AvgLogins=count() by UserPrincipalName;
let recent = SigninLogs
| where TimeGenerated > ago(1d)
| summarize RecentLogins=count() by UserPrincipalName;
recent
| join kind=inner baseline on UserPrincipalName
| where RecentLogins > AvgLogins * 3
| project UserPrincipalName, RecentLogins, AvgLogins, Ratio=round(RecentLogins * 1.0 / AvgLogins, 2)IOC Matching
// Match network events against threat intelligence
let iocs = ThreatIntelligenceIndicator
| where Active == true
| where ExpirationDateTime > now()
| where isnotempty(NetworkIP)
| distinct NetworkIP;
CommonSecurityLog
| where TimeGenerated > ago(1d)
| where DestinationIP in (iocs) or SourceIP in (iocs)
| project TimeGenerated, SourceIP, DestinationIP, DeviceAction, ActivityImpossible Travel
SigninLogs
| where TimeGenerated > ago(1d)
| where ResultType == 0
| project TimeGenerated, UserPrincipalName, Location=tostring(LocationDetails.city),
Lat=todouble(LocationDetails.geoCoordinates.latitude),
Lon=todouble(LocationDetails.geoCoordinates.longitude)
| sort by UserPrincipalName, TimeGenerated asc
| serialize
| extend PrevTime=prev(TimeGenerated), PrevLat=prev(Lat), PrevLon=prev(Lon), PrevUser=prev(UserPrincipalName)
| where UserPrincipalName == PrevUser
| extend TimeDiffHours = datetime_diff('hour', TimeGenerated, PrevTime)
| extend DistanceKm = geo_distance_2points(Lon, Lat, PrevLon, PrevLat) / 1000
| where TimeDiffHours > 0
| extend SpeedKmH = DistanceKm / TimeDiffHours
| where SpeedKmH > 1000 // faster than commercial flight
| project TimeGenerated, UserPrincipalName, Location, DistanceKm=round(DistanceKm, 0), TimeDiffHours, SpeedKmH=round(SpeedKmH, 0)---
Time-series Patterns
Binned Aggregation
Perf
| where TimeGenerated > ago(24h)
| where CounterName == "% Processor Time"
| summarize AvgCPU=avg(CounterValue) by bin(TimeGenerated, 15m), Computer
| render timechartRolling Window
let data = SecurityEvent
| where TimeGenerated > ago(7d)
| summarize EventCount=count() by bin(TimeGenerated, 1h);
data
| order by TimeGenerated asc
| serialize
| extend Rolling4h = row_window_session(TimeGenerated, 4h, 4h)Anomaly Detection with series_decompose_anomalies
let startTime = ago(14d);
let endTime = now();
SecurityEvent
| where TimeGenerated between (startTime .. endTime)
| where EventID == 4625
| make-series FailedLogins=count() on TimeGenerated from startTime to endTime step 1h
| extend (anomalies, score, baseline) = series_decompose_anomalies(FailedLogins, 1.5, -1, 'linefit')
| render anomalychart with (anomalycolumns=anomalies)---
Data Enrichment Patterns
GeoIP Enrichment
SigninLogs
| where TimeGenerated > ago(1d)
| extend City = tostring(LocationDetails.city),
State = tostring(LocationDetails.state),
Country = tostring(LocationDetails.countryOrRegion),
Lat = todouble(LocationDetails.geoCoordinates.latitude),
Lon = todouble(LocationDetails.geoCoordinates.longitude)JSON Parsing
AzureActivity
| where TimeGenerated > ago(1d)
| extend Props = parse_json(Properties)
| extend StatusCode = tostring(Props.statusCode),
Resource = tostring(Props.resource)External Data (ADX only)
let ipRanges = externaldata(CIDR:string, Owner:string, Description:string)
[@"https://raw.githubusercontent.com/org/repo/main/ip-ranges.csv"]
with (format="csv", ignoreFirstRecord=true);
CommonSecurityLog
| where TimeGenerated > ago(1h)
| evaluate ipv4_lookup(ipRanges, SourceIP, CIDR)Azure Service Tables Reference
Table of Contents
---
Log Analytics / Azure Monitor
Infrastructure & Compute
| Table | Description |
|---|---|
Heartbeat | Agent health signals (1-min intervals) — use for VM inventory and connectivity |
Perf | Performance counters (CPU, memory, disk, network) |
InsightsMetrics | VM Insights metrics (newer format, replaces some Perf data) |
VMConnection | Network connection data from VM Insights |
VMComputer | VM inventory and configuration from VM Insights |
VMProcess | Running processes from VM Insights |
Event | Windows Event Log entries |
Syslog | Linux syslog messages |
ConfigurationData | Change Tracking — current state |
ConfigurationChange | Change Tracking — changes detected |
Update | Update Management — available updates |
UpdateSummary | Update Management — compliance summary |
Azure Platform
| Table | Description |
|---|---|
AzureActivity | Azure control plane operations (ARM) |
AzureDiagnostics | Diagnostic logs from Azure resources (legacy) |
AzureMetrics | Platform metrics |
AzureNetworkAnalytics_CL | Network Watcher analytics |
AzureDevOpsAuditing | Azure DevOps audit events |
Containers
| Table | Description |
|---|---|
ContainerInventory | Container metadata |
ContainerLog | Container stdout/stderr (legacy) |
ContainerLogV2 | Container logs (new schema) |
KubeEvents | Kubernetes events |
KubeNodeInventory | K8s node inventory |
KubePodInventory | K8s pod inventory |
KubeServices | K8s service inventory |
ContainerInsights | Perf data from Container Insights |
Networking
| Table | Description |
|---|---|
AzureNetworkAnalytics_CL | Traffic analytics |
W3CIISLog | IIS web server logs |
CommonSecurityLog | CEF-format logs (firewalls, proxies) |
Custom & Agent
| Table | Description |
|---|---|
CommonSecurityLog | CEF (Common Event Format) data |
Syslog | Syslog from Linux agents |
CustomLogs_CL | Custom log collection (suffix _CL) |
LAQueryLogs | Query audit log for the workspace |
---
Microsoft Sentinel
Sentinel extends Log Analytics — all tables above are available, plus these security-specific tables:
Identity & Access
| Table | Description |
|---|---|
SigninLogs | Azure AD interactive sign-ins |
AADNonInteractiveUserSignInLogs | Non-interactive sign-ins |
AADServicePrincipalSignInLogs | Service principal sign-ins |
AADManagedIdentitySignInLogs | Managed identity sign-ins |
AADProvisioningLogs | Provisioning events |
AuditLogs | Azure AD audit trail |
IdentityInfo | UEBA identity enrichment |
BehaviorAnalytics | UEBA behavioral analysis |
IdentityDirectoryEvents | Directory changes (MDE) |
IdentityLogonEvents | Logon events (MDE) |
IdentityQueryEvents | Query events (MDE — LDAP, DNS) |
Security Events
| Table | Description |
|---|---|
SecurityEvent | Windows Security Event Log |
SecurityAlert | Alerts from all providers |
SecurityIncident | Sentinel incidents |
SecurityRecommendation | Defender recommendations |
SecurityBaseline | Baseline assessment |
SecurityBaselineSummary | Baseline compliance summary |
Threat Intelligence
| Table | Description |
|---|---|
ThreatIntelligenceIndicator | TI indicators (IOCs) |
HuntingBookmark | Saved hunting bookmarks |
Watchlist | Sentinel watchlists |
Microsoft 365 Defender
| Table | Description |
|---|---|
DeviceEvents | Miscellaneous device events |
DeviceFileEvents | File creation, modification, deletion |
DeviceImageLoadEvents | DLL loading events |
DeviceInfo | Device inventory |
DeviceLogonEvents | Device logons |
DeviceNetworkEvents | Network connections |
DeviceNetworkInfo | Network configuration |
DeviceProcessEvents | Process creation and related events |
DeviceRegistryEvents | Registry modifications |
DeviceFileCertificateInfo | Certificate info from file events |
EmailEvents | Email delivery events |
EmailUrlInfo | URLs in emails |
EmailAttachmentInfo | Email attachment metadata |
CloudAppEvents | Cloud application events (MCAS) |
AlertEvidence | Evidence linked to alerts |
Network Security
| Table | Description |
|---|---|
AzureNetworkAnalytics_CL | NSG flow logs |
DnsEvents | DNS query logs |
CommonSecurityLog | CEF-format (firewalls, WAF, proxies) |
AzureDiagnostics | Azure Firewall logs (Category = AzureFirewallNetworkRule/ApplicationRule) |
---
Application Insights
| Table | Description |
|---|---|
requests | Incoming HTTP requests |
dependencies | Outbound calls (HTTP, SQL, etc.) |
exceptions | Handled and unhandled exceptions |
traces | Log traces (ILogger, TraceSource) |
customEvents | Custom tracked events |
customMetrics | Custom tracked metrics |
pageViews | Browser page view telemetry |
browserTimings | Client-side performance |
availabilityResults | Availability/ping test results |
performanceCounters | Server performance counters |
Key Columns (common across App Insights tables)
timestamp— event time (NOTTimeGenerated)operation_Id— request correlation IDoperation_ParentId— parent operationcloud_RoleName— service/app namecloud_RoleInstance— instance identifierclient_IP— client IP addresscustomDimensions— dynamic bag of custom properties
---
Azure Data Explorer (ADX)
ADX uses custom schemas — tables are defined per database. No predefined table names.
System Tables (available in every ADX database)
| Table / Function | Description |
|---|---|
.show tables | List tables in database |
.show table T schema | Table schema |
.show table T details | Table details (size, extents) |
.show database datastats | Database storage stats |
.show queries | Running/recent queries |
.show journal | Database journal (DDL history) |
Common Patterns for ADX
- IoT/telemetry data often uses:
Timestamp,DeviceId,Value,Metric - Time-series tables commonly have:
Timestamp(notTimeGenerated) - Ingestion tracking:
.show ingestion failures,.show operations
SQL to KQL Translation Reference
Concept Mapping
| SQL | KQL | Notes |
|---|---|---|
SELECT | project | Column selection |
SELECT *, computed | extend | Add column without dropping others |
SELECT DISTINCT | distinct | Unique rows |
FROM table | TableName | Table is the starting point of the pipe |
WHERE | where | Row filtering |
AND / OR | and / or | Boolean operators |
IN (...) | in (...) | Value set membership |
LIKE '%text%' | contains or has | Use has when possible (faster) |
LIKE 'text%' | startswith | Prefix match |
LIKE '%text' | endswith | Suffix match |
GROUP BY | summarize ... by | Aggregation |
HAVING | where (after summarize) | Filter aggregated results |
ORDER BY | sort by or order by | Both work identically |
TOP N / LIMIT N | top N by ... or take N | top sorts, take is random |
JOIN | join kind=inner | Always specify join kind |
LEFT JOIN | join kind=leftouter | Left outer join |
UNION ALL | union | Concatenate tables |
UNION (deduplicated) | `union \ | distinct *` |
INSERT | .ingest | ADX only |
CREATE TABLE | .create table | ADX only |
CASE WHEN | case() or iff() | iff for binary, case for multi-branch |
CAST(x AS type) | tostring(), toint(), etc. | Type-specific functions |
COALESCE | coalesce() | Same name, same behavior |
COUNT(DISTINCT x) | dcount(x) | Approximate by default |
DATE_TRUNC | bin() | bin(TimeGenerated, 1h) |
DATEDIFF | datetime_diff() | datetime_diff('hour', dt1, dt2) |
NOW() | now() | Current UTC time |
INTERVAL | Timespan literal | 1h, 7d, 30m |
SUBSTR | substring() | substring(s, start, length) |
CONCAT | strcat() | strcat(a, b, c) |
LOWER / UPPER | tolower() / toupper() | Usually unnecessary — operators are case-insensitive |
NULL | null | Same concept |
IS NULL | isnull() or isempty() | isempty also catches empty strings |
CTE (WITH) | let | let varName = expr; |
Subquery | let or inline () | Pipe-based, not nested |
PIVOT | pivot() plugin or summarize | evaluate pivot(...) |
UNPIVOT | mv-expand | Expand rows |
EXISTS | join kind=leftsemi | Semi-join checks existence |
NOT EXISTS | join kind=leftanti | Anti-join checks absence |
Translation Examples
Basic SELECT + WHERE + ORDER
SELECT EventID, Account, Computer, TimeGenerated
FROM SecurityEvent
WHERE EventID = 4625 AND TimeGenerated > '2024-01-01'
ORDER BY TimeGenerated DESC
LIMIT 100;SecurityEvent
| where TimeGenerated > datetime(2024-01-01)
| where EventID == 4625
| project EventID, Account, Computer, TimeGenerated
| sort by TimeGenerated desc
| take 100GROUP BY with HAVING
SELECT Account, COUNT(*) as LoginCount
FROM SecurityEvent
WHERE EventID = 4624
GROUP BY Account
HAVING COUNT(*) > 100
ORDER BY LoginCount DESC;SecurityEvent
| where EventID == 4624
| summarize LoginCount=count() by Account
| where LoginCount > 100
| sort by LoginCount descJOIN
SELECT a.UserPrincipalName, a.ResultType, b.OperationName
FROM SigninLogs a
INNER JOIN AuditLogs b ON a.CorrelationId = b.CorrelationId
WHERE a.TimeGenerated > DATEADD(hour, -1, GETUTCDATE());SigninLogs
| where TimeGenerated > ago(1h)
| join kind=inner (AuditLogs | where TimeGenerated > ago(1h)) on CorrelationId
| project UserPrincipalName, ResultType, OperationNameCTE / WITH
WITH FailedLogins AS (
SELECT Account, COUNT(*) as Failures
FROM SecurityEvent
WHERE EventID = 4625
GROUP BY Account
),
SuccessLogins AS (
SELECT Account, COUNT(*) as Successes
FROM SecurityEvent
WHERE EventID = 4624
GROUP BY Account
)
SELECT f.Account, f.Failures, s.Successes
FROM FailedLogins f
JOIN SuccessLogins s ON f.Account = s.Account;let FailedLogins = SecurityEvent
| where EventID == 4625
| summarize Failures=count() by Account;
let SuccessLogins = SecurityEvent
| where EventID == 4624
| summarize Successes=count() by Account;
FailedLogins
| join kind=inner SuccessLogins on Account
| project Account, Failures, SuccessesSubquery with EXISTS
SELECT DISTINCT Account
FROM SecurityEvent s1
WHERE EventID = 4625
AND EXISTS (
SELECT 1 FROM SecurityEvent s2
WHERE s2.Account = s1.Account AND s2.EventID = 4624
);SecurityEvent
| where EventID == 4625
| distinct Account
| join kind=leftsemi (
SecurityEvent | where EventID == 4624 | distinct Account
) on AccountKey Mindset Shifts
1. Pipes, not nesting — KQL flows top-to-bottom through pipes. Where SQL nests subqueries, KQL uses let statements or inline parenthesized expressions. 2. Table first — In SQL, FROM comes after SELECT. In KQL, the table name starts the query and data flows through operators. 3. *No SELECT ; use project — Always explicitly project the columns you need for readability and performance. 4. Time is a first-class citizen — `ago()`, `bin()`, timespan literals, and `TimeGenerated` are central to KQL in a way that datetime handling in SQL isn't. 5. Aggregation syntax** — summarize AggFunc() by GroupCol replaces SELECT AggFunc() ... GROUP BY GroupCol.
// ============================================================
// Title: IoT Telemetry Anomaly Detection
// Service: ADX
// Tables: Telemetry (custom — adjust table/column names)
// Description: Detects anomalous sensor readings using time-series
// decomposition. Identifies devices reporting values that deviate
// significantly from their historical baseline.
// Parameters: timeRange (default 7d), anomalySensitivity (default 1.5)
// Complexity: Advanced
// ============================================================
let timeRange = 7d;
let anomalySensitivity = 1.5; // lower = more sensitive
let stepSize = 1h;
// Step 1: Build time series per device
let deviceSeries = Telemetry
| where Timestamp > ago(timeRange)
| where MetricName == "temperature" // adjust to your metric
| make-series Value=avg(MetricValue) on Timestamp from ago(timeRange) to now() step stepSize
by DeviceId;
// Step 2: Decompose and detect anomalies
deviceSeries
| extend (anomalies, score, baseline) = series_decompose_anomalies(Value, anomalySensitivity, -1, 'linefit')
// Step 3: Extract anomaly details
| mv-apply
Timestamp to typeof(datetime),
Value to typeof(double),
anomalies to typeof(int),
score to typeof(double),
baseline to typeof(double)
on (
where anomalies != 0
| project Timestamp, Value, AnomalyDirection=iff(anomalies > 0, "High", "Low"), Score=round(score, 2), Baseline=round(baseline, 2)
)
| project DeviceId, Timestamp, Value, AnomalyDirection, Score, Baseline,
Deviation = round(Value - Baseline, 2),
DeviationPct = round((Value - Baseline) / Baseline * 100, 1)
| sort by abs(Score) desc
// ============================================================
// Title: Slow Dependency Calls with Impact Analysis
// Service: App Insights
// Tables: dependencies, requests
// Description: Identifies slow outbound dependency calls (HTTP, SQL,
// etc.) and correlates them with the parent requests they slowed
// down. Helps pinpoint external bottlenecks.
// Parameters: timeRange (default 1h), durationThresholdMs (default 2000)
// Complexity: Intermediate
// ============================================================
let timeRange = 1h;
let durationThresholdMs = 2000;
// Step 1: Find slow dependencies
let slowDeps = dependencies
| where timestamp > ago(timeRange)
| where duration > durationThresholdMs
| project
timestamp,
operation_Id,
DependencyName = name,
DependencyType = type,
Target = target,
DurationMs = round(duration, 0),
Success = success,
ResultCode = resultCode;
// Step 2: Correlate with parent requests
slowDeps
| join kind=leftouter (
requests
| where timestamp > ago(timeRange)
| project operation_Id, RequestName = name, RequestDuration = duration, RequestSuccess = success
) on operation_Id
| summarize
SlowCallCount = count(),
AvgDurationMs = round(avg(DurationMs), 0),
P95DurationMs = round(percentile(DurationMs, 95), 0),
MaxDurationMs = round(max(DurationMs), 0),
FailureRate = round(countif(Success == false) * 100.0 / count(), 1),
AffectedRequests = dcount(operation_Id),
ImpactedEndpoints = make_set(RequestName, 5)
by DependencyName, DependencyType, Target
| sort by SlowCallCount desc
| extend Impact = strcat(AffectedRequests, " requests, avg ", AvgDurationMs, "ms, ", FailureRate, "% failures")
// ============================================================
// Title: Kubernetes Container Restart Analysis
// Service: Log Analytics
// Tables: KubePodInventory, ContainerLogV2
// Description: Finds pods with frequent container restarts and
// correlates with recent error logs. Helps identify crash loops,
// OOMKills, and misconfigured health probes.
// Parameters: timeRange (default 6h), restartThreshold (default 3)
// Complexity: Intermediate
// ============================================================
let timeRange = 6h;
let restartThreshold = 3;
// Step 1: Find pods with high restart counts
let restartingPods = KubePodInventory
| where TimeGenerated > ago(timeRange)
| where ContainerRestartCount >= restartThreshold
| summarize
arg_max(TimeGenerated, *),
MaxRestarts = max(ContainerRestartCount)
by ContainerName, PodName, Namespace, ClusterName
| project PodName, Namespace, ClusterName, ContainerName, MaxRestarts, PodStatus;
// Step 2: Get recent error logs from those containers
let errorLogs = ContainerLogV2
| where TimeGenerated > ago(timeRange)
| where LogLevel in ("error", "fatal", "critical")
| where PodName in ((restartingPods | project PodName))
| summarize
ErrorCount = count(),
RecentErrors = make_list(LogMessage, 5)
by PodName, ContainerName;
// Step 3: Join restart info with error context
restartingPods
| join kind=leftouter errorLogs on PodName, ContainerName
| project
ClusterName,
Namespace,
PodName,
ContainerName,
MaxRestarts,
PodStatus,
ErrorCount = coalesce(ErrorCount, 0),
RecentErrors = coalesce(RecentErrors, dynamic([]))
| sort by MaxRestarts desc
// ============================================================
// Title: High CPU Virtual Machines (Last 24 Hours)
// Service: Log Analytics
// Tables: Perf
// Description: Identifies VMs with sustained high CPU utilization
// (>85% average over 15-minute bins). Useful for capacity planning
// and identifying resource-constrained workloads.
// Parameters: timeRange (default 24h), cpuThreshold (default 85)
// Complexity: Beginner
// ============================================================
let timeRange = 24h;
let cpuThreshold = 85;
Perf
| where TimeGenerated > ago(timeRange)
| where ObjectName == "Processor" and CounterName == "% Processor Time"
| where InstanceName == "_Total"
| summarize
AvgCPU = round(avg(CounterValue), 1),
MaxCPU = round(max(CounterValue), 1),
P95CPU = round(percentile(CounterValue, 95), 1)
by Computer, bin(TimeGenerated, 15m)
| where AvgCPU > cpuThreshold
| summarize
SustainedHighCPUBins = count(),
OverallAvgCPU = round(avg(AvgCPU), 1),
PeakCPU = max(MaxCPU)
by Computer
| sort by SustainedHighCPUBins desc
| extend SustainedHours = round(SustainedHighCPUBins * 15.0 / 60, 1)
| project Computer, OverallAvgCPU, PeakCPU, SustainedHours
// ============================================================
// Title: Failed Sign-ins by Geographic Location
// Service: Sentinel
// Tables: SigninLogs
// Description: Identifies failed authentication attempts grouped by
// country and city. Useful for detecting brute-force attacks from
// unexpected locations or geo-anomalous activity.
// Parameters: timeRange (default 24h), failureThreshold (default 10)
// Complexity: Beginner
// ============================================================
let timeRange = 24h;
let failureThreshold = 10;
SigninLogs
| where TimeGenerated > ago(timeRange)
| where ResultType != 0 // non-zero = failure
| extend City = tostring(LocationDetails.city),
Country = tostring(LocationDetails.countryOrRegion),
Lat = todouble(LocationDetails.geoCoordinates.latitude),
Lon = todouble(LocationDetails.geoCoordinates.longitude)
| summarize
FailureCount = count(),
DistinctUsers = dcount(UserPrincipalName),
Users = make_set(UserPrincipalName, 10),
ErrorCodes = make_set(ResultType, 5)
by Country, City, Lat, Lon
| where FailureCount >= failureThreshold
| sort by FailureCount desc
// ============================================================
// Title: Suspicious PowerShell Execution Detection
// Service: Sentinel
// Tables: DeviceProcessEvents
// Description: Detects PowerShell processes with encoded commands,
// download cradles, or obfuscation patterns commonly used in
// attacks. Flags processes matching known malicious patterns.
// Parameters: timeRange (default 24h)
// Complexity: Intermediate
// ============================================================
let timeRange = 24h;
let suspiciousPatterns = dynamic([
"-enc", "-EncodedCommand",
"FromBase64String", "Convert",
"DownloadString", "DownloadFile",
"Invoke-Expression", "IEX",
"Net.WebClient", "Start-BitsTransfer",
"Invoke-WebRequest", "wget", "curl",
"-nop", "-noni", "-w hidden",
"bypass", "-ep bypass"
]);
DeviceProcessEvents
| where Timestamp > ago(timeRange)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| where ProcessCommandLine has_any (suspiciousPatterns)
| extend
HasEncodedCmd = ProcessCommandLine has_any ("-enc", "-EncodedCommand"),
HasDownload = ProcessCommandLine has_any ("DownloadString", "DownloadFile", "Invoke-WebRequest"),
HasBypass = ProcessCommandLine has_any ("bypass", "-ep bypass"),
CmdLength = strlen(ProcessCommandLine)
| extend RiskScore = (toint(HasEncodedCmd) * 3)
+ (toint(HasDownload) * 3)
+ (toint(HasBypass) * 2)
+ iff(CmdLength > 500, 2, 0)
| project
Timestamp,
DeviceName,
AccountName,
ProcessCommandLine = substring(ProcessCommandLine, 0, 500),
HasEncodedCmd,
HasDownload,
HasBypass,
CmdLength,
RiskScore
| sort by RiskScore desc, Timestamp desc
DebugOptimize Workflow
Steps
1. Read the query — Understand what it's trying to do before changing anything. Identify the target service.
2. Check for common issues — Read references/patterns.md for the anti-patterns checklist. Look for:
- Missing time filters (full table scan)
containswherehaswould work (10x slower)*in project (unnecessary column transfer)- Late filtering (WHERE after JOIN or SUMMARIZE)
- Unnecessary
tolower()/toupper()— KQL string operators are case-insensitive by default search *instead of querying specific tables- Cartesian joins (missing
kind=specification) distinct+countinstead ofdcount()
3. Identify the bottleneck — Is it:
- Data volume: needs tighter time/column filters
- Join explosion: wrong join kind or missing key
- Serialization: unnecessary
serializeorprev()/next()on large datasets - Regex overuse: replace
matches regexwithhas/startswith/endswithwhere possible
4. Apply fixes — Make the minimum changes needed. Show before/after with inline comments explaining each change.
5. Suggest query hints if applicable:
hint.strategy=shufflefor large joinshint.num_partitions=Nfor parallel executionhint.materialized=truefor reused subqueriesmaterialize()to avoid recomputation of shared expressions
Performance Rules of Thumb
| Operator | Fast | Slow |
|---|---|---|
| String match | has, has_any, startswith | contains, matches regex |
| Case handling | Default (case-insensitive) | tolower(), toupper() |
| Existence check | isnotempty(), isnotnull() | != "", != null |
| Count distinct | dcount() | `distinct \ |
| Multi-table | union with specific tables | search * |
| Projection | project needed columns | project * or no project |
| Time filter | First where clause | After joins/aggregations |
Tooling Workflow
Automate KQL queries via Azure CLI, REST API, SDKs, and alerting integrations.
Azure CLI
Log Analytics Query
# Run a query against a Log Analytics workspace
az monitor log-analytics query \
--workspace <workspace-id> \
--analytics-query "SecurityEvent | where TimeGenerated > ago(1h) | count" \
--output table
# From a .kql file
az monitor log-analytics query \
--workspace <workspace-id> \
--analytics-query "$(cat my-query.kql)" \
--output json > results.json
# With time range
az monitor log-analytics query \
--workspace <workspace-id> \
--analytics-query "SecurityEvent | count" \
--timespan P7D \
--output tableApplication Insights Query
az monitor app-insights query \
--app <app-id> \
--analytics-query "requests | where timestamp > ago(1h) | summarize count() by resultCode"ADX Query
# Using the Kusto CLI (az kusto is for cluster management, not queries)
# For queries, use the REST API or SDK
az kusto query \
--cluster-name <cluster> \
--database-name <db> \
--query "TableName | take 10"REST API
Log Analytics
POST https://api.loganalytics.io/v1/workspaces/{workspace-id}/query
Authorization: Bearer {token}
Content-Type: application/json
{
"query": "SecurityEvent | where TimeGenerated > ago(1h) | count",
"timespan": "PT1H"
}ADX
POST https://{cluster}.{region}.kusto.windows.net/v1/rest/query
Authorization: Bearer {token}
Content-Type: application/json
{
"db": "database-name",
"csl": "TableName | take 10"
}Python SDK
from azure.monitor.query import LogsQueryClient
from azure.identity import DefaultAzureCredential
from datetime import timedelta
credential = DefaultAzureCredential()
client = LogsQueryClient(credential)
response = client.query_workspace(
workspace_id="<workspace-id>",
query="SecurityEvent | where TimeGenerated > ago(1h) | count",
timespan=timedelta(hours=1)
)
for table in response.tables:
for row in table.rows:
print(row)Query Validation
Before running in production, validate queries:
1. Syntax check — Run with | take 0 appended to verify parsing without returning data 2. Row estimate — Run with | count first to gauge result size 3. Time-bound — Always include a time filter; without one, queries scan the full retention period 4. Column check — Use | getschema to verify expected columns exist in the table
Alert Rule Integration
Create a scheduled alert rule (Log Analytics)
az monitor scheduled-query create \
--name "High Failed Logins" \
--resource-group <rg> \
--scopes "/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.OperationalInsights/workspaces/<ws>" \
--condition "count > 50" \
--condition-query "SigninLogs | where ResultType != 0 | summarize count() by bin(TimeGenerated, 5m)" \
--evaluation-frequency 5m \
--window-size 5m \
--severity 2 \
--action-groups "/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Insights/actionGroups/<ag>"Export Patterns
# JSON export
az monitor log-analytics query --workspace <id> --analytics-query "$(cat query.kql)" -o json > output.json
# CSV export (via jq)
az monitor log-analytics query --workspace <id> --analytics-query "$(cat query.kql)" -o json \
| jq -r '.[0] | (.[0] | keys_unsorted) as $cols | $cols, (.[] | [.[$cols[]]] | @csv)' > output.csv
# PowerShell export
Invoke-AzOperationalInsightsQuery -WorkspaceId <id> -Query (Get-Content query.kql -Raw) |
Select-Object -ExpandProperty Results |
Export-Csv -Path output.csv -NoTypeInformationTranslate Workflow
Converts SQL or Splunk SPL queries to idiomatic KQL.
Steps
1. Identify the source language — SQL or Splunk SPL. Read the query to understand its intent.
2. Map the concepts — Read references/sql-to-kql.md for the SQL mapping table. For SPL, use the mapping below.
3. Identify the target service — Ask which Azure service this will run on. This determines available tables and functions.
4. Translate — Don't do a literal 1:1 translation. Write idiomatic KQL that achieves the same result using KQL's strengths:
- Pipe-based flow instead of nested subqueries
letstatements instead of CTEssummarizeinstead of GROUP BYextendinstead of computed columns in SELECTmv-expandinstead of UNNEST/LATERAL- Native time functions (
ago(),bin()) instead of date arithmetic
5. Output as .kql file with the standard header. Note the original language in the description.
SPL to KQL Quick Reference
| Splunk SPL | KQL Equivalent |
|---|---|
index=main | TableName (specify the table directly) |
search | where |
stats count by field | summarize count() by field |
stats dc(field) | summarize dcount(field) |
eval newfield=if(cond, a, b) | extend newfield=iff(cond, a, b) |
table field1, field2 | project field1, field2 |
sort -count | sort by count desc |
dedup field | summarize take_any(*) by field |
top 10 field | top 10 by field |
timechart span=1h count | `summarize count() by bin(TimeGenerated, 1h) \ |
rex field=raw "(?<name>pattern)" | extend name=extract("pattern", 1, raw) |
lookup | join kind=inner or lookup |
mvexpand field | mv-expand field |
transaction | summarize with make_list() + session windowing |
spath | parse_json() + bag_unpack() |
earliest(_time) | min(TimeGenerated) |
latest(_time) | max(TimeGenerated) |
WriteQuery Workflow
Steps
1. Identify the target service — Ask or infer: Log Analytics, Sentinel, ADX, or Application Insights? This determines available tables and syntax.
2. Identify tables — Read references/service-tables.md if unsure which tables hold the data the user needs. Pick the narrowest table that covers the requirement.
3. Check for similar samples — Look in samples/ for queries targeting the same service/domain. Reuse patterns rather than starting from scratch.
4. Write the query using these KQL best practices:
- Start with
letdeclarations for parameters (time range, thresholds, resource filters) - Filter with
whereas early as possible — push time filters and equality checks first - Use
hasinstead ofcontainsfor whole-token string matching (10x faster) - Use
ininstead of chainedorfor multiple value checks - Project only needed columns with
projectorproject-awayto reduce data transfer - For joins, put the smaller table on the right side
- Add
| take 100or| limit 100during development to preview results
5. Format as a .kql file following the output format in the main SKILL.md — include the comment header block with title, service, tables, description, parameters, and complexity.
6. Explain the query — After the code block, provide a brief walkthrough of what each major step does, especially for intermediate/advanced queries. Mention any gotchas or service-specific behaviors.
Common Query Patterns
Aggregation with time bins
let timeRange = 24h;
TableName
| where TimeGenerated > ago(timeRange)
| summarize Count=count() by bin(TimeGenerated, 1h), ColumnName
| render timechartTop-N analysis
TableName
| where TimeGenerated > ago(7d)
| summarize Count=count() by ColumnName
| top 10 by Count descJoin pattern (small right table)
let lookupTable = materialize(
SmallTable | where Condition | project Key, Value
);
LargeTable
| where TimeGenerated > ago(1d)
| join kind=inner lookupTable on KeyMulti-value expansion
TableName
| mv-expand parse_json(JsonColumn)
| evaluate bag_unpack(JsonColumn)