question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Make `Span` and `Transaction` disposable? - Agent API design

See original GitHub issue

This issue is about a design decision and for discussion about this specific decision. Relates to #26.

To save time/space in the discussion we focus on Span, but the same discussion is valid for Transaction.

Description of the current situation

The default pattern to wrap a particular piece of code into a span is this:

var span = transaction.StartSpan("SampleSpan", Span.TYPE_DB);
try
{
    //code you want to capture as a span
}
catch(Exception e)
{
    span.CaptureException(e);
    throw; //don't use `throw e` to preserve the call stack.
}
finally
{
    span.End();
}

This is very similar to the Java Agent API.

Currently both ISpan and ITransaction offer a convenient method to start, stop spans/transactions and also to automatically report unhandled exceptions. The intention is to avoid having to write too many additional lines just to capture something as a span or transaction.

So, the equivalent of the code from above with the convenient API is this:

transaction.CaptureSpan("TestSpan", Span.TYPE_DB, () =>
{
   //code you want to capture as a span
});

There are overloads for async methods, and also for methods with return value.

Alternative solution, IDisposable

Lots of similar tools, like MiniProfiler solve this by implementing IDisposable, and not by having a method that takes Action or Func as we do. Therefore some users may expect that our Span and Transaction types also implement IDisposable and disposing those would do basically the same as calling the End() method.

With this the code would be something like this:

using(var span = transaction.StartSpan("SampleSpan", Span.TYPE_DB))
{
   //code you want to capture as a span
   //+ capturing exceptions somehow
}

Therefore: Should our Span and Transaction types implement IDisposable?

Pros:

  • Other tools do this (OpenTracing C# API, MiniProfiler).
  • Wrapping these kings of things into a using block seems very natural.
  • using avoids closures, which we have with the methods taking a lambda. This can cause overhead in perf. critical scenarios due to allocations. (On the other hand the StartX() and End() combination still can be used to avoid this problem).

Cons:

  • The using block only partially solves the problem: it can automatically call End(), but it does not capture unhandled exceptions. Actually tools that typically use IDisposable for similar things don’t have a catch(Exception e){capture(e)} part in the pattern that they simplify with the using block. So having the using block plus an additional exception handling logic would make the API more noisy than the plain, original API from the beginning of the discussion.
  • In the original API ending a span happens by calling the End() method, and this is common across all agents. If we implement IDisposable then we will call the End() method from the Dispose() method and from that point we basically have 2 methods that end a span, and that does not feel right. The current API does not have this confusion.
  • Adding IDisposable is a pattern to wrap unmanaged resources. We don’t really wrap unmanaged resources here. So automatically calling the End() method is a similar problem to the problem that IDisposable solved, but it’s not the same. See IDisposable documentation : “Provides a mechanism for releasing unmanaged resources.” That’s not what we do by calling End().

Currently we don’t implement IDisposable and that seems to be the better decision, but this can be of course revisited. Happy to hear opinions!

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Comments:9 (8 by maintainers)

github_iconTop GitHub Comments

2reactions
gregkalaposcommented, Jan 31, 2019

We discussed this offline and currently we don’t plan to add IDisposable to our spans and transactions.

I close this now, but we can reopen this and revisit this decision later.

0reactions
gregmaccommented, Dec 15, 2022

I am brand new to using this, but it definitely felt very off this wasn’t using using.

Another pro

  • You can use C#8 using declarations to very easily add spans with a single line change, without wrapping code in braces, eg:
using var span = transaction.StartSpan("SampleSpan", Span.TYPE_DB);

I’ve used this with great effect in the past adding MiniProfiler to a legacy application: I was specifically interested in profiling some slow, multi-hundred-line, badly-indented monstrosities of functions, and this let me create a readable pull request. Wrapping in braces would have meant a ton of whitespace noise in the PR plus having my name forever stuck on the git blame of that awful code.

Arguments against cons

  • The using block only partially solves the problem: it can automatically call End(), but it does not capture unhandled exceptions. So having the using block plus an additional exception handling logic would make the API more noisy than the plain, original API from the beginning of the discussion.

I’d still argue it’s less noisy, because it’s one less line of code:

using var span = transaction.StartSpan("SampleSpan", Span.TYPE_DB);
try
{
    //code you want to capture as a span
}
catch(Exception e)
{
    span.CaptureException(e);
    throw; //don't use `throw e` to preserve the call stack.
}

In the case of nested spans – especially if you are doing this in nested functions, which is the natural use case – you can ignore the exception while letting it be caught by a higher span or at the transaction level.

There’s also a trick to detect exceptions, though it feels a bit hacky.

The logic in the Dispose() handler could basically be:

void Dispose() 
{
    if (!this.Ended)
    {
        // exception happened, but wasn't captured -- mark it for reference
        if (Marshal.GetExceptionCode()!=0 && !this.CapturedException) CaptureException(new UnknownException()); 
        
        End();
    }
}

  • In the original API ending a span happens by calling the End() method, and this is common across all agents. If we implement IDisposable then we will call the End() method from the Dispose() method and from that point we basically have 2 methods that end a span, and that does not feel right. The current API does not have this confusion.

As a .NET developer, I don’t understand why this would be confusing, as it’s quite a common pattern in many objects that implement IDiposable – for example System.Data.SqlClient.SqlConnection.Close. The comments there state “Close and Dispose are functionally equivalent” and adding that to the docs would help.

See also in my code sample above, I added an Ended property to track if End() was called manually and not call it twice.


  • Adding IDisposable is a pattern to wrap unmanaged resources. We don’t really wrap unmanaged resources here. So automatically calling the End() method is a similar problem to the problem that IDisposable solved, but it’s not the same. See IDisposable documentation : “Provides a mechanism for releasing unmanaged resources.” That’s not what we do by calling End().

Arguing semantics, I think it is what this does. A “Span” is pretty much an unmanaged resource – even if it’s not consuming an actual resource like a socket or file handle.

Practically speaking, it’s a widely-used convention in .NET, and frankly, not having it makes this feel like an API from the early 2000’s. This answer links to a lot more debate (both for and against) than I can cover.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Public API | APM Java Agent Reference [1.x]
The public API of the Elastic APM Java agent lets you customize and manually create spans and transactions, as well as track errors....
Read more >
How to create custom span with all details as it's ...
The apm-agent-api doesn't allow this. Is there a way to set additional details, like url, to custom span? (I don't want to use...
Read more >
GET /api/v1/provider-consents
Our API is designed around REST, has predictable resource-oriented URLs, ... Amount Span, Searches for transactions within the given amount span.
Read more >
Fundamentals of good API design — Paystack API Casestudy
As a designer, I've always been intrigued by the idea of designing an API, The idea of creating a code endpoint that anyone...
Read more >
Butenafinum cheap
Ways to save on Casodex These programs and tips can help make your ... hydrochloride is a synthetic benzylamine antifungal agent.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found