APM Tips

Application Insights

Correlation in Application Insights: trace context, operation_Id and Application Map

Updated · 7 min read · By APM Tips editorial team

Every item in one distributed operation shares an operation_Id, which is the W3C trace-id, and points at its parent through operation_ParentId, the parent's span-id; HTTP calls carry both in the traceparent header, so .NET services correlate automatically. Application Map draws a node per cloud role name and an edge per correlated HTTP dependency. When it's empty or shows one node, the cause is almost always missing or identical role names, services you can't access, sampling that breaks traces, or work that crosses a queue without passing the context.

Correlation is what turns a pile of telemetry into a story: this page view called that API, which called this database and threw that exception. Application Insights keeps the story in a few fields, and Application Map and the end-to-end transaction view are just ways of reading them.

The fields

Every item carries operation_Id. Requests and dependencies also have their own id, and every item records the id of its parent in operation_ParentId. A dependency call’s id becomes the operation_ParentId of the request it triggers in the next service, which is how the chain crosses process boundaries.

These map one-to-one onto W3C Trace Context, the W3C Recommendation that standardised the traceparent and tracestate headers, and onto OpenTelemetry’s vocabulary:

Application InsightsW3C Trace ContextOpenTelemetry
operation_Idtrace-idTrace ID
id of a request or dependencyparent-id sent downstreamSpan ID
operation_ParentIdparent-id of this span’s parentParent span ID
RequestServer span
DependencyClient, producer or internal span

In .NET, an Activity’s Id in the default W3C format is exactly a traceparent value: 00-<trace-id>-<span-id>-<flags>. Since .NET 5, W3C is the default ID format, and ASP.NET Core and System.Net.Http read and write the headers themselves, so HTTP correlation needs no code.

Follow one operation across tables, using the operation_Id from any item:

union requests, dependencies, exceptions, traces
| where timestamp > ago(1d) and operation_Id == "<operation id>"
| project timestamp, itemType, cloud_RoleName, name, id, operation_ParentId, success, message
| order by timestamp asc

In a workspace the same columns are OperationId, ParentId, Id and AppRoleName across AppRequests, AppDependencies, AppExceptions and AppTraces.

Give the ID to people, not just tools

Correlation pays off most when a person can start from it. Logs written through ILogger inside a request already carry the request’s operation_Id, so a search for one log line leads to the whole operation. The other direction needs a little code: return the trace ID to the caller, so a support ticket or an error page can quote it, and add it to your own log output if those logs go somewhere else too.

ASP.NET Core middleware that returns the current trace ID (the operation_Id) on every response:

app.Use(async (context, next) =>
{
    var traceId = Activity.Current?.TraceId.ToHexString();
    if (traceId is not null)
        context.Response.Headers["X-Trace-Id"] = traceId;
    await next();
});

Paste that value into the KQL above, or into the search box of the end-to-end transaction view, and you’re looking at the exact request a user complained about. Microsoft documents Activity.Current?.TraceId.ToHexString() and SpanId.ToHexString() as the way to read the IDs with OpenTelemetry; the same calls work under the classic SDK, which also builds on Activity. Don’t put anything else in that header: a trace ID identifies an operation, not a user.

How Application Map is drawn

Application Map draws one node per cloud role name and follows HTTP dependency calls between instrumented services to draw the edges. Dependencies on things you don’t instrument (a SQL database, a third-party API) appear as targets of the calling node. Components can be roles within one Application Insights resource or separate resources, even in different subscriptions; the map stitches separate resources together only when you have access to each of them. When all components are roles in a single resource, no discovery step is needed.

That explains the common failures:

  • One node for everything. All services report the same role name. Microsoft states that when two or more services send telemetry to the same resource, you’re required to set cloud role names.
  • Missing nodes. The other service reports to a resource you can’t read, or it isn’t instrumented, or the call between them isn’t HTTP. To query across those resources yourself, see cross-resource queries.
  • Disconnected nodes. Context isn’t reaching the next service: a proxy strips headers, a service runs an SDK that doesn’t speak W3C, or each service samples independently so one side of a call was dropped. The sampling guide covers using the same sampler everywhere.
  • Breaks at queues. Message brokers don’t carry HTTP headers, so the context has to travel in the message.

Classic SDK 2.x

All recent classic .NET SDKs use W3C Trace Context by default and stay compatible with the older Request-Id header, which Microsoft describes as being deprecated. The SDK sets request.source and dependency.target from the Request-Context header so a call can point at the component on the other side. Set the role name in a telemetry initializer (telemetry.Context.Cloud.RoleName), as in telemetry initializers and processors.

For queues, Microsoft’s classic guidance is to put the context in the message and restore it on the consumer before tracking anything. It also notes that, unlike HTTP, end-to-end views across separate resources aren’t supported for queue-based flows.

Producer and consumer correlation through a queue with the 2.x SDK:

// Producer: carry the current context in the message.
message.ApplicationProperties["traceparent"] = Activity.Current?.Id;
await sender.SendMessageAsync(message);

// Consumer: restore it, then track the processing as a request.
var activity = new Activity("ProcessOrder");
if (received.ApplicationProperties.TryGetValue("traceparent", out var parent))
    activity.SetParentId((string)parent);
activity.Start();
using (telemetryClient.StartOperation<RequestTelemetry>("ProcessOrder"))
{
    await HandleAsync(received);
}
activity.Stop();

Starting the operation inside the restored Activity makes the request, and everything tracked within it, share the producer’s operation_Id. The same pattern fixes “manual correlation” in older ASP.NET MVC apps where work hops to a thread the SDK doesn’t follow: capture Activity.Current.Id before the hop and set it as the parent after.

Today: SDK 3.x and the Microsoft OpenTelemetry Distro

With OpenTelemetry the model is native: requests are server spans, dependencies are client and other non-server spans, and traceparent is the default propagation format. SDK 3.x is built on the same pipeline (its StartOperation uses OpenTelemetry activities instead of the legacy correlation system), so the two current paths behave the same.

Role names. The Distro sets a default role name and instance from the environment where it can. Override them with resource attributes: the role name comes from service.namespace plus service.name (or service.name alone), and the instance from service.instance.id. The environment variables are the easiest way:

export OTEL_SERVICE_NAME="orders-api"
export OTEL_RESOURCE_ATTRIBUTES="service.namespace=contoso,service.instance.id=$HOSTNAME"

OTEL_SERVICE_NAME wins over a service.name set in OTEL_RESOURCE_ATTRIBUTES. Java uses APPLICATIONINSIGHTS_ROLE_NAME or role.name in its JSON configuration.

Queues. Put the producer’s traceparent in the message and start the consumer’s span from it. Use ActivityKind.Consumer, which Application Insights stores as a request, so the processing appears as its own operation step:

private static readonly ActivitySource Messaging = new("Contoso.Orders.Messaging");

// Producer
using (var send = Messaging.StartActivity("orders send", ActivityKind.Producer))
{
    message.ApplicationProperties["traceparent"] = send?.Id ?? Activity.Current?.Id;
    await sender.SendMessageAsync(message);
}

// Consumer
received.ApplicationProperties.TryGetValue("traceparent", out var tp);
ActivityContext.TryParse(tp as string, null, out var parent);
using (var process = Messaging.StartActivity("orders process", ActivityKind.Consumer, parent))
{
    await HandleAsync(received);
}

Register the source with AddSource("Contoso.Orders.Messaging"); DiagnosticSource and Activity explains why that registration matters. When one consumer span handles messages from many traces, as in batch processing, a span can have only one parent; .NET lets you attach the others as ActivityLinks when the activity starts.

Troubleshooting

SymptomLikely causeFix
One node on the mapSame role name everywhereSet OTEL_SERVICE_NAME or the role name per service
A downstream service missingNo access to its resource, or not HTTPGet read access; correlate non-HTTP calls by hand
Requests with no parent in the next serviceHeader stripped or not propagatedCheck proxies and gateways forward traceparent
Half a trace presentServices sample differentlyUse the same sampler and rate everywhere
Consumer work appears as separate operationsContext not passed through the queueWrite traceparent into the message; start the consumer from it
Map still shows old structureMap not rediscoveredUpdate map components and check the time range

Queries for this topic

From the KQL library: explained queries for the Application Insights tables, ready to copy.

  • End-to-end trace for one operation_Id

    requestsdependenciestracesexceptionspageViewscustomEvents operations

    Returns: Every telemetry item recorded with the given operation_Id in the last 7 days, oldest first, with its type, service, name or message, duration, outcome, severity, and the IDs that link parent and child.

  • Returns: One row per cloud role name with its request count and its p50, p95 and p99 duration in milliseconds, slowest p95 first.

  • Outbound HTTP calls by target host

    dependencies dependencies

    Returns: One row per outbound HTTP target with its call count, p95 duration in milliseconds, failed calls and percentage, and how many of your services call it, busiest first.

Frequently asked questions

Why does Application Map show only one node?

Every service reports the same cloud role name, so the map can't tell them apart. Microsoft says that when two or more services send to one resource you're required to set role names; with OpenTelemetry, set service.name (and optionally service.namespace) per service.

Can Application Map join services in different Application Insights resources?

Yes, even across subscriptions, as long as the calls are correlated HTTP dependencies and you have access to every resource involved. Nodes in resources you can't read don't appear. Use Update map components to rediscover them.

How do I find all telemetry for one request?

Take its operation_Id and query requests, dependencies, exceptions and traces for that value, or open the request's end-to-end transaction view in the portal, which does the same across the resources you can read.

Sources, checked

  1. Application map in Azure Application Insightslearn.microsoft.com · checked
  2. Monitor .NET and Node.js applications with Application Insights (Classic API 2.x, distributed tracing and queues)learn.microsoft.com · checked
  3. Configure OpenTelemetry in Application Insights (cloud role name and instance)learn.microsoft.com · checked
  4. Enable OpenTelemetry in Application Insights (cloud role names on the map)learn.microsoft.com · checked
  5. Migrate Application Insights Classic API SDKs to OpenTelemetry (terminology)learn.microsoft.com · checked
  6. Trace Context (W3C, editor's draft of the Recommendation)w3c.github.io · checked
  7. Distributed tracing concepts in .NETlearn.microsoft.com · checked
  8. Add distributed tracing instrumentation in .NETlearn.microsoft.com · checked