Splunk AppDynamics

.NET Core async transaction: Exit Calls / Threads column empty in Function Trace

adhi
New Member

Hi Team,

I am testing a simple ASP.NET Core async application with AppDynamics .NET Agent.

The agent is successfully detecting:
- Business Transactions
- HTTP Exit Calls
- Database Calls
- Service Topology
- Async timings

I can see the transaction flow map correctly between ServiceA and ServiceB, and database calls are also visible.

However, in the Transaction Snapshot -> Call Graph / Function Trace view, the “Exit Calls / Threads” column is empty for async execution.

Application Flow:
1. ASP.NET Core endpoint receives request
2. Async HTTP call using HttpClient
3. Async SQLite DB operations
4. Response returned successfully

Sample Code:

```csharp
using Microsoft.AspNetCore.Builder;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Hosting;
using System.Net.Http;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

string dbPath = "Data Source=test.db";

using (var conn = new SqliteConnection(dbPath))
{
conn.Open();

var cmd = conn.CreateCommand();
cmd.CommandText =
"CREATE TABLE IF NOT EXISTS Test (Id INTEGER PRIMARY KEY, Name TEXT)";

cmd.ExecuteNonQuery();
}

app.MapGet("/call", async () =>
{
var httpClient = new HttpClient();

var response =
await httpClient.GetStringAsync("http://localhost:5002/api/hello");

using var conn = new SqliteConnection(dbPath);

await conn.OpenAsync();

var insertCmd = conn.CreateCommand();
insertCmd.CommandText =
"INSERT INTO Test (Name) VALUES ('APM Test')";

await insertCmd.ExecuteNonQueryAsync();

var selectCmd = conn.CreateCommand();
selectCmd.CommandText = "SELECT COUNT(*) FROM Test";

var count =
(long)await selectCmd.ExecuteScalarAsync();

return new
{
message = "Service A response",
serviceB = response,
dbCount = count
};
});

app.Run();

adhi_0-1778485267643.png

 

adhi_1-1778485295825.png

Questions:

  1. Is this expected behavior for async/await execution in .NET Core?
  2. Does AppDynamics correlate async continuations differently from thread-based execution?
  3. Is there any configuration needed to populate the Exit Calls / Threads column for async transactions?
  4. Does this work differently for synchronous requests?

I have attached screenshots of:

  • Transaction Flow Map
  • Function Trace / Call Graph

Any clarification would be helpful.

Thanks.

Labels (1)
Tags (2)
0 Karma

natecrisler
Path Finder

Thanks for the clearer shot. I had one of the numbers wrong: the HTTP call is 232 ms, not 4 ms. That's worth correcting because it actually splits the answer into two different reasons.

Reading the flow map properly: the HTTP call from ServiceA to ServiceB is 232 ms (async), the ADO.NET/SQLite call to test.db is 4 ms (async), and ServiceA's own work is about 5 ms. So you've got one exit well above the call graph threshold and one well below it, and they're missing from the Exit Calls/Threads column for different reasons.

The SQLite call (4 ms) is the simple one. It's below the call graph's minimum capture threshold (min-duration-for-jdbc-call-in-ms, 10 ms by default for SQL/ADO.NET), so it never gets drawn in that column even though the agent recorded it. Drop that threshold toward 1 ms on the test node and it'll start showing.

The HTTP call (232 ms) is the interesting one, and the threshold doesn't explain it, because 232 ms is far over any limit. This is the async continuation behavior. When you await the HttpClient call, the method yields and the real 232 ms wait happens on a detached continuation, not inline on the synchronous frames the sampler is showing you (the AsyncTaskMethodBuilder and TaskAwaiter rows). For async exits the agent doesn't nest the call under your method, it surfaces it as an "await@TierName" link in the Exit Calls/Threads column, and it lists every exit in the snapshot's DB & Remote Service Calls tab regardless. So the reliable place to confirm the 232 ms call is that DB & Remote Service Calls tab and the flow map. One thing to check first: make sure the snapshot you're drilling into is actually a full /call instance that contains the HTTP hop (something around the 252 ms total), not a short instance that never made the call. If the Exit Calls/Threads column is still empty on a snapshot that clearly includes the 232 ms call, that points at how your agent renders async rather than at a missing exit.

So to your four questions, now with the right numbers:

  • Expected for async? Yes. A fast exit (your DB call) is hidden by the threshold, and a slow async exit (your HTTP call) is handled as an async continuation rather than nested inline in the call graph. Neither means anything is broken.
  • Correlated differently from threads? Yes, and the 232 ms call is the clean example. If it were synchronous it would block the calling thread and get annotated inline against your method. Because it's awaited, it detaches to the continuation and shows as the await link instead. Async backend tracking and thread correlation are separate mechanisms in the agent.
  • Config to populate the column? For the SQLite call, lower min-duration-for-jdbc-call-in-ms. For the HTTP call there's nothing to lower, it's already above threshold, so it's about opening the right snapshot, not a setting. Also confirm you're on a full call graph rather than a partial one, since partials can omit exit calls. The thread-correlation node property only matters if you spawn real threads.
  • Different for synchronous? Yes, meaningfully. Run that 232 ms HTTP call synchronously and it would sit on the stack and show inline in the call graph against your method, with the exit in that column. Async moves it to the continuation. The threshold still applies either way, so the 4 ms DB call would stay hidden even synchronously.

One caveat worth knowing: the await link in the Exit Calls/Threads column is documented for the .NET Framework agent. The .NET Core agent can represent async exits differently, so if you never find an await link, treat the DB & Remote Service Calls tab and the flow map as the source of truth for that 232 ms call.

Worth a read:

On the drill-down, if you open a snapshot that actually shows the full /call (around the 252 ms total, not the 8 ms or 23 ms instances), do you see the 232 ms HTTP call or an await@ServiceB link in the DB & Remote Service Calls tab or the Exit Calls/Threads column? That'll tell us whether your agent renders async exits with the await-link style at all, or only lists them in the exit-call tab.

0 Karma
Got questions? Get answers!

Join the Splunk Community Slack to learn, troubleshoot, and make connections with fellow Splunk practitioners in real time!

Meet up IRL or virtually!

Join Splunk User Groups to connect and learn in-person by region or remotely by topic or industry.

Get Updates on the Splunk Community!

Think Like an Architect: Introducing the Splunk Certified Cybersecurity Defense ...

In cybersecurity, defenders respond to threats. Architects design the systems that stop them.    As ...

Best Practices: Splunk auto adjust pipeline queue

When you enable autoAdjustQueue in Splunk, maxSize should be understood as the queue size Splunk starts with ...

Announcing Modern Navigation: A New Era of Splunk User Experience

We are excited to introduce the Modern Navigation feature in the Splunk Platform, available to both cloud and ...