Every Business Central tenant can stream operational telemetry to Azure Application Insights: slow queries, locks, errors, page load times, job queue runs, extension updates, sign-ins. Almost nobody switches it on. How to, what the signals mean, the five KQL queries to start with, and how to add your own events from AL.
When a customer says "the system was slow yesterday afternoon", there are two kinds of partner. The one that asks the user to reproduce it, and the one that opens Application Insights, filters to yesterday afternoon, and says "it was the aged debtors report run by two people at once, 40 seconds each, locking Cust. Ledger Entry; here is the query". The difference is one setting in the admin centre.
Create an Application Insights resource in Azure (any subscription, pay-as-you-go pricing; a mid-sized tenant costs a few pounds a month). Copy its connection string. In the Business Central admin centre, on the environment, paste it into the Application Insights field. From then on, the platform sends telemetry for that environment: no code, no extension. Repeat per environment, or point them at one resource and filter by environment name. For an extension's own telemetry, the applicationInsightsConnectionString in app.json sends that app's events to the publisher's resource as well.
Each event has an ID (RT0005, AL0000E24 and so on; Microsoft documents them all) and a set of custom dimensions. The ones that matter day to day:
Kusto Query Language, in the Logs blade of the Application Insights resource. Microsoft publishes a full set of sample queries and Power BI reports; these five get a new administrator most of the value.
// 1. Slowest SQL in the last 24 hours, by AL object
traces
| where timestamp > ago(24h) and customDimensions.eventId == "RT0005"
| extend obj = tostring(customDimensions.alObjectName), ms = toint(customDimensions.executionTimeInMs)
| summarize count(), avg(ms), max(ms) by obj
| order by max_ms desc
// 2. Lock timeouts and who was waiting
traces
| where timestamp > ago(7d) and customDimensions.eventId in ("RT0012","RT0013")
| project timestamp, customDimensions.alObjectName, customDimensions.alStackTrace, customDimensions.sqlStatement
// 3. Every error users saw this week, counted
traces
| where timestamp > ago(7d) and customDimensions.eventId == "RT0030"
| summarize n = count() by msg = tostring(customDimensions.failureReason)
| order by n desc
// 4. Job queue failures
traces
| where timestamp > ago(7d) and customDimensions.eventId == "AL0000HE7"
| project timestamp, customDimensions.alJobQueueObjectName, customDimensions.alJobQueueStacktrace
// 5. API calls by endpoint and caller
traces
| where timestamp > ago(1d) and customDimensions.eventId == "RT0008"
| summarize n = count(), avg(toint(customDimensions.serverExecutionTime)) by ep = tostring(customDimensions.endpoint), app = tostring(customDimensions.clientAppId)
| order by n desc
Pin them to a dashboard, add an alert on queries 2 and 4, and you have a monitoring setup most businesses do not have after five years.
var
ShipmentSentEventIdTok: Label 'CON0001', Locked = true;
ShipmentSentMsg: Label 'Courier shipment sent', Locked = true;
ShipmentNoTok: Label 'ShipmentNo', Locked = true;
CarrierTok: Label 'Carrier', Locked = true;
procedure SendShipment(var Shipment: Record "CON Courier Shipment")
var
Dim: Dictionary of [Text, Text];
begin
Dim.Add(ShipmentNoTok, Shipment."No.");
Dim.Add(CarrierTok, Format(Shipment.Carrier));
Session.LogMessage(ShipmentSentEventIdTok, ShipmentSentMsg, Verbosity::Normal,
DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, Dim);
end;
Telemetry text is a Label with Locked = true, as Microsoft's own code does: a label because all text belongs in labels, locked because telemetry must never be translated or the queries stop matching. Give every event an ID with your prefix, a stable message, and dimensions with no personal data (classify accordingly). TelemetryScope::ExtensionPublisher sends to the app.json resource; All also sends to the customer's environment resource. Log the start, end and failure of integrations and long operations, and business-rule failures that are not errors (a shipment rejected by the carrier). Then your extension explains itself in the same place as the platform does. The performance post ends with "look at the telemetry"; this is how.
Application Insights charges per gigabyte ingested; a typical mid-sized tenant sends a few hundred megabytes a month. Set a daily cap and 90 days' retention and the bill is small. Ownership: the person who administers the environment looks at the dashboard weekly and the alerts land in their inbox; the support partner has read access and uses it on every ticket. If your partner is not asking for it, ask why; it is in our list of what to expect.