Handleiding KQL BC SaaS

KQL voor Business Central SaaS
KQL voor Business Central SaaS

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.json van 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.
  • has zoekt op een heel woord en is snel; contains zoekt 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, kolom2 laat kolommen weg.
  • project-rename nieuweNaam = oudeNaam hernoemt.
  • distinct bedrijf geeft 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

FunctieWat 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).

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

  1. 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
  2. Kies in Logs New alert rule.
  3. Stel de voorwaarde in (bijvoorbeeld “groter dan 0”), de frequentie (elke 15 minuten) en de periode (1 uur).
  4. 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

eventIdSignaalBelangrijke velden in customDimensions
RT0004Inloggen / autorisatieclientType, result, failureReason
RT0005Trage SQL-query (>750 ms)executionTime, sqlStatement, alObjectName, alStackTrace
RT0006Rapport uitgevoerdtotalTime, sqlRowsRead, alObjectName, result
RT0008Inkomende webservice-aanroep (API/OData/SOAP)endpoint, httpStatusCode, serverExecutionTime, requestQueueTime
RT0012Database-lock time-outsqlStatement, alObjectName, alStackTrace
RT0018Lange AL-methodeexecutionTime, alMethod, sqlStatements, alStackTrace
RT0019Uitgaande webservice-aanroep vanuit ALendpoint, httpStatusCode, serverExecutionTime, alStackTrace
RT0030Foutdialoog getoond aan gebruikeralErrorMessage, alObjectName, alStackTrace
AL0000E25 / E26 / HE7Taakwachtrij gestart / geslaagd / misluktalJobQueueId, 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

  1. Begin elke query met een tijdfilter, zo vroeg mogelijk.
  2. Filter daarna direct op eventId; pas dan extend en project.
  3. Zet customDimensions-velden altijd om met tostring(), toint() of totimespan().
  4. Gebruik has in plaats van contains als het kan.
  5. Kijk dus naar p95 en totale tijd, niet alleen naar het gemiddelde.
  6. Filter op een minimum aantal metingen (where aantal >= 10) voordat je conclusies trekt.
  7. Zet instellingen in let-variabelen dus bovenaan.
  8. Sla goede queries op als functie of in een query pack, zodat je team ze hergebruikt.
  9. Vergelijk dus na elke update met de week ervoor (6.3).
  10. Telemetrie bevat geen klantdata uit tabellen, maar wel gebruikers-id’s en objectnamen: deel resultaten met dezelfde zorg als andere logbestanden.

Bronnen

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.

0 Shares:
You May Also Like