Skip to content

Health Alert Rules

Sam Betts edited this page Nov 12, 2026 · 8 revisions

Health Alerts → Health Alert Rules

Walk-through: alert for "no recent web-tracker data"

Example: create a rule to check if there's no recent data into App Insights from the web tracker.

Why might this happen? A bad deploy of a new tracker version, for example. It probably won't happen, but it could for many reasons potentially; we need to know if this happens ASAP so we can fix it.

Create a new rule & add a condition:

In Azure Monitor, open Alerts → Create → Alert rule, choose the solution's Application Insights/Log Analytics scope, then use Custom log search for the condition. Set the evaluation window, frequency and threshold for the query below; attach an action group and test delivery. See create a log search alert.

The signal for the condition will be "Custom log search" meaning, from the results of an Application Insights query.

For the query we just want pageViews, nothing else.

For the alert logic, in this case, we want to know when no hits are arriving "for X time". That is obviously going to be enough time that it's clear there's a problem instead of just inactivity, so let's say "48 hours".

If there's been no hits at all in 48 hours on any sites, then the chances are there's something wrong – you may have your own ideas what X may need to be, but in this example, our alert will look like this:

Use Number of results = 0 over your chosen window. For example:

pageViews
| where timestamp > ago(48h)

Choose a window long enough to allow normal weekends/low traffic. Absence of browser traffic is not by itself proof that an importer stopped; pair this with cycle-completion and infrastructure checks.

Save the trigger.

Next up: define an action when the trigger is hit, which we do with "action groups", which is basically a way of configuring a collection of things to happen when a trigger is hit. You pre-save an action group & associate it to an alert.

From an alert, this is now selectable.

Now add the alert details...

...and create. It will now be active.

Add another alert (repeatable procedure)

The walk-through above is one example; every alert in this guide is created the same way. Two shapes cover everything.

One-time — create a default action group. An action group is the "who to tell" (email, SMS, Teams/webhook, Logic App). Create one under Monitor → Alerts → Action groups (for example M365-Analytics-alerts) with an email receiver, then reuse it for every rule so recipients are configured in one place.

Add a log-search (KQL) alert — for the custom-event / exception rules (most of this page):

  1. Open the Application Insights resource → Logs, paste the query and confirm it returns rows.
  2. Click New alert rule at the top of the Logs blade — the query carries over as the condition.
  3. Measurement: aggregate Table rows / Count; set the threshold and the Aggregation granularity + Frequency of evaluation to match the window (e.g. 30 min for liveness, 24 h for a stalled cycle).
  4. For "alert when there are no rows" rules (a stalled import, no heartbeat), use Number of results = 0 with the window as the evaluation period.
  5. Actions → select the default action group. Details → name + severity → Create.

Add a metric alert — for capacity rules (SQL DTU, App Service CPU, Service Bus dead-letter):

  1. Open the resource (SQL DB / App Service Plan / Service Bus) → Alerts → New alert rule.
  2. Condition → pick the platform metric (e.g. DTU percentage, CPU Percentage, Dead-lettered messages), set operator / threshold / window.
  3. Actions → default action group. Name + severity → Create.

Write the threshold you chose next to each rule (or in the rule description) so whoever inherits the system can tune it.

Alert Rules

To create alerts for the other rules, here are the triggers you need to create. Except where noted, all are based on the "Custom log search" signal in Application Insights and fire when the result count crosses the threshold shown.

Data-flow alerts

These alerts cover enabled imports. A cycle's pause defaults to 10 minutes (20 at Gentle), but the work before it can take much longer; “10-minute pause” is not a promise of a completed cycle every ten minutes. Disable workload-specific rules when that import is intentionally off.

  • Microsoft 365 importer cycle has stalled (no completed cycle)
App Insights Query Time Period Trigger count
customEvents | where name == "FinishedImportCycle" and operation_Name == "Office365ActivityImporter" 24 hours (1440 minutes) 0

The Microsoft 365 importer (the Office365ActivityImporter WebJob) sends one FinishedImportCycle event at the end of every import cycle, whichever imports are switched on. A missing one for 24 hours means the importer has died, is crash-looping, or is stuck in one very long cycle. It shows that the import loop is turning, not that every source succeeded: a stage that fails, such as the Graph import or the audit import, is logged and the cycle still finishes, and the once-a-day usage reports run in the background and finish after the cycle event. To check each source, use the section and stage events in Monitoring.

Don't use the Finished activity import. Time taken in = <N> minutes. Stats: ... trace for this. Only the audit-log (Office 365 Management Activity API) stage writes it, and only when an audit-based import (ActivityLog, Copilot, ImportPowerPlatform or ImportDlp) is switched on, so on a deployment with only Graph imports the rule would fire all the time.

  • No audit events received from the Activity API
App Insights Query Time Period Trigger count
traces | where operation_Name == "Office365ActivityImporter" and message startswith "Audit events import: Got" 6 hours (360 minutes) 0

This trace is emitted by ActivityImporter once per cycle and includes the number of audit events returned from the Office 365 Management Activity API ("Audit events import: Got {N} audit events from {M} summary reports..."). A long gap means either the Activity API is failing for this tenant (auth/permissions/tenant-side outage) or the importer is no longer reaching the audit phase — both warrant immediate investigation. The 6-hour window is short enough to catch a real outage but long enough to ride out a slow import or transient Graph 5xx.

  • User metadata import has stopped completing
App Insights Query Time Period Trigger count
traces | where operation_Name == "Office365ActivityImporter" and message startswith "User import - complete" 24 hours (1440 minutes) at High, the default; 30 hours at Balanced or Gentle 0

This trace is the final line of UserMetadataUpdater.RunUpdate ("User import - complete. Inserted {N} new users, updated metadata for {N} existing users..."). If it doesn't appear in the window, the Graph user import is either disabled, throwing repeatedly, or running but never finishing (e.g. throttled by Graph). At Balanced and Gentle the user import runs only once every 24 hours, on the first cycle after that, so a 24-hour window often holds no completion and the rule would fire almost every day. Use the user import's interval plus about 6 hours: 30 hours for those presets, or GraphMetadataImportIntervalHours + 6 if you set it yourself (see Importer tuning reference → Individual overrides). That's a starting value, not a guarantee: a large directory can take longer. Two common false-positive cases to be aware of:

  • The trace "Skipping user metadata import" is emitted when the user import is intentionally turned off in configuration — if you've disabled it, also disable this alert.

  • If user import permissions are missing, the parent step logs "ERROR: Can't access Teams user data - are application permissions configured correctly?" and the rest of that cycle's Graph import is skipped. Pair this rule with the next one to distinguish the two scenarios.

  • Graph/Teams import is being skipped because of permissions

App Insights Query Time Period Trigger count
traces | where message startswith "ERROR: Can't access Teams user data" 1 hour > 0

Fires when Microsoft Graph refuses the app (HTTP 403), typically because an application permission was removed or was never consented. The web-job logs this warning instead of crashing, so without an alert the user metadata and Teams import will silently stop refreshing. It doesn't fire for an expired or wrong client secret or certificate: that fails earlier, when the importer asks Entra ID for a token, and is caught by Runtime credential rejected below.

  • Issues writing data into SQL

Persistent SQL errors of any kind. Time period 1 hour, trigger count > 10:

exceptions
| where outerType in ("Microsoft.Data.SqlClient.SqlException", "System.Data.SqlClient.SqlException")
    or innermostType in ("Microsoft.Data.SqlClient.SqlException", "System.Data.SqlClient.SqlException")

A full or read-only database. Azure SQL makes a database read-only when it reaches its maximum size, and a full transaction log or disk fails writes too. Time period 10 minutes, trigger count > 2:

exceptions
| where outerType in ("Microsoft.Data.SqlClient.SqlException", "System.Data.SqlClient.SqlException")
    or innermostType in ("Microsoft.Data.SqlClient.SqlException", "System.Data.SqlClient.SqlException")
| where outerMessage has "read-only" or innermostMessage has "read-only"
    or outerMessage contains "database is full" or innermostMessage contains "database is full"
    or outerMessage contains "insufficient disk space" or innermostMessage contains "insufficient disk space"
    or outerMessage contains "has reached its size quota" or innermostMessage contains "has reached its size quota"
    or outerMessage contains "Could not allocate space" or innermostMessage contains "Could not allocate space"
    or (outerMessage has "transaction" and outerMessage has "log" and outerMessage has "full")
    or (innermostMessage has "transaction" and innermostMessage has "log" and innermostMessage has "full")

Builds from Stable 1832 use Microsoft.Data.SqlClient, so a rule that matches only System.Data.SqlClient.SqlException never fires on them. These queries match both, as the outer or the innermost exception, so they also work on older builds and when another exception wraps the SQL error. They use the same conditions as the installer's Importer SQL or permission failures rule: if you enabled the installer-managed default alerts, you already have them.

Infrastructure load alerts

  • App Service Plan

For this, you need to add an alert to the App Service Plan that hosts the web app and web-jobs (not on an individual App Service).

It's normal to have high CPU during an import cycle, so set this trigger value high (e.g. > 90% average) for a fairly long period (e.g. 1 hour) to avoid noise.

  • SQL Database

Create the alert on the database & select DTU percentage for the signal (not DTUs used). DTUs are a simplified measure of CPU, memory, and disk I/O all together. If the database is consistently at 100% then you need to scale up the number of DTUs available.

DTUs explained — Service tiers - DTU-based purchase model - Azure SQL Database | Microsoft Docs.

  • Service Bus (only if the Teams calls import is enabled)

Service Bus may need scaling if the number of Teams calls to be tracked is very high. Alert on the Throttled Requests metric (should stay at 0) and Server Errors (also 0). If you start to see throttling, scale the namespace up.

Also alert on Count of dead-lettered messages in a Queue/Topic. (DeadletteredMessages), filtered by its EntityName dimension to the calls queue, when the maximum is above 0 over 1 hour. From Stable 1856, a call that runs out of retries is dead-lettered, and it's never imported unless you replay it. Microsoft Graph keeps a call record for only 30 days, so replay soon: see Replaying the Teams calls dead-letter queue. Administration → Service health shows the same count on its ServiceBus row. Since Stable build 1860 (PR #870): so does the Dead-lettered Teams calls card under Administration → Service configuration, which can also reprocess or clear them.

Permissions alerts

  • Runtime credential rejected (expired or wrong client secret)
App Insights Query Time Period Trigger count
union exceptions, traces | where outerMessage has_any ("AADSTS7000222", "AADSTS7000215") or innermostMessage has_any ("AADSTS7000222", "AADSTS7000215") or message has_any ("AADSTS7000222", "AADSTS7000215") 1 hour > 0

Entra ID refuses the runtime app registration's client secret with AADSTS7000222 when the secret has expired, and with AADSTS7000215 when it's wrong, for example a deleted secret, or the secret's ID pasted instead of its value (see Microsoft Entra authentication and authorization error codes). The importers then fail to get a token and log the error with its code. Match the codes rather than the message text, which Entra ID can reword. To clear it, add a new client secret to the runtime app registration and replace the old one wherever this deployment holds it: Key Vault and the App Service ClientSecret setting (see Security model → Secrets & Key Vault).

What this rule can and can't tell you:

  • It fires only after the secret has stopped working, the next time an importer asks for a token. Nothing in the solution can warn you before a client secret expires: track the expiry date in Entra ID (App registrations → the runtime app → Certificates & secrets) and rotate the secret before it.
  • It doesn't cover certificates. A certificate problem returns other codes. For a certificate deployment, use Runtime credential expiring soon below.
  • It doesn't cover missing API permissions. Graph refusing the app (403) is the Graph/Teams permissions rule above.
  • With the default health checks on, the Credential component also reports Unhealthy within five minutes of a secret failing (see Any component reported unhealthy), without the error code.

Health-telemetry alerts (component health, heartbeat, credential expiry, exception spike)

These rules use the importers' HealthCheck and ImporterHeartbeat events. Both importers send a heartbeat every minute from Stable 1854. The periodic HealthCheck events are sent only when the default health checks are on (EnableDefaultHealthChecks=true, which the installer writes when you turn on its health monitoring; see Configuration reference → Default health monitoring). A Service health page check in the portal is not one of these events.

  • Any component reported unhealthy
App Insights Query Time Period Trigger count
customEvents | where name == "HealthCheck" and tostring(customDimensions.Status) == "Unhealthy" 15 minutes > 0

The Component dimension tells you which check failed. Append and tostring(customDimensions.Component) == "Sql" to alert on a single component only. These are the components the importers report, and whether each can make this rule fire:

Component Sent by Can be Unhealthy?
Sql Both importers, every five minutes, when the default health checks are on Yes: the database can't be reached, isn't read/write, or its schema doesn't match the build.
Credential Both importers, every five minutes, when the default health checks are on Yes: the runtime credential can't get a token (for example an expired or wrong client secret), or its certificate has under three days left.
CopilotCostApi, CopilotStudioConsumption The Microsoft 365 importer, every five minutes, when the default health checks are on (from Stable 1857) Yes, as described below.
BlobCheckpoint The Microsoft 365 importer, each time its audit import starts No, only Healthy or Degraded. Use the storage rule below.
MessageTracing The Microsoft 365 importer No, only Healthy or Degraded.
CopilotAuditBackfill The Microsoft 365 importer, for a Copilot audit backfill No, only Healthy or Degraded. Follow a backfill on its own page instead.

A periodic check that fails with an error, or doesn't finish within 30 seconds, is reported as Unhealthy. No importer reports Graph, the Activity API, Key Vault, Service Bus or DNS as a component, so this rule never fires for them: their failures show up as errors of the import that needed them.

Message tracing reports as MessageTracing: Degraded for as long as it's switched on, as a reminder, or when it was requested but can't run or can't save to storage, and Healthy once it's off. It's never Unhealthy, so this rule doesn't fire for it. See Troubleshooting → Message tracing.

Since Stable 1857 (PR #787): the Microsoft 365 Activity importer also reports CopilotCostApi (the Copilot credit history connection) and CopilotStudioConsumption (the Copilot Studio billing connection). They read the saved connection rather than calling the cost APIs, and the status follows it: Healthy when switched off or after a successful read; Degraded with no connection saved, no successful read yet, or reads failing for under 15 minutes; Unhealthy when a reconnection is needed, a read was refused, the connection store cannot be read (Studio consumption with no Storage connection string configured counts as not connected instead), or reads have been failing for 15 minutes or more. This rule therefore fires for them only at Unhealthy, and a cost source nobody has connected never fires it. Append and tostring(customDimensions.Component) == "CopilotCostApi" (or "CopilotStudioConsumption") to alert on one.

  • Audit checkpoint fell back to memory (storage)
App Insights Query Time Period Trigger count
customEvents | where name == "HealthCheck" and tostring(customDimensions.Component) == "BlobCheckpoint" and tostring(customDimensions.Status) == "Degraded" 24 hours > 0

The audit import sends this when it can't open its durable checkpoint table in the storage account, and falls back to a checkpoint in memory that a restart loses; also when no Storage connection string is configured. The commonest cause is the storage account's firewall: see Storage firewall troubleshooting. This event comes from the audit import starting, not from a periodic check, so it's sent once per cycle at most, and never while no audit-based import is switched on. Use a window longer than a cycle, and don't take the alert resolving as proof that storage has recovered: look for a Healthy BlobCheckpoint event, or the trace Blob checkpoint: durable Azure Table store initialised.

  • Web-jobs not heartbeating (alert when the result is 0)
App Insights Query Time Period Trigger count
customEvents | where name in ("ImporterHeartbeat", "FinishedImportCycle") and operation_Name == "Office365ActivityImporter" | summarize beats = count() | where beats > 0 30 minutes (builds before Stable 1854 send no heartbeat: use the 24-hour cycle rule instead) 0

Create a second rule with operation_Name == "AppInsightsImporter": combining both jobs can hide one stopped job behind the other's activity.

The final where beats > 0 is required for a row-count alert. summarize count() without grouping always returns one row—even when its value is zero—so the earlier query could never meet Number of results = 0. Do not remove that filter unless configuring the alert to measure the numeric beats column instead.

  • Runtime credential expiring soon (certificate deployments only)
App Insights Query Time Period Trigger count
customEvents | where name == "HealthCheck" and tostring(customDimensions.Component) == "Credential" | extend days = toint(customDimensions.DaysToExpiry) | where days < 14 1 hour > 0

Warns before the runtime certificate expires and imports start failing. DaysToExpiry is the number of whole days left on the certificate the running importer loaded from Key Vault, and is sent only with the default health checks on. Add a second, higher-severity rule at days < 3 for a "critical" warning.

This rule never fires for a client secret, the default and what the Teams calls import needs. The importer can't read a client secret's expiry date, so for a secret the Credential check reports Degraded with Runtime token acquired. Client-secret expiry is unavailable; track it in the app registration. and no DaysToExpiry. Track the secret's expiry date in Entra ID instead, and rely on Runtime credential rejected to tell you when an expired secret has stopped working.

  • Exception spike (general health probe)
App Insights Query Time Period Trigger count
exceptions | summarize errors = count() | where errors > 10 1 hour > 0

A cheap catch-all. 10 is a synthetic example threshold: replace it with a value above your own normal hourly baseline. This query returns a row only when that threshold is crossed, so the row-count trigger is > 0, not the exception total. Alternatively measure the numeric errors column explicitly. Test both a below-threshold and an above-threshold case before relying on the rule.

Optional-service checks

For opted-in services, inspect Service health, the billing connection page, prompt-category run counters and the Agent 365 report's last completed/failed snapshot. Treat disabled, connection required, not classified, failed, usage not reported and measured zero as different states. Do not manufacture a generic HealthCheck alert for a component that your build has not emitted.

Useful deep diagnostics: Unknown licence coverage, Copilot Adoption lifecycle and billing connection remedies.

The original guidance also listed alerts for the standalone Yammer importer. Yammer is now imported through the standard Microsoft 365 admin-report APIs by the Microsoft 365 Activity Importer, so the dedicated Yammer alerts are no longer applicable. Yammer auth issues will surface in the generic SqlException / activity-import alerts above.


Return to Health Alerts · All topics in this guide

Start here

Copilot analytics

Using the portal

Install & configure

Beyond Copilot

Operate

Upgrading from an older build

Reference

Project

Clone this wiki locally