Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Adding support for HttpHandlerDiagnosticListener. #18101

Merged
merged 6 commits into from
Apr 12, 2017
Merged

Conversation

brahmnes
Copy link
Contributor

@brahmnes brahmnes commented Apr 8, 2017

This listener compensates for the fact that .NET45/46 doesn't use DiagnosticSource. This solution attempts to recreate that by inserting into the Http stack and generating events.

This HttpHandlerDiagnosticListener is a singleton, and the bulk of its initialization happens when the first subscribe subscribes.
The way this listener injects inself into the Http stack is via reflection. More details are in the code documentation.

This listener compensates for the fact that .NET45/46 doesn't use DiagnosticSource. This solution attempts to recreate that by inserting into the Http stack and generating events.

This HttpHandlerDiagnosticListener is a singleton, and the bulk of its initialization happens when the first subscribe subscribes.
The way this listener injects inself into the Http stack is via reflection. More details are in the code documentation.
@dnfclas
Copy link

dnfclas commented Apr 8, 2017

@brahmnes,
Thanks for your contribution.
To ensure that the project team has proper rights to use your work, please complete the Contribution License Agreement at https://cla2.dotnetfoundation.org.

It will cover your contributions to all .NET Foundation-managed open source projects.
Thanks,
.NET Foundation Pull Request Bot

Guid loggingRequestId = Guid.Empty;

// TODO: Figure what's the right way to detect error conditions here.
if (response.StatusCode >= (HttpStatusCode)400)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think Exception event should be sent on >=400 status code.
Consumer can always use response object and status code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, removed the ExceptionEventName and the status code checking logic.

@@ -42,6 +42,8 @@ public static IObservable<DiagnosticListener> AllListeners
{
get
{
GC.KeepAlive(HttpHandlerDiagnosticListener.s_instance);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you probably want to do it only for net45/net46/netfx

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely put a #if NET45 || NET46 || NETFX around it.
Also put a comment indicating its purpose (on Desktop platforms to insure HttpHandlerDiagnosticListener.s_instance has been initialized, which in turn will insure that a DiagnosticListener for the HTTP requests is injected into the desktop framework using reflection).

This hook moved to be in the body of the if (s_allListenerObservable == null) below (it only has to be called one time).

into the framework for HTTP

Copy link
Contributor Author

@brahmnes brahmnes Apr 10, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not claiming we support NET45, since I am not going back to verify 4.5. I am just putting NET46 and NETFX on it.

using System.Reflection.Emit;

// This HttpHandlerDiagnosticListener class is applicable only for .NET 4.5/4.6, and not for .NET core.
// If you are making these changes, please test your changes manually via custom test applications.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does "these changes" in the 2nd sentence refer to?
Also where are the "custom test applications"? Can you provide a link in the comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I chatted with Vance and currently the test project only works with .NET core. For testing against .NET 45/46, the best option right now is manual tests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to have the tests here. It is only that we don't run the automatically (so you have to run them by hand).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to have the tests here. It is only that we don't run the automatically (so you have to run them by hand).

Doing this in the test folder will run the tests against netfx (Desktop):

"msbuild /t:rebuildandtest /p:TargetGroup=netfx"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can also this do (but you'll get other failures right now):

@dotnet-bot test outerloop netfx Windows_NT Debug Queues OuterLoop netfx Windows_NT Debug x64
@dotnet-bot test outerloop netfx Windows_NT Release Queues OuterLoop netfx Windows_NT Release x64

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the tip. Will add some test in my 3rd iteration.

{
/// <summary>
/// A HttpHandlerDiagnosticListener is a DiagnosticListener for .NET 4.5/4.6 where HttpClient
/// doesn't have a DiagnosticListener built-in. This class is not used for .NET Core because

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: built in. (two words, not hyphenated)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

{
Initialize();
return base.Subscribe(observer, isEnabled);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to add one more overload of Subscribe

    public IDisposable Subscribe(IObserver<KeyValuePair<string, object>> observer)

This is not obvious because it is part of the IObservable pattern, but you also want that one to call Initialize().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also noticed this subtlety. It means I would need to make this Subscribe method on DiagnosticListener virtual as well. Would that cause existing applications that calls Subscribe to fail? I don't know if the IL for virtual vs non-virtual method is different.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overload above IS virtual (you have to be virtual to override an interface method). It is simply a feature of C# that you don't have to be explicit about it. If you add the method (you don't need override) it will do what we want (which is override anyone calling the interface).

Copy link
Contributor Author

@brahmnes brahmnes Apr 10, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is actually not true.
I just ran a quick experiment:

using System;
namespace ConsoleApp4
{
public interface IMyInterface
{
void FuncA();
}
public class MyClass1 : IMyInterface
{
public void FuncA()
{
Console.WriteLine("From class 1");
}
}
public class MyClass2 : MyClass1
{
public void FuncA()
{
Console.WriteLine("From class 2");
}
}
class Program
{

    static void Main(string[] args)
    {
        IMyInterface o = new MyClass2();
        o.FuncA();
    }
}

}

It called MyClass1.FuncA, and not MyClass2.FuncA. If my experiment looks incorrect, let me know.

Copy link
Contributor

@vancem vancem Apr 10, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is OK to mark DiagnosticListener.Subscribe virtual.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a minute. In your example, MyClass2 doesn't implement (some people say "derive from") IMyInterface, so of course it doesn't work. You should instead get a warning from the compiler about hiding the original FuncA (requiring a 'new' if that's what's intended).

Copy link

@pharring pharring Apr 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took a look at DiagnosticListener's implementation and I see why it's tempting to make the 1-arg version of Subscribe virtual. But, as as I hinted at above, you don't actually need to: Just implement the IObservable<KeyValuePair<string, object>> on this class.

If we could go back and design DiagnosticListener again, I would propose we introduce a single protected virtual method called SubscribeCore which has the same signature as SubscribeInternal and, for the base class just calls SubscribeInternal. That way, implementations like this have just one method to override. (Actually, we could still do that without breaking any existing code).

Copy link
Contributor

@vancem vancem Apr 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First, I don't see any issue with Brahmnes suggestion of making Subscribe(IObserver) virtual. Given that doing this is trivial, this is the solution to beat.

I also toyed with the idea of simply implementing IObservable.Subscribe(Observer) on HttpHandlerDiagnosticListener but it has a subtle bug in that if an instance of HttpHandlerDiagnosticListener is typed as a DiagnosticListener (the common case), and you call Subscribe(IObserver) on it, then you don't get our override (it goes to the nonvirtual method defined on DiagnosticListener).

I don't think adding a protected SubscriberCore is warranted at this time. If we truly believe that people want to override subscribe of DiagnosticListeners, that would be one thing, but frankly that is not at all clear. The alternative (overriding all three overloads), is not really a big deal, and we can always do it later (as you point out).

/// that in .NET 4.5/4.6. HttpHandlerDiagnosticListener has no public constructor, but it has a
/// singleton style Initialize method to control its creation.
///
/// To use this, the application just needs to call HttpHandlerDiagnosticListener.Initialize(), and this
Copy link

@pharring pharring Apr 10, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I mentioned elsewhere, an application cannot call Initialize because it's private (and non-static). Also, please use <see cref="..." /> when referring to methods/types/etc.

[Edit: to escape xml doc comment]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the comment to explain how to initialize this object

/// singleton style Initialize method to control its creation.
///
/// To use this, the application just needs to call HttpHandlerDiagnosticListener.Initialize(), and this
/// will register itself with the DiagnosticListener's all listeners collection.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a cref for DiagnosticListener.AllListeners

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

/// </summary>
private void Initialize()
{
lock (this.initializationLock)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just go ahead and use lock(this) for this lock. It avoids another object allocation in the constructor of this DiagnosticListener and generally simplifies the code. We have been allergic to using lock(this) because historically we cared about inprocess hostile code (thus people outside might 'lock' us and force a deadlock) but we really don't care about that here.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I've mentioned elsewhere, you don't need a lock if you use static initialization.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, will change that.

}
catch (Exception)
{
// If anything went wrong, just no-op
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should go head and do a Write("ReflectionInitializationFailed", new { Exception = e }), so that errors are more noticeable. It cost us next to nothing, and could make debugging significantly easier.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, added.

/// threading issues. If Initialize has been called before, it will not doing anything.
/// </summary>
private void Initialize()
{

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know we've discussed this elsewhere, but I'd like to re-recommend that you make this static and do all the reflection/injection set-up in a static constructor. It doesn't have to be a static constructor on this type - it could be in a nested type, but it would remove the need for initializationLock.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just make sure that cure is not worse than the disease. Making a little function for each of the s_* fields that is used to initialize them (and thus become part of a nice big .cctor), is fine (because the code is arguably cleaner) But don't do unnatural things just to avoid a lock(this) {} here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am following Vance's suggestion of using lock(this), so the extra lock object can disappear. As for the lock(this) statement, since multiple threads can call Subscribe at the same time, I don't see how we can make this completely lock free.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CLR provides atomic initialization of class constructors. You don't need a lock if you use static initialization. Talk to me offline if you need help.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried and couldn't do it cleanly. Basically I would need to add an inner class with the static constructor. This inner class would have the static constructor. However, in the case of a failure, I cannot let any exceptions leaving the static constructor, so I need DiagnosticListener.Initialize() to check on some validity of some fields, to see if it's null (indication that it failed), and write the failure event with Write() if needed. However, to make sure I don't write multiple instances of this event, I need to know if this is the very first time, which puts me back to having a flag.

There may be a pattern to achieve this cleanly, but it's not something I can figure out in a sitting and this is not a critical blocker. Paul, if you have ideas or I am missing something, please feel free to suggest. We can add it in another iteration.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

/// It also intercepts all HttpWebRequest objects that are about to get removed from
/// Connection.m_WriteList as they have completed the request.
/// </summary>
private class HttpWebRequestArrayList : ArrayList

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sealed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

/// intercept each new Connection object being added to ConnectionGroup.m_ConnectionList
/// and replace its m_WriteList arraylist field.
/// </summary>
private class ConnectionArrayList : ArrayList

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sealed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

/// intercept each new ConnectionGroup object being added to ServicePoint.m_ConnectionGroupList
/// and replace its m_ConnectionList arraylist field.
/// </summary>
private class ConnectionGroupHashtable : Hashtable

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sealed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

internal static HttpHandlerDiagnosticListener s_instance = new HttpHandlerDiagnosticListener();

#region private fields
private const string DiagnosticListenerName = "System.Net.Http.Desktop.V4";
Copy link
Contributor

@vancem vancem Apr 10, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change the .Desktop.V4 suffix to .Net45. This is the correct name for the earliest desktop version that this code supports. We may end up making later versions of this (if we can't do it compatibly).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually removed the V4 suffix, so it's just called System.Net.Http.Desktop.
I think this is cleaner and not needing to version rev this. Let me know if you think this is not good enough.

// which allows us to intercept each new Connection object added under
// this ConnectionGroup.
ArrayList originalArrayList = s_connectionListField.GetValue(value) as ArrayList;
ConnectionArrayList newArrayList = new ConnectionArrayList();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add a ConnectionArrayList constructor that takes a System.Collections.ICollection parameter and then you can avoid the copy here. (Although you'll need a null check). I would also suggest overriding AddRange so you can make the constructor simpler. i.e.

private sealed class ConnectionArrayList : ArrayList
{
    public ConnectionArrayList(ICollection collection) : base(collection)
    {
    }

    public override void AddRange(ICollection collection)
    {
        // Implement in terms of overridden Add
    }

    public override int Add(etc...

}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am adding a hash table wrapper and an array list wrapper. The idea is that these wrapper are better with race conditions because they reference the same table/list as the underlying data structures. That change should also remove the need for copying.

// which allows us to intercept each new HttpWebRequest object added under
// this Connection.
ArrayList originalArrayList = s_writeListField.GetValue(value) as ArrayList;
HttpWebRequestArrayList newArrayList = new HttpWebRequestArrayList();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to my comment on initializing ConnectionArrayList, add a constructor that does the AddRange for you.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed, see above.

Copy link

@CIPop CIPop left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Please sign the CLA
  • The change is relying on internals of the HTTP managed stack that are not guaranteed to exist in future versions of .Net Framework.
  • There are no tests to prove this actually works.

// The caller should catch all exceptions and swallow.

// First step: Get all the reflection objects we will ever need.
Assembly systemNetHttpAssembly = typeof(ServicePoint).Assembly;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/cc @weshaggard, @davidsh, @karelz

There are several concerns here (aside from the unsigned CLA):

  1. The solution is based on reflection over .Net Framework internals that are not guaranteed to exist in the next versions.
  2. The code is separated from the .Net Framework code which is not public and so a test showing that System.Net changes may affect it is not possible.
  3. There are no tests to verify this even works.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will add tests in my 3rd iteration.

/// intercept each new ServicePoint object being added to ServicePointManager.s_ServicePointTable
/// and replace its ConnectionGroupList hashtable field.
/// </summary>
private class ServicePointHashtable : Hashtable

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sealed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@davidsh davidsh removed their assignment Apr 10, 2017
Added wrapper classes for hashtable and arraylist to address race condition concerns.
Renamed the DiagnosticSource to not have the V4 suffix
Also many other mini changes per feedback
@dnfclas
Copy link

dnfclas commented Apr 11, 2017

@brahmnes, thanks for signing the contribution license agreement. We will now validate the agreement and then the pull request.

Thanks, .NET Foundation Pull Request Bot

/// the application just needs to call <see cref="DiagnosticListener.AllListeners" /> and
/// <see cref="DiagnosticListener.AllListenerObservable.Subscribe(IObserver{DiagnosticListener})"/>,
/// then in the <see cref="IObserver{DiagnosticListener}.OnNext(DiagnosticListener)"/> method,
/// when it seems the System.Net.Http.Desktop source, subscribe to it. This will trigger the

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: 'sees' (not 'seems')

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

private class SimpleHashtableWrapper : Hashtable, IEnumerable
{
protected Hashtable _table;
public override int Count

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you allowed to use expression-bodied members in corefx? If so, these wrapper methods are all one-liners.
i.e.

public override int Count => this._table.Count;
public override bool IsReadOnly => this._table.IsReadOnly;
etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks nice. Don't know if I am allowed to use them.

}
internal SimpleHashtableWrapper(SerializationInfo info, StreamingContext context) : base(info, context)
{
this._table = (Hashtable)info.GetValue("ParentTable", typeof(Hashtable));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Introduce a private const string for "ParentTable"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

#region private helper classes

[Serializable]
private class SimpleHashtableWrapper : Hashtable, IEnumerable

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would just HashtableWrapper suffice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, fixed.

else
{
newTable = new ConnectionGroupHashtable(new Hashtable());
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lines 240-246 could be a one-liner:

ConnectionGroupHashtable newTable = new ConnectionGroupHashtable(originalTable ?? new Hashtable());

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

/// </summary>
private void Initialize()
{
lock (this)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest: Use double-checked locking and handle the exception outside the lock. Like this:

private static Exception PrepareReflection()
{
    try
    {
        PrepareReflectionObjects(); 
        PerformInjection(); 
        return null;
    }
    catch (Exception ex)
    {
        return ex;
    }
}

private void Initialize()
{
    if (this.initialized)
    {
        return;
    }

    Exception ex;

    lock (this)
    {
        if (this.initialized)
        {
            return;
        }

        // This flag makes sure we only do this once. Even if we failed to initialize in an 
        // earlier time, we should not retry because this initialization is not cheap and 
        // the likelihood it will succeed the second time is very small. 
        this.initialized = true; 
        ex = PrepareReflection(); 
    }

    // If anything went wrong, just no-op. Write an event so at least we can find out. 
    if (ex != null) 
    {
        this.Write(InitializationFailed, new { Exception = ex });
    }
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not clear why doing the Write outside the lock is important, and it does complicate the code.

However thinking about it the Write() call is not useful and we should probably just get rid of it. The problem is that we KNOW that this code happen JUST BEFORE the first SUBSCRIBE to this DiagnosticListener. When at that point there are GUARENTEED to be no subscribers, so the Write HAS to do nothing.

Fixing this would require remembering the exception and logging it when the subscribers come along. I don't think this is worth it as the exception was already thrown, so we do have SOME indication that something went wrong, and it is not clear that having the DiagnosticSource tell us is that important (we can add it later if we find it is useful, but frankly this kind of thing is best effort).

Thus I think we should just catch and swallow the exception for now (Brahmnes original code).

Copy link
Contributor Author

@brahmnes brahmnes Apr 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realize the other simple possibility is reverse the order. It's weird, but less so, and the code is simpler.

public override IDisposable Subscribe(IObserver<KeyValuePair<string, object>> observer, Predicate<string> isEnabled)
{
    IDisposable result = base.Subscribe(observer, isEnabled);
    try
    {
        Initialize();
    }
    catch (Exception ex)
    {
        // If anything went wrong, just no-op. Write an event so at least we can find out.
        this.Write(InitializationFailed, new { Exception = ex });
    }

    return result;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I saw Paul mention this. You should put the try-catch in Initialize (avoid code cloning).

I do like the simple solution to Initialize after Subscribing however.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Made the 3rd DiagnosticListener.Subscribe method virtual and override it
Added tests
using System.Runtime.Serialization;

// This HttpHandlerDiagnosticListener class is applicable only for .NET 4.6, and not for .NET core.
// If you are making these changes, please test your changes manually via custom test applications.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is still confusing. What are "these changes"? Where are the "custom test applications"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Outdated comment. Fixed.

public override IDisposable Subscribe(IObserver<KeyValuePair<string, object>> observer, Predicate<string> isEnabled)
{
IDisposable result = base.Subscribe(observer, isEnabled);
try

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't the try/catch be done inside Initialize? Reduce code duplication.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

{
}

private void RaiseRequestEvent(HttpWebRequest request)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to understand why we are not using activities and trying to make this look as much like the .NET Core case as possible. Same goes for the timestamp (We probably need to a 'Activity.UtcNow method that on .NET Core just rebounds to DateTime.UtcNow but on V4.X uses Stopwatch to make high res UtcNow (it is now hard, I can give you code if you like).

Don't we want be extracting and inserting IDs/Baggage (again like .NET Core?)

@lmolkova

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can speak for this. :)
Since this is not a long term solution, we are taking the approach of getting just the hooks in place here and leaving the activity logic in the caller. There are two reasons:

  1. For a single HttpWebRequest, it actually triggers multiple send and receive events. An example is HTTP redirection. DiagnosticSource doesn't have full information to deduplicate these events. Application Insights however, has enough states to handle duplicates and log only one AI event.
  2. Application Insights has a path handling cases where user uses profiler, so it handles all the activity stuff already. It's far easier to share that code path with this new path.

In the long term, we can look at adding activity logic here. However, I kind of like to first see how well this change goes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given this all is 'hack' code that in theory goes away, and is 'agile (it is relatively easy to update this and get it deployed), I guess I could live with this. Still I think we overweighting the 'App Insights' scenario here. Pretty much ANYONE using this diagnostic source (e.g. Glimpse etc) who cares about HTTP is likely to want the ID passing logic, so that argues that we should do it once for everyone here. Also if we update this logic in the HttpClient (e.g. add interoperability with OpenTracing features (exactly what header name is used ...)), we could simply add that logic here as well and have a complete feature that works on .NET Core AND .NET Desktop. If we delegate to the client of this hook, any updates to HttpClient need coordination with at least the AI team and probably more. Feels like a mess.

For things like ID and Baggage passing, de-duping is easy (if it is there already do nothing), so it seems like we should at least be doing that.

Comments?

Copy link

@lmolkova lmolkova Apr 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that having the same logic here and in HttpClient is a good thing. De-duplication could be based on the presence of headers in the request.
Events names then should be renamed to be consistent with HttpClient, the whole IsEnabled flow also goes here.

The problem is that the change to AppInsights (that consumes the hook) was done in the last minute before the code freeze and now it is probably too late to do any changes there.

I see following options:
1. Do the right thing here, in DiagnosticSource, make it fully repeat HttpClient instrumentation on corefx: create activity and inject headers.
It requires some minor changes in AppInsights @brahmnes, we need to check if AI team is fine allowing us to do this as a bugfix. If they are fine, I can finish this change and fix AI.

If we don't do it now, we either

  • leave current behavior (no activities, consumer injects headers) forever
  • OR have to deal with events deprecation later (e.g. in the next major DiagnosticSource version)

2. Do not have this hook in DiagnosticSource for now, keep it in AppInsights (it is copy-pasted for testing). Add the hook in the next major release of DiagnosticSource, make some future AppInsights release compatible with it and remove hook from AppInsights.
However there is an agreement with AI team that the the hook will be moved to DiagnosticSource.

3. Leave current behavior, but as i mentioned before we either have to live with this forever or deal with deprecation problems later.

I strongly prefer option 1 as long as AppInsights team agree on taking fix for this release.

Otherwise, option 2 is not that bad: only AI knows how to properly use the hook and it is custom for it, so it lives in AI codebase.

@brahmnes could you discuss this options with AI team?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think option 1 is fine. We need to make a change to remove the temporary diagnosticlistener in there anyways, we can bundle this with that change. People are usually comfortable if you give good explanations and it's code deletion and not code addition.
Regarding Vance's comment about it being overweighted on Application Insights, I don't think that is true. Outside of correlation ID support, which is the motivation we have, the feature is merely notifying you when HttpWebRequest is about to send and receive. I think it can stand on its own.
Liudmila can look at how to add activity support. Nevertheless, I think we should get this change in first, and then make more changes on top of it.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vancem
Would you be ok with the following plan?

  1. Merge this change (unless there are other open issues, except activity support)
  2. I will submit another PR with Activity sometime soon (this or next week) that will add activities and header injection.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am OK with @lmolkova s plan.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks! Could you please merge it?

…ght files.

Fixed issues in the test code.
Removed the serialization attribute for HashtableWrapper because .NET didn't allow it at runtime due to security reasons.
Fixed a few issues discovered in product code.
using System.Reflection.Emit;
using System.Runtime.Serialization;

// This HttpHandlerDiagnosticListener class is applicable only for .NET 4.6, and not for .NET core.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about:

#if !NET46
#error This is for .NET 4.6 only.
#endif

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The csproj file already conditionally include this file, so the #error is probably redundant.
The truth is this will be needed for .NET 4.7 since .NET 4.7 has released. I also changed the comment to say .NET 4.6 and above. In the future, when we have a better solution implemented natively in HttpClient, we should revisit this component and change.

{
}

private const string ParentTableSerializationField = "ParentTable";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually used now? Looks like serialization isn't implemented.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Argh, I missed this. Good catch.
Fixed now, and removed both this const and the OnDeserialization method.

@vancem vancem merged commit 7aac98a into dotnet:master Apr 12, 2017
@karelz karelz modified the milestone: 2.0.0 Apr 15, 2017
macrogreg pushed a commit to open-telemetry/opentelemetry-dotnet-instrumentation that referenced this pull request Sep 24, 2020
Adding support for HttpHandlerDiagnosticListener.

Commit migrated from dotnet/corefx@7aac98a
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants