C
C#4w ago
Ainz

OpenTelemetry and Method Boundaries

Probably a dumb question, but does ActivitySource (within the context of OpenTelemetry-Dotnet and Application Insights) track context across a method boundary or an async method boundary? From what I've read, it internally tracks Activity.Current and should track context across method boundaries further down in the application, but I guess I don't understand entirely how that works internally. From what I've seen elsewhere, you have to set the parent context when you have multiple nexted Activities within the same scope, for setting an Outer activity and then small Inner activities if you wanted to track things within the same method. Outside of that, I know you need to pass parent context across thread boundaries if you using producer / consumer patterns and pushing things onto a queue (from what I understand) I guess my confusion lies where I don't understand how the Span and TraceId propagate across method boundaries into child method calls and how those subsequent child methods are instrumented and measured. In application insights, I see a Gantt chart with processing times for methods and I'm just wondering how that chart is created with the top level method calls propagating through all of the child method calls, and whether I have to pass the context down through those methods with each of their own activities for each method to be measured. Or if ActivitySources / OpenTelemetry-Dotnet does that for you
ActivitySource Source = new("Program");

using var ignore = Source.StartActivity("User Interface");
await UserInterface();

return;

static async Task UserInterface()
{
await ServiceLayer();
}

static async Task ServiceLayer()
{
await DataAccessLayer();
}

static async Task DataAccessLayer()
{
// Make a HTTP Call or access a database
}
ActivitySource Source = new("Program");

using var ignore = Source.StartActivity("User Interface");
await UserInterface();

return;

static async Task UserInterface()
{
await ServiceLayer();
}

static async Task ServiceLayer()
{
await DataAccessLayer();
}

static async Task DataAccessLayer()
{
// Make a HTTP Call or access a database
}
Or do I need to explicitly pass parent context down into each layer of the application
ActivitySource Source = new("Program");

using var x = Source.StartActivity("Program.Main");
await UserInterface(x.Context);

return;

static async Task UserInterface(ActivityContext parent)
{
using var a = Source.StartActivity("User Interface", ActivityKind.Internal, parent);
await ServiceLayer(a.Context);
}

static async Task ServiceLayer(ActivityContext parent)
{
using var b = Source.StartActivity("Service Layer", ActivityKind.Internal, parent);
await DataAccessLayer(b.Context);
}

static async Task DataAccessLayer(ActivityContext parent)
{
using var _ = Source.StartActivity("Data Layer", ActivityKind.Internal, parent);
// Make a HTTP Call or access a database
}
ActivitySource Source = new("Program");

using var x = Source.StartActivity("Program.Main");
await UserInterface(x.Context);

return;

static async Task UserInterface(ActivityContext parent)
{
using var a = Source.StartActivity("User Interface", ActivityKind.Internal, parent);
await ServiceLayer(a.Context);
}

static async Task ServiceLayer(ActivityContext parent)
{
using var b = Source.StartActivity("Service Layer", ActivityKind.Internal, parent);
await DataAccessLayer(b.Context);
}

static async Task DataAccessLayer(ActivityContext parent)
{
using var _ = Source.StartActivity("Data Layer", ActivityKind.Internal, parent);
// Make a HTTP Call or access a database
}
15 Replies
Nox
Nox4w ago
Most applications will only have one ActivitySource. It can and should be statically referenced. Activities are the dotnet name; in the OTel world they are traces. A trace has a single root, and then every trace underneath the root is a nested/child trace. This is the chart you're seeing in app insights. Activities can and should be created at the point of ingress for an application. - For an API, that is your endpoints/controllers - For a background processing job, it is the timer that starts that job - For a user interface, it is the event handler that reacts to user input (clicking a button, drawing on the screen, etc) It might be good to see if the UI framework you're using has OTel libraries or something baked in; ideally this is handled transparently for you
Ainz
AinzOP4w ago
I don't believe Avalonia does out of the box, but I could check
Nox
Nox4w ago
It's actually hard to find answers because results for avalonia and "Activity" result in android answers GPT says you can do this with InputManager.Instance.PreProcessInput += OnPreProcessInput; And then examining the routed events. Regardless - the point is that you want some very central way to start an activity. You do not want to think about it, and the conventions for the arguments and tags associated with that activity should be highly consistent and predictable. For ASP.NET, when a request comes in, I always know the path of the rest, the HTTP method, and a couple other things. TYPICALLY you don't need to be concerned with starting all kinds of internal sub-activities.
Ainz
AinzOP4w ago
They have a couple of activities of their own for raising events, etc
No description
Nox
Nox4w ago
Sounds like RaisingRoutingEvent is what you want Basically - by the time your code is hit, you want an activity already started.
Ainz
AinzOP4w ago
Gotcha
Nox
Nox4w ago
So, where it gets messy I actually bitched about this a bit ago
Nox
Nox4w ago
No description
Nox
Nox4w ago
OTel easily allows for static access to its concepts; they're thread-safe, and easy to consume anywhere The most common pieces you'll use are likely ActivitySource and Meter In most apps, you want just one of each, and to store it in a readily accessible area JUST LIKE how you might have seen HttpContext.Current before as a way to access the current HTTP request used anywhere in the business logic of ASP.NET apps... We can do the same with Activity.Current It's "not bad" because observability is a cross-cutting concept upon which you don't build business logic, therefore should be trivial to access The "current" activity flows across your application because it's backed by an underlying AsyncLocal<Activity> So when Avalonia starts the RaisingRoutedEvent activity for you, always capable of referencing that activity. If they're nice, they'll have added some tags (which show up as custom dimension in app insights) indicating the name of the clicked object and perhaps the name of the invoked handler method When you call StartActivity, it automagically knows to associate it with the parent activity as-needed.
Ainz
AinzOP4w ago
Gotcha, that's what I was wondering about
Nox
Nox4w ago
It's a tough sell because we've been trained for years to inject and pass parameters And now we're being told that observability "just works" like magic. I'm a fan of it because of the simplicity, but it's going to be hard to disambiguate good vs poor usages of static access for my team. Generally speaking, as a first pass for observability, less is more. Ensure your activities are starting and automatically wired up for you on event handlers Ensure when you log something, the logs are associated to a trace ID, and accessible via the app insights transcation ID search Start with that. Then as you get a feel for it, start tagging activites with extra bits of data as you load them. Perhaps augment the global event handler in Avalonia with a tag that indicates who the currently authenticated user is
Unknown User
Unknown User4w ago
Message Not Public
Sign In & Join Server To View
Ainz
AinzOP4w ago
Even though I see that Avalonia is supposed to call StartActivity internally when a Button is clicked / when it's internal routed event is triggered, I don't see any traces being created when this happens. So not really sure what's going on with that. I have called AddSource("Avalonia.Diagnostic.Source") when building the tracer provider, but it doesn't seem to register. Actually: I think I found what the issue is after more digging AppContext needs to be set to true for this to be enabled in their static constructor AppContext.TryGetSwitch("Avalonia.Diagnostics.Diagnostic.IsEnabled", out var isEnabled) && isEnabled; Ok, after setting RuntimeConfig Avalonia.Diagnostics.Diagnostic.IsEnabled, that enabled it
No description
No description
No description
Unknown User
Unknown User4w ago
Message Not Public
Sign In & Join Server To View
Ainz
AinzOP4w ago
Yeah, I just saw it after reading through the Diagnostic class here, then I found their old pull request https://github.com/AvaloniaUI/Avalonia/pull/18314 where they talked about how they implemented it with a RuntimeConfig check <ItemGroup> <RuntimeHostConfigurationOption Include="Avalonia.Diagnostics.Diagnostic.IsEnabled" Value="true" /> </ItemGroup>
GitHub
Add diagnostic metrics/activities by maxkatz6 · Pull Request #1831...
What does the pull request do? Histogram metrics avalonia.comp.render.time avalonia.comp.update.time avalonia.ui.measure.time avalonia.ui.arrange.time avalonia.ui.render.time avalonia.ui.input.tim...
No description

Did you find this page helpful?