In the previous lesson you configured the diagnostic settings and everything started flowing into log-contoso-pro. Now there is data: HTTP requests from the bookings website, slow queries from db-reservas, access to kv-contoso-pro, logs from the containers in aks-contoso-operaciones and the activity log from both subscriptions. Storing it is worth nothing if nobody knows how to interrogate it, and that is exactly the point where many teams get stuck: they pay for the ingestion and never exploit the data.
This lesson teaches KQL — the Kusto Query Language — from scratch and step by step, always over real Contoso Airlines data. You will finish by using it to solve the complaint that opened the module: "I have paid and my boarding pass has not arrived". And you will learn to control the bill, because ingestion and retention in Log Analytics are the line item that surprises people most when the following month comes around.
Contents
- What a Log Analytics workspace is
- Workspace design: one or several, and RBAC
- The tables you are going to find
- KQL from scratch: filter, project and extend
- Aggregating:
summarize,bin()and time series - Combining and transforming:
join,union,let,parseandmv-expand - The full investigation of a boarding pass that never arrived
- Saved functions and log search alerts
- Cross-workspace queries and data export
- Table plans, retention and the three cost levers
- Common Mistakes and Tips
- Exercises
- Conclusion
- What a Log Analytics workspace is
A workspace is the container where logs are stored and queried: an Azure resource, in a specific region, with its own RBAC, its own retention and its own bill. Inside there are tables, each with a fixed schema of typed columns, and you query them with KQL, a read-only, pipeline-style language designed for enormous volumes of time series data.
log-contoso-pro lives in rg-contoso-seguridad-pro and is deliberately the single point of convergence. That is where the resource logs from the whole platform arrive, along with Application Insights telemetry (07-03), Container Insights data from AKS, the activity log and the Microsoft Defender for Cloud alerts from module 4. That convergence is what lets you write a single query that cross-references the passenger's HTTP request with the function's exception and with the queue message. If that data lived in four different workspaces, the investigation would be an exercise in copying and pasting between tabs.
- Workspace design: one or several, and RBAC
The first architecture decision is how many workspaces to create:
| Reason to separate | Reason to unify |
|---|---|
| Legal data residency in another region, or isolation demanded by a client | Correlating across services in a single query |
| Costs that must be billed to different subscriptions | A single set of retentions and plans to maintain |
| Legally incompatible retentions | Alerts and workbooks that span the whole platform |
Contoso uses a single production workspace. Before deciding, the argument against was access control: not everybody should see the kv-contoso-pro logs. The answer lies in the two access modes:
- Workspace context: whoever has permission on the workspace sees every table and every resource; that is what the central operations team needs.
- Resource context: the user queries from the resource's blade, or with
resource(), and sees only the logs of the resources they have RBAC on. A developer withReaderonapp-contoso-reservas-devsees those and nothing else, even though they sit in the same workspace.
It is switched on with az monitor log-analytics workspace update --workspace-name log-contoso-pro --resource-group rg-contoso-seguridad-pro --set properties.features.enableLogAccessUsingOnlyResourcePermissions=true. With that option, permission on the resource governs and permission on the workspace stops granting universal access: the Contoso-Desarrollo group gets Reader on rg-contoso-reservas-dev and sees its own; Contoso-Operaciones gets the Log Analytics Reader role on the workspace and sees everything. A single workspace, permissions per resource: exactly the RBAC from module 4 applied to observability.
- The tables you are going to find
| Table | What it holds | Source |
|---|---|---|
AzureActivity |
Control plane operations: who created, changed or deleted | Activity log |
AzureDiagnostics / AppServiceHTTPLogs |
Resource logs in the generic schema or in a dedicated table | Diagnostic setting |
AppRequests |
Requests as seen by the application, with duration and result | Application Insights |
AppTraces / AppExceptions |
Log messages from the code and exceptions with their stack | Application Insights |
AppDependencies |
Outbound calls: SQL, HTTP, queues | Application Insights |
ContainerLogV2 |
Standard output from the AKS containers | Container Insights |
Heartbeat / SecurityAlert |
Each agent's heartbeat and Defender for Cloud alerts | Agent / Defender |
Usage |
How much each table has ingested: the money table | Internal, free |
Two warnings. AzureDiagnostics is a wide table shared by many services, with columns of the field_s, field_d or field_b variety and a column limit that can cause data loss: always prefer the dedicated tables with --export-to-resource-specific true, as you did in 07-01. And the App* names are those of a workspace-based Application Insights resource: the telemetry lives in log-contoso-pro and is queried alongside everything else, which is precisely what makes section 7 possible.
- KQL from scratch: filter, project and extend
A KQL query starts with a table and chains operators with the pipe character |. Each operator receives a table and returns another one.
AppServiceHTTPLogs
| where TimeGenerated > ago(1h)
| where CsHost == "reservas.contosoairlines.example" and ScStatus >= 500
| project TimeGenerated, CsUriStem, ScStatus, TimeTaken, CIp
| order by TimeGenerated desc
| take 20Line by line:
AppServiceHTTPLogs— the source table. Always start from a specific table, never fromsearch.where TimeGenerated > ago(1h)— a time filter; it accepts5m,7d,30d. It always goes first: it is the one that reduces the volume scanned and therefore the cost and the runtime of the query.- The successive
whereclauses filter by site and by status code:==is case-sensitive,=~is not. projectselects columns and discards the rest, reducing what travels to the browser;order by ... descsorts — without it the order is not guaranteed — andtake 20truncates the result.
The search "XR7742" operator looks for a piece of text across all tables: useful once, when you do not know where the data is, and extremely expensive because it scans everything. As soon as you know the table, use it directly. For its part, extend adds computed columns without losing the existing ones:
AppServiceHTTPLogs
| where TimeGenerated between (datetime(2026-08-14 08:00) .. datetime(2026-08-14 12:00))
| extend ResponseSeconds = TimeTaken / 1000.0
| extend Family = case(ScStatus < 400, "ok", ScStatus < 500, "client error", "server error")
| where ResponseSeconds > 2
| project TimeGenerated, CsUriStem, ResponseSeconds, Familybetween (datetime(...) .. datetime(...))bounds an absolute window, so you can reconstruct an incident that is already closed. Other frequent time operators:startofday(now()),startofweek(),now()-2d.extendcreatesResponseSecondsfrom milliseconds; the.0forces decimal division.case(...)evaluates conditions in order and returns the first match; the last value is the "otherwise".
- Aggregating:
summarize, bin() and time series
summarize, bin() and time seriessummarize is the operator that turns millions of rows into an answer.
AppRequests
| where TimeGenerated > ago(24h) and AppRoleName == "app-contoso-api-disponibilidad-pro"
| summarize Requests = count(), Failed = countif(Success == false), AvgMs = avg(DurationMs),
P95Ms = percentile(DurationMs, 95), UniqueUsers = dcount(UserId) by Name
| extend ErrorPercent = round(100.0 * Failed / Requests, 2)
| order by P95Ms desccount()counts rows;countif(condition)counts only the ones that meet something, saving you a second query.avg()gives the average, which lies about latency: a handful of 20-second requests barely move the mean.percentile(DurationMs, 95)is the value below which 95% of requests fall: the honest metric, and the one that underpins the SLO from 07-01.dcount()counts distinct values approximately — a probabilistic algorithm, with an error of around 1% — which makes it blazingly fast over billions of rows;dcount(UserId, 4)raises the accuracy at the expense of resources.by Namegroups by operation: one row per endpoint.
To see the evolution over time you need bin(), which rounds each timestamp down to a multiple of the interval:
AppRequests
| where TimeGenerated > ago(12h) and AppRoleName == "app-contoso-reservas-pro"
| summarize P95 = percentile(DurationMs, 95), Errors = countif(Success == false)
by bin(TimeGenerated, 5m)
| render timechartbin(TimeGenerated, 5m) groups everything that happened in the same 5-minute slot; the result is a time series. render timechart draws it in the portal — barchart, columnchart, piechart and areachart also exist. And top 10 by P95 desc is the shorthand for order by plus take.
- Combining and transforming:
join, union, let, parse and mv-expand
join, union, let, parse and mv-expandlet declares reusable constants and subqueries — let window = 2h; or let apps = dynamic(["app-contoso-reservas-pro", "app-contoso-api-disponibilidad-pro"]);, then usable with where AppRoleName in (apps) — which makes long queries readable and is indispensable in the investigation in section 7. union stacks tables with different schemas — columns missing from one are left empty: union AppExceptions, ContainerLogV2 merges application exceptions and container output into a single timeline. Inside a union, $table returns which table each row came from and coalesce() takes the first non-empty value across several equivalent columns.
join cross-references two sets by a common key. Its types:
| Type | What it returns | Typical use at Contoso |
|---|---|---|
innerunique (default) |
Matches, deduplicating the left-hand key | Rarely the one you want |
inner |
All matching combinations | Requests with their dependencies |
leftouter |
Everything on the left, with the right-hand side if it exists | Bookings with or without a boarding pass issued |
rightouter / fullouter |
The mirror images of the above | Reconciliations |
leftanti |
The left-hand rows that have no match | Bookings with no boarding pass: this module's case |
leftsemi / rightsemi |
Filters without bringing columns from the other side | Checking existence |
The default innerunique is the number one source of baffling results in KQL: it deduplicates silently and the counts come out lower than expected. Always write the type explicitly.
let bookings = AppRequests
| where TimeGenerated > ago(6h) and Name == "POST /api/reservas" and Success == true
| project BookingRef = tostring(Properties["localizador"]), BookingTime = TimeGenerated;
let passes = AppTraces
| where TimeGenerated > ago(6h) and Message startswith "TarjetaEmitida"
| project BookingRef = tostring(Properties["localizador"]);
bookings
| join kind=leftanti passes on BookingRef
| order by BookingTime ascThis query answers the business question with a precision no metric could ever give: which booking references were paid for and have no boarding pass. leftanti keeps exactly those left-hand rows with no match on the right.
Text and structures: parse and mv-expand
Finally, parse extracts fields from a string without writing regular expressions:
ContainerLogV2
| where TimeGenerated > ago(1h) and ContainerName == "motor-disponibilidad"
| parse LogMessage with * "flight=" Flight:string " seats=" Seats:int " ms=" Duration:int *
| where Duration > 1500
| summarize Queries = count(), AvgMs = avg(Duration) by Flight
| top 10 by AvgMs descThe parse pattern reads literally: the leading * ignores whatever comes before, then it looks for the text flight=, captures up to seats= into Flight, and so on. If the message format changes, the capture returns empty instead of failing, so it is worth validating.
Its companion is mv-expand, which turns a field with several values — usually a nested JSON read with todynamic() — into several rows, one per element. A booking with three legs produces three rows, and that is how you can aggregate routes even though the data arrived nested; you will apply it in exercise 3.
- The full investigation of a boarding pass that never arrived
This is the journey that opened the module. The passenger gives their booking reference, XR7742. Five chained steps.
flowchart LR Q["Complaint: XR7742"] --> P1["1. AppRequests<br/>was the purchase recorded?"] --> P2["2. AppDependencies<br/>was it queued?"] P2 --> P3["3. Function<br/>did it run?"] --> P4["4. AppExceptions<br/>which error?"] --> P5["5. Key Vault<br/>root cause"]
Step 1: find the purchase and its operation identifier.
AppRequests
| where TimeGenerated > ago(3d) and Properties["localizador"] == "XR7742"
| project TimeGenerated, Name, Success, DurationMs, OperationId, AppRoleNameOperationId is the key to everything: it identifies the complete business operation and is propagated across components. You will see it in detail in 07-03.
Step 2: follow that operation across every component.
let op = toscalar(AppRequests
| where TimeGenerated > ago(3d) and Properties["localizador"] == "XR7742"
| top 1 by TimeGenerated desc | project OperationId);
union AppRequests, AppDependencies, AppTraces, AppExceptions
| where TimeGenerated > ago(3d) and OperationId == op
| project TimeGenerated, Type = $table, AppRoleName, Detail = coalesce(Name, Message, ExceptionType)
| order by TimeGenerated asctoscalar() reduces a subquery to a single reusable value, and the union sorted by time produces the complete timeline of that booking: the web request, the call to sql-contoso-reservas-pro, the write to cola-emision-tarjetas and whatever happened afterwards.
Step 3: check whether the function ever ran. It is enough to filter AppRequests by AppRoleName == "func-contoso-tarjetas-pro" and Name == "GenerarTarjetaEmbarque" with the same booking reference. If it returns no rows, the message was queued but nobody processed it and the problem is in the trigger; if it returns rows with Success == false, move on to step 4.
Steps 4 and 5: the error and its root cause.
AppExceptions
| where TimeGenerated > ago(3d) and AppRoleName == "func-contoso-tarjetas-pro"
| summarize Occurrences = count(), BookingRefs = make_set(tostring(Properties["localizador"]), 10)
by ExceptionType, OuterMessage
| order by Occurrences descmake_set(column, N) collects up to N distinct values into a list: it reveals at a glance whether the failure affects only XR7742 or another two hundred bookings. If the exception is an access denied, the cause is one step away, in the Key Vault audit logs:
AzureDiagnostics
| where TimeGenerated > ago(3d) and ResourceProvider == "MICROSOFT.KEYVAULT"
| where OperationName == "SecretGet" and ResultSignature != "OK"
| project TimeGenerated, id_s, identity_claim_appid_g, ResultSignatureThe real ending at Contoso: the PDF signing certificate in kv-contoso-pro had expired, the id-contoso-api-pro managed identity was getting an error when retrieving it, the function was failing after five retries and the message ended up in the dead-letter queue. Five queries, from complaint to root cause, without opening a single session on a server.
- Saved functions and log search alerts
That journey should not be reinvented every time. A saved function turns a query into a new operator, with parameters, available to the whole team:
// Body saved in log-contoso-pro under the alias SeguirLocalizador with parameters (loc:string, days:int = 3)
let op = toscalar(AppRequests
| where TimeGenerated > ago(days * 1d) and Properties["localizador"] == loc
| top 1 by TimeGenerated desc | project OperationId);
union AppRequests, AppDependencies, AppTraces, AppExceptions
| where TimeGenerated > ago(days * 1d) and OperationId == op
| project TimeGenerated, Type = $table, AppRoleName, Detail = coalesce(Name, Message, ExceptionType)
| order by TimeGenerated ascWith the function saved under the "Contoso - Operations" category, anybody in the Contoso-Operaciones group types SeguirLocalizador("XR7742") and gets the whole journey. It is the difference between the investigation depending on Marta Ríos and it being done by whoever picks up the call first. These functions and the shared example queries are versioned in the contoso-infra repository, alongside the workbooks and the Bicep templates from module 5.
Log search alerts
Any KQL query that returns rows can be turned into an alert. That is what lets you alert on things no platform metric can express:
az monitor scheduled-query create \
--name alerta-tarjetas-no-emitidas \
--resource-group rg-contoso-seguridad-pro --scopes "$LOG_ID" \
--condition "count 'Rows' > 5" \
--condition-query Rows='AppRequests | where Name == "POST /api/reservas" and Success == true | extend L = tostring(Properties["localizador"]) | join kind=leftanti (AppTraces | where Message startswith "TarjetaEmitida" | extend L = tostring(Properties["localizador"])) on L' \
--evaluation-frequency 15m --window-size 30m --severity 2 \
--action-groups ag-equipo-contoso \
--tags entorno=produccion proyecto=contoso-reservas centro-coste=CC-1042 propietario=marta.riosThree decisions. A --window-size larger than the frequency allows for ingestion latency — data takes one to five minutes to become available, and a tight window generates false negatives. --evaluation-frequency 15m instead of 1m because these alerts are billed per rule and per frequency. And severity 2 with ag-equipo-contoso: five boarding passes not issued is a genuine problem, but it does not justify a text message in the middle of the night.
- Cross-workspace queries and data export
Even though Contoso uses a single workspace, there are two ways to reach outside it. workspace("log-contoso-millas").AppRequests points at another workspace by name or identifier and combines with union to query several at once; resource("/subscriptions/.../sites/app-contoso-reservas-pro") queries a specific resource's logs without naming the workspace, and is the natural way to work in resource context.
The workspace's data export sends whole tables continuously to a storage account or to Event Hubs, without writing code. Contoso exports AzureActivity and AppServiceHTTPLogs to stlagocontosopro, the bronce layer: seven-year compliance at cold storage prices and availability for Synapse (module 3) without paying for the ingestion again. The combination to internalize is that one: Log Analytics for the last 30-90 days, which is when investigation happens; cheap storage for the rest.
- Table plans, retention and the three cost levers
Here is the module's central warning. Ingestion and retention in Log Analytics is one of the bills that surprises people most in Azure. There is no warning at all: you switch on some debug logging during an investigation, you forget to switch it off, and thirty days later the Log Analytics line exceeds the compute line that generates it. Every table has a plan that fixes what you can do with it and how much it costs:
| Plan | Ingestion price | Queries | Alerts | Retention | What for |
|---|---|---|---|---|---|
| Analytics | The full one | Unrestricted KQL | Yes | Interactive, up to 2 years | What you investigate and watch |
| Basic | Heavily reduced | Limited KQL, over a single table | No | Short interactive, 30 days | High-volume, low-value logs |
| Auxiliary | The lowest | Very limited and slower | No | Long | Compliance and auditing at volume |
Retention, in addition, splits into two stretches: interactive (queryable immediately, more expensive) and long-term archive (much cheaper, up to 12 years, from which you have to "rehydrate" or query with search jobs). With that, you can already pull the three levers of cost, in order of impact:
- What gets ingested. The dominant lever; debug and console logs are the big culprits. Transformations in the DCRs let you discard heartbeats, health probes and requests for static resources at ingestion time — they contribute nothing and they are half the volume.
- On which plan. Moving
ContainerLogV2to Basic when it is only used to read the last few days cuts the bill for Contoso's most voluminous table dramatically. - How long it is kept. Retention is set per table, not just per workspace:
AzureActivityat 365 days for compliance,AppTracesat 30.
The Usage table is free and says exactly where the money is going:
Usage
| where TimeGenerated > ago(30d) and IsBillable == true
| summarize GB = round(sum(Quantity) / 1024, 2) by DataType
| extend ApproxEur = round(GB * 2.30, 2) // illustrative price per GB
| top 15 by GB descQuantity comes in megabytes, hence the division. The estimated cost is indicative — the real price depends on the plan and on the commitment — but it is enough to rank by impact. At Contoso, this query revealed that AppServiceConsoleLogs was 41% of the total volume and nobody was querying it. Swapping the summarize for by bin(TimeGenerated, 1d), DataType and rendering it as a columnchart also shows the exact day something took off. Applying the cut:
WS="--workspace-name log-contoso-pro --resource-group rg-contoso-seguridad-pro"
az monitor log-analytics workspace table update $WS --name AppTraces \
--retention-time 30 --total-retention-time 30 # 30 interactive days, no archive
az monitor log-analytics workspace table update $WS --name ContainerLogV2 --plan BasicFinally, the workspace's pricing model: pay-as-you-go per gigabyte versus a commitment tier. From around 100 GB a day, committing to a fixed daily tier applies a notable discount, and from there it grows in steps. Contoso's rule: measure for three months with Usage, cut what nobody queries first, and only then commit capacity on the already optimized volume. Committing capacity on junk data is paying a discount for junk. A daily ingestion cap also acts as a safety net against a runaway logging loop, though you have to accept that once the cap is reached data stops being received until the next day: do not put one on the security tables.
Common Mistakes and Tips
- Not filtering by time up front, or using
searchin production. These are the two mistakes that cost most in performance: always putwhere TimeGenerated > ago(...)as the first operator and start from a specific table, becausesearchscans them all. - Leaving the
joinon its default.inneruniquededuplicates silently and produces incorrect counts. Always writekind=. - Alerting on a window equal to the frequency. Ingestion latency will make you lose events: the window must be larger.
- Confusing the average with a percentile. Average latency hides the long tail, which is what the passenger sees.
- Forgetting debug logging you switched on. The number one source of surprise bills: review
Usageevery month and put it in the calendar. - Tip: use
| take 10while you are developing and remove it at the end; iterating over 10 rows is instantaneous. And prefer the dedicated tables (--export-to-resource-specific), which migrate out ofAzureDiagnosticswith a better schema and per-table plans. - Tip: save as a function any query you have written twice. Investigation knowledge belongs in the workspace, not in somebody's history.
Exercises
Exercise 1. Write a KQL query that, over the last week, returns for each operation on app-contoso-api-disponibilidad-pro the number of requests, the error percentage and the 95th percentile of the duration, showing only operations with more than 1,000 requests and a p95 above 800 ms. Explain every line.
Exercise 2. The monthly bill for log-contoso-pro has gone from 400 to 1,750 euros with nothing new deployed. Describe how to investigate it with KQL and propose four concrete measures, stating which lever each one pulls.
Exercise 3. Nuria Peña wants a weekly report with the five most booked routes and their average revenue, for the Contoso Miles project (centro-coste=CC-2077). The data is in custom events with a nested tramos field. Write the query and explain how you would deliver it on a recurring basis.
Solutions
Solution 1:
AppRequests
| where TimeGenerated > ago(7d) and AppRoleName == "app-contoso-api-disponibilidad-pro"
| summarize Requests = count(), Failed = countif(Success == false),
P95 = percentile(DurationMs, 95) by Name
| extend ErrorPercent = round(100.0 * Failed / Requests, 2)
| where Requests > 1000 and P95 > 800
| order by P95 descThe time filter goes first to reduce the volume scanned. The second where narrows to the API. summarize aggregates per operation with by Name, using countif so a second query is not needed and percentile instead of avg because the average hides the tail. extend computes the percentage with 100.0 to force decimals. The volume and latency filter goes after summarize, because it operates on columns that did not exist before; putting it earlier would raise an error. order by leaves the worst cases at the top.
Solution 2: you investigate with Usage, grouping by DataType and by bin(TimeGenerated, 1d) over 60 days and rendering as columns; the chart shows the exact day of the jump and which table caused it. It is usually one of three things: debug logging switched on during an investigation and forgotten, a new diagnostic setting deployed by the diagnostico-app-service assignment onto freshly created resources, or an exception loop generating thousands of traces per minute. Cross-check with AzureActivity to see what changed on those days. Measures: (1) switch off the diagnostic categories nobody queries — the "what gets ingested" lever, the highest impact one; (2) add a transformation in the DCR that discards health probes and heartbeats — the same lever, at the source; (3) move ContainerLogV2 to the Basic plan — the "which plan" lever; (4) lower the retention on AppTraces to 30 days while keeping AzureActivity at 365 — the "how long" lever. Plus a daily ingestion cap as a safety net, never on the security tables.
Solution 3:
AppEvents
| where TimeGenerated > ago(7d) and Name == "ReservaConfirmada" and tostring(Properties["proyecto"]) == "contoso-millas"
| extend Legs = todynamic(tostring(Properties["tramos"])), Amount = todouble(Properties["importe"])
| mv-expand Leg = Legs
| extend Route = strcat(tostring(Leg.origen), "-", tostring(Leg.destino))
| summarize Bookings = count(), AvgRevenue = round(avg(Amount), 2) by Route
| top 5 by Bookings descmv-expand unfolds each leg into its own row and strcat composes the readable route. Recurring delivery: the query is saved as a function in log-contoso-pro, embedded in a parameterized workbook with the time range as a parameter, and that workbook is sent automatically every Monday. Automating the delivery is solved with an Azure Automation runbook or a Logic App — the comparison between the two options is exactly the subject of lesson 07-04. Tags on the resource involved: entorno, proyecto=contoso-millas, centro-coste=CC-2077 and propietario.
Conclusion
You now know what a Log Analytics workspace is and why log-contoso-pro is the single point where everything converges: resource logs, the activity log, application telemetry and security alerts, with the decisive advantage of being able to cross-reference them in a single query. You know the criteria for separating or unifying workspaces, and the piece that makes the single workspace viable — resource context versus workspace context, with enableLogAccessUsingOnlyResourcePermissions, so each team sees only its own with the RBAC it already has — plus the map of the tables you are going to find, with the recommendation to prefer the dedicated ones over AzureDiagnostics. Above all, you know KQL: filtering with where and the time operators, selecting with project, computing with extend and case, aggregating with summarize using count, countif, avg, percentile and dcount, building time series with bin() and drawing them with render, combining with union, let and the six kinds of join — with the warning about innerunique — and extracting structure with parse and mv-expand. You have applied it to the complete investigation journey that opened the module: from the complaint about booking reference XR7742 to an expired certificate in kv-contoso-pro, in five chained queries, and you have turned it into the shared SeguirLocalizador function so the investigation no longer depends on a single person. You have built a log search alert on top of a query, alerta-tarjetas-no-emitidas, and you know how to export data to stlagocontosopro to keep cheaply what you no longer investigate.
And you carry the money lesson, which is the one most often learned too late: the table plans Analytics, Basic and Auxiliary, interactive retention versus long-term archive, and the three levers — what gets ingested, on which plan and for how long it is kept — with the Usage table as the instrument for knowing where the money is going before touching anything, and a capacity commitment only after having cut. One gap remains. Every query in section 7 leaned on OperationId, on the AppRequests and AppDependencies tables, and on properties such as localizador that somebody had to emit from the code. That data does not appear by magic: it is generated by Application Insights, and that is what the next lesson is about — the telemetry model, instrumenting app-contoso-reservas-pro and func-contoso-tarjetas-pro, distributed correlation with the W3C Trace Context, the application map, sampling, and the custom telemetry that turns a confirmed booking into a queryable piece of data.
Azure Course
Module 1: Introduction to Azure
- What Is Azure?
- Service Models, Regions and Availability Zones
- Creating and Setting Up Your Azure Account
- A Tour of the Azure Portal
- Azure Resource Manager: Subscriptions, Resource Groups and Tags
- Azure CLI, PowerShell and Cloud Shell
Module 2: Core Azure Services
- Azure Virtual Machines
- Compute Scaling and High Availability
- Azure App Service
- Azure Storage: Blobs, Files, Queues and Tables
- Azure Networking: Virtual Networks, Subnets and NSGs
- Hybrid Connectivity and Global Delivery
Module 3: Azure Databases
- Choosing the Right Data Service
- Azure SQL Database
- Azure Cosmos DB
- Azure Database for MySQL
- Azure Database for PostgreSQL
- Data Analytics: Data Lake, Data Factory and Synapse
Module 4: Security in Azure
- Microsoft Entra ID and Identity Management
- RBAC and Managed Identities
- Azure Key Vault
- DDoS Protection and Web Application Firewall
- Microsoft Defender for Cloud
- Governance and Compliance with Azure Policy
Module 5: Azure DevOps
- Introduction to Azure DevOps
- Azure Repos
- Azure Pipelines: Continuous Integration
- Continuous Deployment with Environments and Approvals
- Azure Artifacts
- Infrastructure as Code with Bicep
Module 6: Advanced Azure Services
- Containers in Azure: Container Registry and Container Apps
- Azure Kubernetes Service (AKS)
- Azure Functions
- Azure Logic Apps
- Messaging and Events: Service Bus, Event Grid and Event Hubs
- Azure AI Services
Module 7: Monitoring and Management
- Azure Monitor: Metrics, Alerts and Dashboards
- Log Analytics and KQL Queries
- Application Insights
- Azure Automation and Runbooks
- Backup and Disaster Recovery
Module 8: Cost Management and Optimization
- Pricing Calculator and Cost Estimation
- Azure Cost Management: Analysis, Budgets and Alerts
- Reservations, Savings Plans and Azure Hybrid Benefit
- Azure Advisor
- Optimization Strategies and FinOps Culture
