Since the last two lessons, the Kladde sees its requests, SQL commands and calls to TED, locally and in Azure. ASP.NET Core, HttpClient and
SqlClient delivered that without you writing a single line. But two things are still missing, and no library can take them off your hands:
- Your own logic. In the trace of a booking, there is time between the SQL spans that nobody says what it was for. Does the Kladde check for overlaps there, calculate free times, render the page?
- The numbers of the business. How many bookings come in per day, through which channel, how far ahead? ASP.NET Core counts requests, but a request is not a booking: It can fail, arrive twice or only display a page.
In this lesson you write your own spans and your own measurements, switch them on and test them.
A span of your own
You write a span with an ActivitySource. You create a source once, usually as a static field, and give it a name that is filtered on later.
For the Kladde's bookings it is called Kladde.Bookings. This file app shows the building blocks of a span: attributes, status and exception.
// span.cs
using System.Diagnostics;
using var source = new ActivitySource("Kladde.Bookings");
using var listener = new ActivityListener
{
ShouldListenTo = s => s.Name == "Kladde.Bookings",
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllDataAndRecorded,
ActivityStopped = Print,
};
ActivitySource.AddActivityListener(listener);
// A check that finds the slot free: a normal outcome, status stays Unset.
using (Activity? check = source.StartActivity("kladde.booking.check_overlap"))
{
check?.SetTag("kladde.booking.overlaps", false);
}
// A check that fails: the span records the exception and says Error.
try
{
using Activity? check = source.StartActivity("kladde.booking.check_overlap");
try
{
throw new TimeoutException("The database did not answer within 30 seconds.");
}
catch (Exception ex)
{
check?.AddException(ex);
check?.SetStatus(ActivityStatusCode.Error, ex.GetType().Name);
throw;
}
}
catch (TimeoutException)
{
Console.WriteLine("(exception reached the caller)");
}
static void Print(Activity span)
{
Console.WriteLine($"{span.DisplayName}: Status {span.Status}{(span.StatusDescription is null ? "" : $" ({span.StatusDescription})")}");
foreach (KeyValuePair<string, object?> tag in span.TagObjects)
{
Console.WriteLine($" Attribute {tag.Key} = {tag.Value}");
}
foreach (ActivityEvent e in span.Events)
{
Console.WriteLine($" Event {e.Name}: {string.Join(", ", e.Tags.Where(t => t.Key != "exception.stacktrace").Select(t => $"{t.Key} = {t.Value}"))}");
}
}
dotnet run span.cs
kladde.booking.check_overlap: Status Unset
Attribute kladde.booking.overlaps = False
kladde.booking.check_overlap: Status Error (TimeoutException)
Event exception: exception.message = The database did not answer within 30 seconds., exception.type = System.TimeoutException
(exception reached the caller)
What you take away from it:
StartActivitycan returnnull, namely when nobody is listening. That is why there is a?.everywhere. It is not a blemish but the reason spans cost almost nothing as long as nobody collects them.usingends the span. Its duration is the time betweenStartActivityand the end of theusingblock. What happens in this block and creates spans itself, such as a SQL command, hangs below it.- The status stays
Unsetwhen everything went normally. Even a taken slot is a normal outcome, not an error: The Kladde answered correctly. You setErroronly when the work failed. Otherwise Application Insights counts taken slots as failures, and the failure rate no longer means anything. AddExceptionattaches the exception to the span as an event with type, message and stack. In Application Insights it therefore shows up inAppExceptions, linked to the trace. After that, the code rethrows the exception: The span describes the failure, it does not handle it.
The names follow the rules of OpenTelemetry: lowercase, structured with dots, with a prefix of your own (kladde.) so they do not mix with the
names of other libraries. The name of a span describes the kind of work, not the single case:
kladde.booking.check_overlap, never Check for booking 1042. What makes up the single case belongs in attributes.