APM Tips

KQL · cost

Noisiest trace messages by volume and severity

The log messages that cost the most over the last 24 hours, grouped by message prefix and severity level, with billed megabytes and item counts.

Updated

traces cost

let lookback = 24h;
traces
| where timestamp > ago(lookback)
| extend message_start = substring(message, 0, 80)
| summarize
    items = sum(itemCount),
    billed_mb = round(sum(_BilledSize) / 1e6, 1),
    roles = dcount(cloud_RoleName),
    example_role = take_any(cloud_RoleName)
    by message_start, severityLevel
| top 25 by billed_mb desc

Returns: Up to 25 rows, one per message prefix and severity level, with the sampling-adjusted number of trace items, their billed megabytes and the services that wrote them, most expensive first.

Pitfalls

  • Grouping by the first 80 characters folds messages that differ only at the end, such as an ID, into one row. Messages that start with a timestamp or an ID won't group; shorten or move the prefix.
  • severityLevel is a number. In the classic SeverityLevel enum, 0 is Verbose, 1 Information, 2 Warning, 3 Error and 4 Critical.
  • Billed size counts what was stored. Trace items dropped by sampling cost nothing, so a sampled message can be noisier at the source than it looks here.
  • message can contain personal data your code logged. Mind where you share the result.

How it works

Logs are usually the biggest and least-read part of an Application Insights bill. A handful of messages, logged on every request at a level nobody looks at, can outweigh everything else. This query finds them, ranked by what they cost rather than how often they occur.

Line by line

  • let lookback = 24h; covers a day; logging volume follows traffic, so a day is a fair sample.
  • message_start = substring(message, 0, 80) keeps the first 80 characters. Log messages usually start with fixed text and end with the variable part (an order number, a duration), so the prefix works as a rough template and keeps the grouping from exploding into one row per message.
  • items = sum(itemCount) counts trace items the sampling-aware way.
  • billed_mb = round(sum(_BilledSize) / 1e6, 1) adds up billed bytes in megabytes. _BilledSize is a standard column Microsoft documents on every ingested Application Insights event; it only appears when you name it.
  • roles and example_role show which services write the message, which tells you whose logging configuration to change.
  • top 25 by billed_mb desc ranks by cost. Rank by items instead to find the most frequent messages.

What to do with the result

For each expensive row, decide whether anyone reads it. Information-level messages written on every request are the usual culprits: raise the minimum level for that category, or log the event once per batch instead of once per item. The logging guide shows where the levels are set for ILogger in the classic SDK and the OpenTelemetry Distro. Messages you do need but not at full volume are a job for sampling, which the pricing guide compares with filtering.

If your logging library records the message template as a property, grouping by it beats a prefix. Look at customDimensions on a few rows (traces | take 10 | project message, customDimensions) to see what your provider records.

Variations

  • By level only: group by severityLevel to see how much each level costs in total.
  • Workspace tables: use AppTraces, TimeGenerated, Message, SeverityLevel, AppRoleName, ItemCount and _BilledSize.

For the table-level picture first, see telemetry volume by table.

Sources, checked

  1. Analyze usage in a Log Analytics workspace (Application Insights data volume)learn.microsoft.com · checked
  2. SeverityLevel enum (Microsoft.ApplicationInsights.DataContracts)learn.microsoft.com · checked
  3. substring() (Kusto)learn.microsoft.com · checked
  4. top operator (Kusto)learn.microsoft.com · checked
  5. Migrate an Application Insights classic resource to a workspace-based resource (table schemas)learn.microsoft.com · checked