Azure Log Analytics Workspace: What Nobody Warns You About
We have stood up probably thirty Log Analytics Workspaces across client environments. The pattern is always the same: someone enables diagnostic settings on every resource, connects Sentinel, and three months later gets a bill that looks like a typo. The workspace itself is fine. The defaults are the problem.
This is what we actually configure when we build a LAW that we would put our name on.
Workspace Topology Before You Touch Anything Else
The first decision is whether you run one workspace or many. Microsoft's default guidance pushes you toward consolidation: one workspace per region, maybe one per environment tier. That is correct maybe sixty percent of the time.
The cases where you split:
- Strict data-residency requirements across tenants or jurisdictions. A single LAW in East US will hold logs from every connected resource regardless of where the resource lives, and that matters for GDPR-scoped workloads.
- Sentinel. If you are running Microsoft Sentinel, it ties to exactly one workspace per region per subscription. We have seen teams try to fednel logs across workspaces with cross-workspace queries and it works, but the latency and the query complexity are real costs. Better to decide upfront.
- Blast radius. A misbehaving data source can ingest hundreds of GB/day if a diagnostic setting is misconfigured. One workspace means that spike affects your entire analytics budget.
The workspace itself is a five-minute Bicep deploy. The topology decision takes longer and matters more.
Data Collection Rules Are the Real Configuration Surface
Before DCRs existed, you configured ingestion by flipping diagnostic settings on each resource and hoping. DCRs give you a typed, auditable pipeline: source, transformation, destination. They are also where you do the most important thing in LAW management, which is dropping columns before they hit billable storage.
A real example. Azure Firewall logs include a ThreatIntel_CF column in AZFWThreatIntel that we rarely query but pay to store. A DCR KQL transform at ingestion time:
source
| where Category == "AzureFirewallNetworkRule"
| project-away ThreatIntel_CF, PolicyName, OperationName
| extend ParsedSourceIP = tostring(split(SourceIp, ":")[0])
That project-away runs before billing. The column never lands in the table. On a busy firewall, dropping three unused columns across millions of rows per day is not trivial savings.
DCRs are defined in ARM/Bicep as Microsoft.Insights/dataCollectionRules. Wire them up with a Microsoft.Insights/dataCollectionRuleAssociations on each source resource. Once you have that pattern in Bicep, replicating it across subscriptions is a ten-minute job.
Commitment Tiers and the Auxiliary Log Trap
Pay-as-you-go pricing on LAW is $2.30/GB ingested at current Azure rates (varies by region; check the Azure pricing calculator). At the 100 GB/day commitment tier you drop to roughly $1.50/GB. The crossover math is straightforward: if you are consistently ingesting more than 35-40 GB/day, a commitment tier pays for itself. We have yet to meet a production Sentinel environment that does not qualify.
The trap is Auxiliary logs, Microsoft's cheap-tier table type for high-volume low-value data. You pay roughly $0.08/GB to ingest but $0.50/GB to query. Fine for archival. Terrible if an analyst runs a broad KQL sweep across 90 days of data three times a day because they forgot which table type they are querying.
Tag your Auxiliary tables. Document them. Add a comment in your runbooks. One team we work with puts a _AUX suffix on every Auxiliary table name so analysts know before they write the query. It sounds obvious. It prevents surprise bills.
The KQL You Actually Need Day One
Workspaces are useless without queries. These are the first three we put into any shared query library:
// Daily ingestion by table, run this weekly to catch runaway sources
Usage
| where TimeGenerated > ago(7d)
| summarize TotalGB = round(sum(Quantity) / 1024, 2) by DataType
| sort by TotalGB desc
| take 20
// Cross-resource sign-in failures (requires Azure AD / Entra logs)
SigninLogs
| where TimeGenerated > ago(1d)
| where ResultType != "0"
| summarize FailureCount = count(), DistinctIPs = dcount(IPAddress) by UserPrincipalName, ResultDescription
| where FailureCount > 10
| sort by FailureCount desc
// Resource health events that preceded an alert within 15 minutes
ResourceHealthEvent
| where TimeGenerated > ago(24h)
| where Properties.currentHealthStatus != "Available"
| join kind=inner (
AlertsManagementResources
| where TimeGenerated > ago(24h)
| project AlertTime = TimeGenerated, ResourceId
) on ResourceId
| where abs(datetime_diff("minute", TimeGenerated, AlertTime)) < 15
The last query is the one that earns us credibility fastest with SRE teams. Correlating resource health events with alert firings tells you whether Azure's own health plane saw the problem before your monitors did.
Retention Tiers Without a Data Lifecycle Plan Are Expensive
Default interactive retention is 30 days. Default total retention (including archive) is 90 days. Most compliance frameworks want 12-24 months minimum.
The right call is a three-tier plan:
- Interactive (hot): 30-90 days depending on query frequency. Pay full ingestion price, query is fast and cheap.
- Archive (warm): 90 days to 2 years. Pay $0.002/GB/month to keep the data. Pay $0.006/GB to restore a chunk for querying.
- Export to Storage Account or ADX (cold): anything beyond 2 years. Blob storage costs are fractions of a cent per GB/month. Querying requires a pipeline but the data is yours indefinitely.
Configure this per table. SecurityEvent and SigninLogs probably want 90-day interactive because analysts query recent incidents constantly. AzureMetrics can drop to archive at 30 days because you are mostly querying trends, not raw rows.
The Bicep for per-table retention:
resource workspaceTables 'Microsoft.OperationalInsights/workspaces/tables@2023-09-01' = {
name: '${workspaceName}/SecurityEvent'
properties: {
retentionInDays: 90
totalRetentionInDays: 730
}
}
One resource block per table. Wrap it in a loop if you are setting the same policy across a dozen tables.
Connecting Sentinel Without Doubling Your Bill
Sentinel charges per GB ingested into its tables, on top of LAW ingestion costs. This is the source of most sticker shock. The key is understanding which log sources route through Sentinel's billable tables versus which go into non-Sentinel LAW tables that you choose to query via Sentinel.
Microsoft 365 Defender data is the specific case to get right. If you enable the M365 Defender connector in Sentinel, raw device events flow into the DeviceEvents, DeviceNetworkEvents, etc. tables, which are Sentinel-billable. But those same tables exist in Microsoft Defender XDR's own Advanced Hunting interface for free. You pay Sentinel to have them in one place alongside your other alerts. That is a legitimate tradeoff for teams running a unified SOC. It is a waste for teams that only care about Azure-native resources.
Our detection engineering practice does this assessment before any Sentinel onboarding. The answer shapes whether you turn on every connector by default or build a selective ingestion policy.
Workspace-Level RBAC and the Audit You Should Run First
LAW RBAC has two modes: workspace-level (classic) and resource-level (table-scoped). Workspace-level means anyone with Log Analytics Reader on the workspace can query all tables. Table-level RBAC lets you restrict which tables a given principal can read.
Run this before you go any further:
az monitor log-analytics workspace table list \
--resource-group <rg> \
--workspace-name <workspace> \
--query "[].{Table:name, RetentionDays:retentionInDays, TotalRetention:totalRetentionInDays, Plan:plan}" \
-o table
Then cross-reference who has Log Analytics Reader or higher against what tables they can reach. We have found contractor accounts with read access to SecurityEvent tables more than once. It is not a breach, but it is not a posture you want.
Switch high-sensitivity tables to table-level RBAC with an explicit deny-except list. The ARM property is properties.schema.tableType and you scope it with Azure RBAC conditions on the workspace resource.
Next Steps
If your LAW costs are climbing without a clear explanation, or you are planning a Sentinel deployment and want the ingestion design done right before you commit, reach out to us at /contact.
