New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix/db spans not finishing #2398
Conversation
We won't be able to take a package dependency on If all we need is the private Guid? ConnectionId =>
DiagnosticSourceValue?.GetType().FullName == "Microsoft.EntityFrameworkCore.Diagnostics.ConnectionEventData"
? DiagnosticSourceValue.GetGuidProperty("ConnectionId")
: null; |
... Though I'm surprised that we can't get this value directly from diagnostics data. Are you sure it isn't exposed some other way? |
If we do have to resort to reflection, then that is ok for now, but would be a strong reason to make a separate |
Aha... makes sense.
The only place I've seen it is in the Can you think of somewhere else we'd get this?
The reflection idea is smart... that means our solution will work for all target frameworks as well! The values we need are:
Just have to be cautious that those types may have changed over time between different releases of EF - which we don't detect via static typing when using Reflection. Need to make sure we test it with different versions of EF then (or dig through the history of that repo on GitHub to check if there have been any changes that would break our reflection). |
src/Sentry.DiagnosticSource/Internal/DiagnosticSource/EFDiagnosticSourceHelper.cs
Outdated
Show resolved
Hide resolved
src/Sentry.DiagnosticSource/Internal/DiagnosticSource/SentryEFCoreListener.cs
Outdated
Show resolved
Hide resolved
src/Sentry.DiagnosticSource/Internal/DiagnosticSource/EFCommandDiagnosticSourceHelper.cs
Outdated
Show resolved
Hide resolved
src/Sentry.DiagnosticSource/Internal/DiagnosticSource/EFCommandDiagnosticSourceHelper.cs
Outdated
Show resolved
Hide resolved
src/Sentry.DiagnosticSource/Internal/DiagnosticSource/EFConnectionDiagnosticSourceHelper.cs
Outdated
Show resolved
Hide resolved
src/Sentry.DiagnosticSource/Internal/DiagnosticSource/EFCommandDiagnosticSourceHelper.cs
Outdated
Show resolved
Hide resolved
src/Sentry.DiagnosticSource/Internal/DiagnosticSource/EFConnectionDiagnosticSourceHelper.cs
Show resolved
Hide resolved
src/Sentry.DiagnosticSource/Internal/DiagnosticSource/EFQueryCompilerDiagnosticSourceHelper.cs
Outdated
Show resolved
Hide resolved
src/Sentry.DiagnosticSource/Internal/DiagnosticSource/EFQueryCompilerDiagnosticSourceHelper.cs
Outdated
Show resolved
Hide resolved
src/Sentry.DiagnosticSource/Internal/DiagnosticSource/EFDiagnosticSourceHelper.cs
Outdated
Show resolved
Hide resolved
src/Sentry.DiagnosticSource/Internal/DiagnosticSource/EFDiagnosticSourceHelper.cs
Outdated
Show resolved
Hide resolved
src/Sentry.DiagnosticSource/Internal/DiagnosticSource/SentryEFCoreListener.cs
Outdated
Show resolved
Hide resolved
src/Sentry.DiagnosticSource/Internal/DiagnosticSource/EFCommandDiagnosticSourceHelper.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See feedback inline. Thanks.
The approach seems sound. Thanks for the work here! Looking at the screenshot, it seems we are now placing connections and commands side-by-side in a sibling relationship rather than parent-child. That's good, I think. However I'm not sure why it looks like we have a different connection ID every time. If it's connection pooling, then shouldn't the connection ID be the same on each one? And if so, then we could eliminate the extra ones and just show one connection span at the top with all the related command spans under it? Also, what's this gap about? |
src/Sentry.DiagnosticSource/Internal/DiagnosticSource/EFDiagnosticSourceHelper.cs
Outdated
Show resolved
Hide resolved
src/Sentry.DiagnosticSource/Internal/DiagnosticSource/EFDiagnosticSourceHelper.cs
Outdated
Show resolved
Hide resolved
Just to clarify on my previous comment... We used to have:
... and the connections were hanging open. Now it's better with:
But what we're aiming for is more like:
... where the same connection id is used for the three queries (via pooling), and the logical connection span ends when all the queries end - even if the connection is still technically open, but returned to the pool. |
Co-authored-by: Matt Johnson-Pint <matt.johnson-pint@sentry.io>
…ntry/sentry-dotnet into fix/db-spans-not-finishing
True... that was intentional ;-) The
Yeah, I think we can do that. Looking at what comes through to Sentry, all of the connections have the same ConnectionId. Something to look at as part of #2144 I think... unless you wanted me to tackle both of those issues in the same pull request. |
Thanks!!! |
Instructions and example for changelogPlease add an entry to Example: ## Unreleased
- Fix/db spans not finishing ([#2398](https://github.com/getsentry/sentry-dotnet/pull/2398)) If none of the above apply, you can opt out of this check by adding |
Root cause
Sentry listens for EF related diagnostic data in the
SentryEFCoreListener
class. Typically those events come in pairs - e.g."Microsoft.EntityFrameworkCore.Database.Connection.ConnectionOpening"
and"Microsoft.EntityFrameworkCore.Database.Connection.ConnectionClosed"
.Previously, when we received one of the "opening" events we added a span to the
TransactionTrace
and stored a weak reference to this inSentryEFCoreListener
(usingAsyncLocal<WeakReference<ISpan>>
). However for some reason theConnectionClosed
event was causing the thread context to change, so in the context that those events were processed, theAsyncLocal
would be holding a null reference... meaning we couldn't track down the originalISpan
and finish it.High level solution
We no longer store references to the spans that get added in the
SentryEFCoreListener
class. Instead we use the correlation ids that are provided with the diagnostic events.The
ConnectionOpening
event contains aConnectionId
that we store inISpan.Extra
.ConnectionClosed
events also contain aConnectionId
, so when we receive these, we can extract this, find the span with the matchingConnectionId
and finish it.For queries, the solution is very similar except that the correlation id is called
CommandId
.Note: For
QueryCompilationStarting
,QueryModelCompiling
andQueryExecutionPlanned
there is no appropriate correlation id and it also wasn't possible to use the description of theQueryExpressionEventData
(since this differs between the opening and closing events). In that specific instance, we simply run a FIFO... so the first expressions to start being compiled are assumed to be the first to finish.SQL Listener
The SQL Listener implementation was already using the correlation id technique so didn't suffer from this problem. I haven't touched that. It's worth noting that the implementation of
SentrySqlListener
is slightly different in that it all sits in a single class... whereas for Entity Framework, the logic has been factored out into helper classes. That's because I built most of the solution for the problem with EF before I checked how it was done inSentrySqlListener
. In hindsight, that was a mistake... If we think this is a major issue, we could rewrite one or the other of these implementations so that they both used the same code pattern.Result
DB Spans are now finishing correctly!