Correlation in Application Insights: trace context, operation_Id and Application Map
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 Insights | W3C Trace Context | OpenTelemetry |
|---|---|---|
operation_Id | trace-id | Trace ID |
id of a request or dependency | parent-id sent downstream | Span ID |
operation_ParentId | parent-id of this span’s parent | Parent span ID |
| Request | Server span | |
| Dependency | Client, 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
| Symptom | Likely cause | Fix |
|---|---|---|
| One node on the map | Same role name everywhere | Set OTEL_SERVICE_NAME or the role name per service |
| A downstream service missing | No access to its resource, or not HTTP | Get read access; correlate non-HTTP calls by hand |
| Requests with no parent in the next service | Header stripped or not propagated | Check proxies and gateways forward traceparent |
| Half a trace present | Services sample differently | Use the same sampler and rate everywhere |
| Consumer work appears as separate operations | Context not passed through the queue | Write traceparent into the message; start the consumer from it |
| Map still shows old structure | Map not rediscovered | Update map components and check the time range |
Queries for this topic
-
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.
-
Request latency p50, p95 and p99 by cloud role
requests latency
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
- Application map in Azure Application Insights
- Monitor .NET and Node.js applications with Application Insights (Classic API 2.x, distributed tracing and queues)
- Configure OpenTelemetry in Application Insights (cloud role name and instance)
- Enable OpenTelemetry in Application Insights (cloud role names on the map)
- Migrate Application Insights Classic API SDKs to OpenTelemetry (terminology)
- Trace Context (W3C, editor's draft of the Recommendation)
- Distributed tracing concepts in .NET
- Add distributed tracing instrumentation in .NET