Skip to content
This repository has been archived by the owner on Jun 10, 2020. It is now read-only.

Add EntityFramework diagnostics source #297

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
namespace Microsoft.ApplicationInsights.AspNetCore.DiagnosticListeners.Implementation
{
using System;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.Extensions.DiagnosticAdapter;

/// <summary>
/// <see cref="IApplicationInsightDiagnosticListener"/> implementation that listens for events specific to EntiryFrameworkCore.
/// </summary>
public class EntityFrameworkDiagnosticListener : IApplicationInsightDiagnosticListener
{
private readonly TelemetryClient client;
private readonly ContextData<long> beginDependencyTimestamp = new ContextData<long>();

/// <inheritdoc />
public string ListenerName { get; } = "Microsoft.EntityFrameworkCore";
Copy link
Member

@dnduffy dnduffy Dec 13, 2016

Choose a reason for hiding this comment

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

nitpick: doc comment "/// Gets the listener name."
And doc comments elsewhere.


/// <summary>
/// Initializes a new instance of the <see cref="T:HostingDiagnosticListener"/> class.
/// </summary>
/// <param name="client"><see cref="TelemetryClient"/> to post traces to.</param>
public EntityFrameworkDiagnosticListener(TelemetryClient client)
{
this.client = client;
}

/// <summary>
/// Diagnostic event handler method for 'Microsoft.EntityFrameworkCore.BeforeExecuteCommand' event
/// </summary>
[DiagnosticName("Microsoft.EntityFrameworkCore.BeforeExecuteCommand")]
public void OnBeginCommand(long timestamp)
{
beginDependencyTimestamp.Value = timestamp;
}

/// <summary>
/// Diagnostic event handler method for 'Microsoft.EntityFrameworkCore.AfterExecuteCommand' event
/// </summary>
[DiagnosticName("Microsoft.EntityFrameworkCore.AfterExecuteCommand")]
public void OnEndCommand(IDbCommand command, string executeMethod, Guid instanceId, long timestamp, bool isAsync)
{
var start = beginDependencyTimestamp.Value;
var end = timestamp;

var telemetry = new DependencyTelemetry();
Copy link
Contributor

Choose a reason for hiding this comment

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

How does it work with SQL dependencies collected by DependencyCollector module? Will we have double counting?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Only for SqlClient on full framework.

Copy link
Member

Choose a reason for hiding this comment

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

That would be a problem.

Copy link
Contributor

Choose a reason for hiding this comment

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

Options? Only enable this feature for .NET Core? Double counting is not good

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is useful for non SqlClient connection on full framework too. We can try and filter out SqlCommands by type.

telemetry.Name = command.Connection.Database;
Copy link
Contributor

Choose a reason for hiding this comment

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

Name should be a name of stored procedure or friendly name of SQL command. Not the database name

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Framework only sets command text as a name for stored procedures. We have quite strange logic for full framework: https://github.com/Microsoft/ApplicationInsights-dotnet-server/blob/develop/Src/DependencyCollector/Shared/Implementation/ProfilerSqlProcessing.cs#L144

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So it's, Server | Database | StoredProcedure for stored procedures and Server | Database for everything else. But DataSource unfortunately is SqlClient specific.

telemetry.Data = command.CommandText;
telemetry.Duration = new TimeSpan(end - start);
telemetry.Timestamp = DateTimeOffset.Now - telemetry.Duration;
Copy link
Member

Choose a reason for hiding this comment

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

We had a discussion the other day that we should probably go through our code and replace all instances of DateTimeOffset.Now with DateTimeOffset.UtcNow because there happens to be a perf difference between them so maybe this one should be updated now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You want me to update all occurrences?

Copy link
Member

Choose a reason for hiding this comment

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

I was just thinking of this one that is new, or you could leave it and I'll get it with all the others, probably safe to leave it. Sorry, I just commented on it when I saw it but probably I should do it with the others all at once so don't worry about it.

telemetry.Target = command.Connection.Database;
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we have Server name? Please include it into target

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, because this is provider agnostic there is no common way to get a server name.

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we make our best attempt? Like parse connection string or something. We want to light up scenario in UI when we open database information from the application map's SQL node

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can send entire connection string but it might contain secrets. We can parse connection string syntax but we don't know what to search for.

telemetry.Type = "SQL";
client.TrackDependency(telemetry);
}

/// <summary>
/// Proxy interface for <c>IDbCommand</c> from System.Data
/// </summary>
public interface IDbCommand
{
string CommandText { get; }

IDbConnection Connection { get; }
}

/// <summary>
/// Proxy interface for <c>IDbConnection</c> from System.Data
/// </summary>
public interface IDbConnection
{
string Database { get; }
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------


namespace Microsoft.ApplicationInsights.AspNetCore.Extensibility.Implementation.Tracing
{
using System;
using System.Diagnostics.Tracing;
using System.Reflection;

/// <summary>
/// Event source for Application Insights ASP.NET Core SDK.
Expand All @@ -28,7 +31,7 @@ private AspNetCoreEventSource() : base()
{
try
{
this.ApplicationName = Microsoft.Extensions.PlatformAbstractions.PlatformServices.Default.Application.ApplicationName;
this.ApplicationName = Assembly.GetEntryAssembly()?.GetName().Name;
}
catch (Exception exp)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.AspNetCore;
using Microsoft.ApplicationInsights.AspNetCore.DiagnosticListeners;
using Microsoft.ApplicationInsights.AspNetCore.DiagnosticListeners.Implementation;
using Microsoft.ApplicationInsights.AspNetCore.Extensions;
using Microsoft.ApplicationInsights.AspNetCore.TelemetryInitializers;
using Microsoft.ApplicationInsights.Extensibility;
Expand Down Expand Up @@ -120,6 +121,7 @@ public static IServiceCollection AddApplicationInsightsTelemetry(this IServiceCo
services.AddSingleton<ApplicationInsightsInitializer, ApplicationInsightsInitializer>();
services.AddSingleton<IApplicationInsightDiagnosticListener, HostingDiagnosticListener>();
services.AddSingleton<IApplicationInsightDiagnosticListener, MvcDiagnosticsListener>();
services.AddSingleton<IApplicationInsightDiagnosticListener, EntityFrameworkDiagnosticListener>();

// Using startup filter instead of starting DiagnosticListeners directly because
// AspNetCoreHostingDiagnosticListener injects TelemetryClient that injects TelemetryConfiguration
Expand Down
6 changes: 5 additions & 1 deletion src/Microsoft.ApplicationInsights.AspNetCore/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,15 @@
"type": "build"
},
"Microsoft.ApplicationInsights": "2.2.0",
"Microsoft.AspNetCore.Hosting": "1.0.0",
"Microsoft.AspNetCore.Hosting.Abstractions": "1.0.0",
"Microsoft.AspNetCore.Http": "1.0.0",
"Microsoft.Extensions.Configuration": "1.0.0",
"Microsoft.Extensions.Configuration.Json": "1.0.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
"Microsoft.Extensions.DiagnosticAdapter": "1.0.0",
"Microsoft.Extensions.Logging.Abstractions": "1.0.0",
"Microsoft.Extensions.Options": "1.0.0",
"Microsoft.Net.Http.Headers": "1.0.0",
"Microsoft.CodeAnalysis.Analyzers": {
"version": "1.2.0-beta2",
"type": "build"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace MVCFramework45.FunctionalTests.FunctionalTest
{
using System.Linq;
using System.Net.Http;
using FunctionalTestUtils;
using Microsoft.ApplicationInsights.DataContracts;
using Xunit;

public class EntityFrameworkTelemetryTests : TelemetryTestsBase
{
private const string assemblyName = "MVCFramework45.FunctionalTests";

[Fact]
public void TestEntityFrameworkTelemetryItemsReceived()
{
InProcessServer server;
using (server = new InProcessServer(assemblyName, InProcessServer.UseApplicationInsights))
{
using (var httpClient = new HttpClient())
{
var task = httpClient.GetAsync(server.BaseHost + "/Home/Contact");
task.Wait(TestTimeoutMs);
}
}
DependencyTelemetry[] telemetries = server.BackChannel.Buffer.OfType<DependencyTelemetry>()
.Where(t => t.Type == "SQL" && t.Target == "aspnet-MVCFramework45.FunctionalTests-60cfc765-2dc9-454c-bb34-dc379ed92cd0")
.ToArray();

Assert.True(telemetries.Length >= 2);
Assert.All(telemetries, telemetry =>
{
Assert.StartsWith("SELECT ", telemetry.Data);
Assert.Equal(telemetry.Target, telemetry.Name);
});
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
using System.Threading;
using System.Threading.Tasks;

namespace SampleWebAppIntegration.FunctionalTest
namespace SampleWebAppIntegration.FunctionalTest
{
using System.Linq;
using System.Net.Http;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
namespace Microsoft.ApplicationInsights.AspNetCore.Tests
{
using System;
using System.Data;
using System.Diagnostics;
using Microsoft.ApplicationInsights.AspNetCore.DiagnosticListeners.Implementation;
using Microsoft.ApplicationInsights.AspNetCore.Tests.Helpers;
using Microsoft.ApplicationInsights.DataContracts;
using Xunit;

public class EntityFrameworkDiagnosticListenerTest
{
private const string TestListenerName = "TestListener";

private DependencyTelemetry sentTelemetry;

private DiagnosticListener telemetryListener;

public EntityFrameworkDiagnosticListenerTest()
{
var listener = new EntityFrameworkDiagnosticListener(CommonMocks.MockTelemetryClient(telemetry => this.sentTelemetry = (DependencyTelemetry)telemetry));
telemetryListener = new DiagnosticListener(TestListenerName);
telemetryListener.SubscribeWithAdapter(listener);
}

[Fact]
public void FillsTelemetryProperties()
{
var datetime = DateTimeOffset.Now;
var timestamp = Stopwatch.GetTimestamp();
telemetryListener.Write("Microsoft.EntityFrameworkCore.BeforeExecuteCommand",
new { timestamp });
telemetryListener.Write("Microsoft.EntityFrameworkCore.AfterExecuteCommand",
new
{
timestamp = timestamp + 100000,
command = new MockDbCommand()
{
Connection = new MockDbConnection()
{
Database = "Database name"
},
CommandText = "Command text"
}
});

Assert.Equal("Database name", sentTelemetry.Name);
Assert.Equal("Command text", sentTelemetry.Data);
Assert.Equal("SQL", sentTelemetry.Type);
Assert.True(datetime < sentTelemetry.Timestamp);
Assert.Equal(new TimeSpan(100000), sentTelemetry.Duration);
}


/// <summary>
/// Mock class for <see cref="IDbCommand"/> interface.
/// </summary>
public class MockDbCommand : System.Data.IDbCommand
Copy link
Member

Choose a reason for hiding this comment

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

nitpick: I know it's test code but I would love doc comments anyway please.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

do you want doc comment for members too?

{
public string CommandText { get; set; }

public int CommandTimeout { get; set; }

public CommandType CommandType { get; set; }

public System.Data.IDbConnection Connection { get; set; }

public IDataParameterCollection Parameters { get; set; }

public IDbTransaction Transaction { get; set; }

public UpdateRowSource UpdatedRowSource { get; set; }

public void Cancel()
{
throw new NotImplementedException();
}

public IDbDataParameter CreateParameter()
{
throw new NotImplementedException();
}

public void Dispose()
{
throw new NotImplementedException();
}

public int ExecuteNonQuery()
{
throw new NotImplementedException();
}

public IDataReader ExecuteReader()
{
throw new NotImplementedException();
}

public IDataReader ExecuteReader(CommandBehavior behavior)
{
throw new NotImplementedException();
}

public object ExecuteScalar()
{
throw new NotImplementedException();
}

public void Prepare()
{
throw new NotImplementedException();
}
}

/// <summary>
/// Mock class for <see cref="IDbConnection"/> interface.
/// </summary>
public class MockDbConnection : System.Data.IDbConnection
{
public void Dispose()
{
throw new NotImplementedException();
}

public IDbTransaction BeginTransaction()
{
throw new NotImplementedException();
}

public IDbTransaction BeginTransaction(IsolationLevel il)
{
throw new NotImplementedException();
}

public void Close()
{
throw new NotImplementedException();
}

public IDbCommand CreateCommand()
{
throw new NotImplementedException();
}

public void Open()
{
throw new NotImplementedException();
}

public string ConnectionString { get; set; }

public int ConnectionTimeout { get; set; }

public string Database { get; set; }

public ConnectionState State { get; set; }

public void ChangeDatabase(string databaseName)
{
throw new NotImplementedException();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,14 @@
"Microsoft.NETCore.App": {
"type": "platform",
"version": "1.0.1"
}
},
"System.Data.Common": "4.1.0"
}
},
"net451": {
"frameworkAssemblies": {
"System.Data": ""
}
}
}
}