Business Central SaaS stuurt dus telemetrie naar Azure Application Insights; met KQL (Kusto Query Language) haal je daar antwoorden uit over prestaties, fouten en gebruik. Deze handleiding KQL BC SaaS begint dus bij een query van één regel en eindigt bij dashboards en alerts.
Wat je nodig hebt
- Een Application Insights-resource in Azure, met de connection string ingesteld op de omgeving (BC admin center → omgeving → Telemetry) of in
app.jsonvan je extensie. - Leesrechten op die resource (rol Reader of Monitoring Reader).
- Een plek om queries te draaien: Azure Portal → Application Insights → Logs, of de Power BI-app Dynamics 365 Business Central Usage / Error / Performance en de BCTech-workbooks.
Hoe KQL werkt in één zin: je begint met een tabel en stuurt die met het pijp-teken | door een reeks stappen, die elk de rijen filteren, kolommen aanpassen of samenvatten.
tabel
| stap 1
| stap 2
Bijna alle BC-telemetrie staat in de tabel traces; de BC-specifieke velden zitten dus in de JSON-kolom customDimensions. Elk signaal heeft een vaste code in customDimensions.eventId, bijvoorbeeld RT0005 voor een trage SQL-query.
Niveau 1 — Je eerste queries
Op dit niveau in de handleiding KQL BC SaaS leer je drie dingen: rijen bekijken, filteren op tijd en filteren op eventId.
1.1 Gewoon kijken wat er is
traces
| take 10
take 10 geeft tien willekeurige rijen. Klap een rij open en bekijk customDimensions: daar zie je velden als eventId, environmentName, companyName en extensionName.
1.2 Altijd eerst op tijd filteren
traces
| where timestamp > ago(1d)
| take 50
ago(1d) betekent “de laatste 24 uur”. Andere eenheden: 30m, 2h, 7d. Filter altijd eerst op tijd: dat maakt de query sneller en goedkoper. In de portal kun je ook de tijdkiezer bovenaan gebruiken.
1.3 Filteren op één soort signaal
traces
| where timestamp > ago(1d)
| where customDimensions.eventId == "RT0030"
| take 50
Dit toont foutmeldingen die gebruikers dus op hun scherm kregen. Vervang RT0030 door een andere code uit de spiekbrief onderaan.
1.4 Filteren op omgeving of bedrijf
traces
| where timestamp > ago(7d)
| where customDimensions.environmentName == "Production"
| where customDimensions.companyName == "CRONUS Nederland BV"
| where customDimensions.eventId == "RT0030"
| take 100
1.5 Zoeken op een woord
traces
| where timestamp > ago(1d)
| where message has "Sales Invoice"
| take 50
==is exact en hoofdlettergevoelig;=~is exact zonder hoofdlettergevoeligheid.haszoekt op een heel woord en is snel;containszoekt op elk stukje tekst en is trager.in ("RT0005", "RT0012")filtert op meerdere waarden tegelijk.
Oefening: toon de laatste 20 foutdialogen (RT0030) in je productieomgeving van gisteren.
Niveau 2 — Kolommen kiezen en customDimensions uitpakken
Op dit niveau in de handleiding KQL BC SaaS maak je dus van de JSON-brij een leesbare tabel met alleen de kolommen die je nodig hebt.
2.1 Kolommen kiezen met project
traces
| where timestamp > ago(1d)
| where customDimensions.eventId == "RT0030"
| project timestamp,
bedrijf = tostring(customDimensions.companyName),
gebruiker = user_Id,
object = tostring(customDimensions.alObjectName),
fout = tostring(customDimensions.alErrorMessage)
Velden in customDimensions zijn van het type dynamic. Zet ze met tostring(), toint(), todouble() of totimespan() om naar een vast type; anders werken sorteren, groeperen en rekenen niet betrouwbaar.
2.2 Een kolom toevoegen met extend
traces
| where timestamp > ago(1d)
| where customDimensions.eventId == "RT0005"
| extend duurMs = toreal(totimespan(customDimensions.executionTime)) / 10000
| extend sql = tostring(customDimensions.sqlStatement)
| project timestamp, duurMs, sql
executionTime is een tijdsduur als tekst (bijvoorbeeld 00:00:02.5310000). totimespan() maakt er een duur van; delen door 10.000 geeft milliseconden.
2.3 Sorteren en de top pakken
traces
| where timestamp > ago(1d)
| where customDimensions.eventId == "RT0005"
| extend duurMs = toreal(totimespan(customDimensions.executionTime)) / 10000
| top 20 by duurMs desc
| project timestamp, duurMs,
object = tostring(customDimensions.alObjectName),
sql = tostring(customDimensions.sqlStatement)
top 20 by … desc is korter dan sort by … desc | take 20 en doet hetzelfde.
2.4 Kolommen weglaten of hernoemen
project-away kolom1, kolom2laat kolommen weg.project-rename nieuweNaam = oudeNaamhernoemt.distinct bedrijfgeeft de unieke waarden van één kolom.
Oefening: toon de 10 traagste SQL-queries van vandaag met objectnaam, extensienaam (extensionName) en duur in seconden.
Niveau 3 — Handleiding KQL Business Central SaaS Tellen, groeperen en grafieken
Met summarize beantwoord je vragen als “hoeveel”, “hoe vaak per dag” en “welke het meest”. Dit gedeelte van de handleiding KQL BC SaaS gaat hier dus dieper op in.
3.1 Tellen per groep
traces
| where timestamp > ago(7d)
| where customDimensions.eventId == "RT0030"
| summarize aantal = count() by fout = tostring(customDimensions.alErrorMessage)
| order by aantal desc
Dit geeft de meest voorkomende foutmeldingen van de afgelopen week. order by en sort by zijn hetzelfde.
3.2 Meerdere berekeningen tegelijk
traces
| where timestamp > ago(7d)
| where customDimensions.eventId == "RT0006"
| extend duurSec = toreal(totimespan(customDimensions.totalTime)) / 10000000
| summarize aantal = count(),
gemiddeld = avg(duurSec),
maximaal = max(duurSec),
gebruikers = dcount(user_Id)
by rapport = tostring(customDimensions.alObjectName)
| order by gemiddeld desc
RT0006 is een rapport dat is uitgevoerd. dcount() telt unieke waarden, hier het aantal verschillende gebruikers.
3.3 Tellen per tijdvak met bin
traces
| where timestamp > ago(14d)
| where customDimensions.eventId == "RT0030"
| summarize aantal = count() by bin(timestamp, 1d)
| order by timestamp asc
bin(timestamp, 1d) rondt elk tijdstip af op de dag; met 1h krijg je per uur.
3.4 Direct een grafiek tekenen
traces
| where timestamp > ago(14d)
| where customDimensions.eventId in ("RT0030", "RT0012")
| summarize aantal = count() by bin(timestamp, 1d), signaal = tostring(customDimensions.eventId)
| render timechart
Andere vormen: columnchart, barchart, piechart. Elke tweede groepeerkolom wordt een aparte lijn.
3.5 Handige aggregaties
| Functie | Wat het doet |
|---|---|
count() | aantal rijen |
dcount(x) | aantal unieke waarden |
sum(x), avg(x), min(x), max(x) | totaal, gemiddelde, minimum, maximum |
percentile(x, 95) | de waarde waaronder 95% van de metingen valt |
countif(voorwaarde) | alleen rijen tellen die aan een voorwaarde voldoen |
make_set(x) | lijst van unieke waarden in één cel |
arg_max(timestamp, *) | de meest recente rij per groep |
Oefening: maak een staafdiagram van het aantal foutdialogen per bedrijf in de afgelopen 30 dagen.
Niveau 4 — Handleiding KQL Business Central SaaS Prestaties analyseren
Op dit niveau in de handleiding KQL BC SaaS kijk je dus niet meer naar losse rijen, maar naar patronen: welke code is structureel traag, en voor wie.
4.1 Tijdsduren netjes rekenen
In plaats van delen door ticks kun je een duur delen door een eenheid: totimespan(x) / 1s geeft seconden, / 1ms milliseconden. Dat leest dus makkelijker.
4.2 Percentielen in plaats van gemiddelden
Een gemiddelde wordt scheefgetrokken door één uitschieter. Het 95e percentiel zegt: “95% van de gebruikers wacht korter dan dit”.
traces
| where timestamp > ago(7d)
| where customDimensions.eventId == "RT0006"
| extend duurSec = totimespan(customDimensions.totalTime) / 1s
| summarize aantal = count(),
p50 = percentile(duurSec, 50),
p95 = percentile(duurSec, 95),
max = max(duurSec)
by rapport = tostring(customDimensions.alObjectName)
| where aantal >= 10
| order by p95 desc
where aantal >= 10 na de summarize filtert rapporten weg die dus te weinig zijn gedraaid om iets over te zeggen.
4.3 Welke AL-code veroorzaakt trage SQL?
traces
| where timestamp > ago(7d)
| where customDimensions.eventId == "RT0005"
| extend duurMs = totimespan(customDimensions.executionTime) / 1ms,
stack = tostring(customDimensions.alStackTrace),
extensie = tostring(customDimensions.extensionName)
| summarize aantal = count(), totaalSec = sum(duurMs) / 1000, p95Ms = percentile(duurMs, 95)
by extensie, object = tostring(customDimensions.alObjectName)
| order by totaalSec desc
| take 25
Sorteren op totale tijd laat zien waar de meeste wachttijd zit: een query van 2 seconden die 5.000 keer draait weegt zwaarder dan één van 60 seconden.
4.4 Lange AL-methodes (UI blokkeert)
traces
| where timestamp > ago(7d)
| where customDimensions.eventId == "RT0018"
| extend duurSec = totimespan(customDimensions.executionTime) / 1s
| project timestamp, duurSec,
methode = tostring(customDimensions.alMethod),
object = tostring(customDimensions.alObjectName),
sqlCalls = toint(customDimensions.sqlStatements),
stack = tostring(customDimensions.alStackTrace)
| order by duurSec desc
4.5 Database-locks
traces
| where timestamp > ago(7d)
| where customDimensions.eventId == "RT0012"
| summarize aantal = count() by object = tostring(customDimensions.alObjectName),
bin(timestamp, 1h)
| render columnchart
RT0012 is een lock-timeout. Pieken op hetzelfde uur wijzen vaak op een taakwachtrij-item of batchverwerking die tegelijk met gebruikers draait.
4.6 Webservices: wie belt ons, en hoe snel?
traces
| where timestamp > ago(1d)
| where customDimensions.eventId == "RT0008"
| extend duurMs = totimespan(customDimensions.serverExecutionTime) / 1ms,
status = tostring(customDimensions.httpStatusCode),
eindpunt = tostring(customDimensions.endpoint),
wachtrij = totimespan(customDimensions.requestQueueTime) / 1ms
| summarize aantal = count(), p95Ms = percentile(duurMs, 95),
fouten = countif(status startswith "4" or status startswith "5"),
p95WachtrijMs = percentile(wachtrij, 95)
by eindpunt
| order by aantal desc
Een hoge requestQueueTime betekent dat de aanroeper wordt afgeremd (throttling), niet dat jouw code traag is. Uitgaande aanroepen vanuit AL vind je onder RT0019.
Oefening: zoek de vijf rapporten met het hoogste p95 in de afgelopen maand en toon per rapport het aantal gelezen SQL-rijen (sqlRowsRead).
Niveau 5 — Handleiding KQL Business Central SaaS Gevorderd: variabelen, joins en eigen functies
Op dit niveau combineer je signalen met elkaar en maak je herbruikbare bouwstenen.
5.1 Variabelen met let
let periode = 7d;
let omgeving = "Production";
let drempelMs = 1000;
traces
| where timestamp > ago(periode)
| where customDimensions.environmentName == omgeving
| where customDimensions.eventId == "RT0005"
| where totimespan(customDimensions.executionTime) / 1ms > drempelMs
| count
Zet instellingen bovenaan, dan pas je de query aan zonder in de logica te zoeken. Elke let-regel eindigt met een puntkomma.
5.2 Voorwaarden en categorieën met case en iff
traces
| where timestamp > ago(7d)
| where customDimensions.eventId == "RT0006"
| extend duurSec = totimespan(customDimensions.totalTime) / 1s
| extend klasse = case(duurSec < 5, "snel (<5s)",
duurSec < 30, "matig (5–30s)",
"traag (>30s)")
| summarize aantal = count() by klasse
| render piechart
iff(voorwaarde, ja, nee) is de korte versie voor twee uitkomsten.
5.3 Taakwachtrij: gestart versus mislukt met join
let gestart = traces
| where timestamp > ago(7d)
| where customDimensions.eventId == "AL0000E25"
| project startTijd = timestamp,
taakId = tostring(customDimensions.alJobQueueId),
taak = tostring(customDimensions.alJobQueueObjectName);
let mislukt = traces
| where timestamp > ago(7d)
| where customDimensions.eventId == "AL0000HE7"
| project foutTijd = timestamp,
taakId = tostring(customDimensions.alJobQueueId),
fout = tostring(customDimensions.alErrorMessage);
gestart
| join kind=inner mislukt on taakId
| where foutTijd between (startTijd .. startTijd + 4h)
| project startTijd, foutTijd, taak, fout
| order by startTijd desc
AL0000E25 is een gestart taakwachtrij-item, AL0000E26 een geslaagd en AL0000HE7 een mislukt item. Joinsoorten die je het meest gebruikt:
inner— alleen rijen die aan beide kanten bestaan.leftouter— alles van links, met lege velden als rechts niets matcht.leftanti— alleen rijen links die réchts níet voorkomen (“gestart maar nooit afgerond”).
5.4 Wat deed een gebruiker vlak voor een fout?
let foutSessie = traces
| where timestamp > ago(1d)
| where customDimensions.eventId == "RT0030"
| where customDimensions.alErrorMessage has "blocked"
| top 1 by timestamp desc
| project session_Id, foutMoment = timestamp;
traces
| where timestamp > ago(1d)
| join kind=inner foutSessie on session_Id
| where timestamp between (foutMoment - 10m .. foutMoment)
| project timestamp, eventId = tostring(customDimensions.eventId), message
| order by timestamp asc
Dit reconstrueert de laatste 10 minuten van de sessie vóór de fout: welke rapporten, pagina’s en trage queries er langskwamen.
5.5 Een eigen functie maken
let BCDuur = (x: dynamic) { totimespan(x) / 1ms };
let BCEvent = (id: string, periode: timespan) {
traces
| where timestamp > ago(periode)
| where customDimensions.eventId == id
};
BCEvent("RT0005", 1d)
| extend duurMs = BCDuur(customDimensions.executionTime)
| top 10 by duurMs desc
In de portal kun je een query opslaan als functie (Save → Save as function). Daarna roep je BCEvent("RT0030", 7d) aan vanuit elke andere query.
5.6 Lijsten uitklappen met mv-expand
Sommige velden bevatten een lijst. mv-expand maakt van elk element een eigen rij, zodat je er weer op kunt groeperen:
traces
| where timestamp > ago(1d)
| where customDimensions.eventId == "RT0030"
| summarize bedrijven = make_set(tostring(customDimensions.companyName)) by fout = tostring(customDimensions.alErrorMessage)
| mv-expand bedrijf = bedrijven to typeof(string)
| summarize aantalFouten = dcount(fout) by bedrijf
Oefening: vind taakwachtrij-items die de afgelopen 7 dagen zijn gestart maar nooit als geslaagd of mislukt zijn gemeld (tip: leftanti).
Niveau 6 — Handleiding KQL Business Central SaaS Expert: trends, anomalieën, alerts en dashboards
Op dit niveau laat je KQL zelf afwijkingen vinden en zet je queries om in bewaking die je waarschuwt.
6.1 Tijdreeksen met make-series
summarize … by bin() slaat lege uren over; make-series vult ze met 0. Dat is nodig voor trendanalyse.
traces
| where timestamp > ago(30d)
| where customDimensions.eventId == "RT0030"
| make-series aantal = count() default = 0
on timestamp from ago(30d) to now() step 1h
| render timechart
6.2 Automatisch afwijkingen vinden
traces
| where timestamp > ago(30d)
| where customDimensions.eventId == "RT0005"
| make-series aantal = count() default = 0
on timestamp from ago(30d) to now() step 1h
| extend (afwijking, score, verwacht) = series_decompose_anomalies(aantal, 2.5)
| render anomalychart with (anomalycolumns = afwijking)
series_decompose_anomalies houdt rekening met dag- en weekpatronen (maandagochtend is altijd drukker) en markeert dus alleen wat daarbuiten valt. Een lagere drempel dan 2,5 geeft meer meldingen.
6.3 Vergelijken met vorige week
let dezeWeek = traces
| where timestamp > ago(7d)
| where customDimensions.eventId == "RT0006"
| summarize nu = percentile(totimespan(customDimensions.totalTime) / 1s, 95)
by rapport = tostring(customDimensions.alObjectName);
let vorigeWeek = traces
| where timestamp between (ago(14d) .. ago(7d))
| where customDimensions.eventId == "RT0006"
| summarize toen = percentile(totimespan(customDimensions.totalTime) / 1s, 95)
by rapport = tostring(customDimensions.alObjectName);
dezeWeek
| join kind=inner vorigeWeek on rapport
| extend verschilPct = round((nu - toen) / toen * 100, 1)
| where verschilPct > 25
| order by verschilPct desc
Handig dus na een BC-update of een nieuwe versie van je extensie: je ziet direct welke rapporten trager zijn geworden.
6.4 Meerdere omgevingen of klanten tegelijk
union app("appinsights-klant-a").traces, app("appinsights-klant-b").traces
| where timestamp > ago(1d)
| where customDimensions.eventId == "RT0030"
| summarize aantal = count() by tenant = tostring(customDimensions.aadTenantId),
omgeving = tostring(customDimensions.environmentName)
Met app("naam") lees je uit een andere Application Insights-resource waar je rechten op hebt. Gebruik je één resource voor meerdere klanten, groepeer dan op aadTenantId.
6.5 Zware tussenresultaten hergebruiken met materialize
let basis = materialize(
traces
| where timestamp > ago(7d)
| where customDimensions.eventId == "RT0005"
| extend duurMs = totimespan(customDimensions.executionTime) / 1ms,
object = tostring(customDimensions.alObjectName));
basis
| summarize p95 = percentile(duurMs, 95) by object
| join kind=inner (basis | summarize aantal = count() by object) on object
| project object, aantal, p95
| top 10 by p95 desc
materialize voert dus het eerste deel één keer uit en hergebruikt het resultaat, in plaats van het voor elk vervolg opnieuw te draaien.
6.6 Van query naar alert
- Schrijf een query die precies één getal teruggeeft, bijvoorbeeld het aantal mislukte taakwachtrij-items in het afgelopen uur:
traces | where timestamp > ago(1h) | where customDimensions.eventId == "AL0000HE7" | count - Kies in Logs New alert rule.
- Stel de voorwaarde in (bijvoorbeeld “groter dan 0”), de frequentie (elke 15 minuten) en de periode (1 uur).
- Koppel een action group die mailt, een Teams-bericht stuurt of een Power Automate-flow start.
Houd de frequentie redelijk: elke run kost geld en te veel meldingen worden genegeerd.
6.7 Dashboards en workbooks
- Azure Workbooks: combineer meerdere queries met parameters (omgeving, periode) op één pagina. Microsoft publiceert kant-en-klare BC-workbooks in de BCTech-repository op GitHub.
- Power BI: de Microsoft-apps voor BC-telemetrie (Usage, Error, Performance, Administration) werken dus direct op je Application Insights-resource.
- Azure Data Explorer (dataexplorer.azure.com): een snellere editor met betere autocomplete; je kunt je Application Insights-resource daar als cluster toevoegen.
Oefening: maak een alert die afgaat als het aantal foutdialogen (RT0030) in het afgelopen uur een afwijking is volgens series_decompose_anomalies.
Spiekbrief
Veelgebruikte eventIds
| eventId | Signaal | Belangrijke velden in customDimensions |
|---|---|---|
| RT0004 | Inloggen / autorisatie | clientType, result, failureReason |
| RT0005 | Trage SQL-query (>750 ms) | executionTime, sqlStatement, alObjectName, alStackTrace |
| RT0006 | Rapport uitgevoerd | totalTime, sqlRowsRead, alObjectName, result |
| RT0008 | Inkomende webservice-aanroep (API/OData/SOAP) | endpoint, httpStatusCode, serverExecutionTime, requestQueueTime |
| RT0012 | Database-lock time-out | sqlStatement, alObjectName, alStackTrace |
| RT0018 | Lange AL-methode | executionTime, alMethod, sqlStatements, alStackTrace |
| RT0019 | Uitgaande webservice-aanroep vanuit AL | endpoint, httpStatusCode, serverExecutionTime, alStackTrace |
| RT0030 | Foutdialoog getoond aan gebruiker | alErrorMessage, alObjectName, alStackTrace |
| AL0000E25 / E26 / HE7 | Taakwachtrij gestart / geslaagd / mislukt | alJobQueueId, alJobQueueObjectName, alErrorMessage |
| LC0010 e.v. | Extensie-levenscyclus (installeren, bijwerken) | extensionName, extensionVersion, failureReason |
De exacte veldnamen verschillen dus soms per BC-versie: klap één rij open met take 1 en kijk wat er in customDimensions staat.
Tien vuistregels
- Begin elke query met een tijdfilter, zo vroeg mogelijk.
- Filter daarna direct op
eventId; pas danextendenproject. - Zet
customDimensions-velden altijd om mettostring(),toint()oftotimespan(). - Gebruik
hasin plaats vancontainsals het kan. - Kijk dus naar p95 en totale tijd, niet alleen naar het gemiddelde.
- Filter op een minimum aantal metingen (
where aantal >= 10) voordat je conclusies trekt. - Zet instellingen in
let-variabelen dus bovenaan. - Sla goede queries op als functie of in een query pack, zodat je team ze hergebruikt.
- Vergelijk dus na elke update met de week ervoor (6.3).
- Telemetrie bevat geen klantdata uit tabellen, maar wel gebruikers-id’s en objectnamen: deel resultaten met dezelfde zorg als andere logbestanden.
Bronnen
- Available telemetry — Microsoft Learn
- New telemetry events and data (2023 release wave 1) — Microsoft Learn
- Business Central telemetry – March 2023 update — Stefano Demiliani
Tot slot
Wil je alvast meer weten over database deadlock trace telemetry, klik dan hier. Wil je alvast meer weten over database lock timeout trace telemetry, klik dan hier. Tot slot als je vragen hebt voor de Digitale Mels, neem dan contact met mij op via de contact pagina.