Diagnostics: Initial commit of new EF.AppInsights package.

- Sets up the product/test projects and enables simple AI dependency forwarding of EF generated ADO events including EF correlation ids.

TBD:

- DbContext correlation? NB: ASP.NET AI integration already correlates everything at the Request level.
- Forward other events? NB. ASP.NET AI integration already forwards all logging events as AI Events and all errors as AI Errors, so it is unclear what we should do here.
This commit is contained in:
Andrew Peters 2017-05-04 16:12:37 -07:00
Родитель 6b336aa4ac
Коммит 99f322920e
11 изменённых файлов: 487 добавлений и 10 удалений

Просмотреть файл

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26228.4
VisualStudioVersion = 15.0.26403.7
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCore", "src\EFCore\EFCore.csproj", "{715C38E9-B2F5-4DB2-8025-0C6492DEBDD4}"
EndProject
@ -81,6 +81,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-ef.Tests", "test\dot
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ef.Tests", "test\ef.Tests\ef.Tests.csproj", "{935B51B9-A9B9-4DA2-93A2-663D3BCEAA83}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCore.ApplicationInsights.Tests", "test\EFCore.ApplicationInsights.Tests\EFCore.ApplicationInsights.Tests.csproj", "{55B7DC7C-A2B0-4181-9EFF-DECFBE83602D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCore.ApplicationInsights", "src\EFCore.ApplicationInsights\EFCore.ApplicationInsights.csproj", "{51F53552-AEC3-4CF5-93D3-DB15DEAA39EA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -235,6 +239,14 @@ Global
{935B51B9-A9B9-4DA2-93A2-663D3BCEAA83}.Debug|Any CPU.Build.0 = Debug|Any CPU
{935B51B9-A9B9-4DA2-93A2-663D3BCEAA83}.Release|Any CPU.ActiveCfg = Release|Any CPU
{935B51B9-A9B9-4DA2-93A2-663D3BCEAA83}.Release|Any CPU.Build.0 = Release|Any CPU
{55B7DC7C-A2B0-4181-9EFF-DECFBE83602D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{55B7DC7C-A2B0-4181-9EFF-DECFBE83602D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{55B7DC7C-A2B0-4181-9EFF-DECFBE83602D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{55B7DC7C-A2B0-4181-9EFF-DECFBE83602D}.Release|Any CPU.Build.0 = Release|Any CPU
{51F53552-AEC3-4CF5-93D3-DB15DEAA39EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{51F53552-AEC3-4CF5-93D3-DB15DEAA39EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{51F53552-AEC3-4CF5-93D3-DB15DEAA39EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{51F53552-AEC3-4CF5-93D3-DB15DEAA39EA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -277,5 +289,7 @@ Global
{31ED3EA7-8270-478D-935D-0067BD7935B7} = {CE6B50B2-34AE-44C9-940A-4E48C3E1B3BC}
{27018CE2-C235-439C-80F2-C573C8904892} = {258D5057-81B9-40EC-A872-D21E27452749}
{935B51B9-A9B9-4DA2-93A2-663D3BCEAA83} = {258D5057-81B9-40EC-A872-D21E27452749}
{55B7DC7C-A2B0-4181-9EFF-DECFBE83602D} = {258D5057-81B9-40EC-A872-D21E27452749}
{51F53552-AEC3-4CF5-93D3-DB15DEAA39EA} = {CE6B50B2-34AE-44C9-940A-4E48C3E1B3BC}
EndGlobalSection
EndGlobal

Просмотреть файл

@ -1,5 +1,6 @@
<Project>
<PropertyGroup>
<AppInsightsVersion>2.4.0-*</AppInsightsVersion>
<AspNetCoreVersion>2.0.0-*</AspNetCoreVersion>
<CoreFxVersion>4.3.0</CoreFxVersion>
<DependencyModelVersion>2.0.0-*</DependencyModelVersion>

Просмотреть файл

@ -0,0 +1,277 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using JetBrains.Annotations;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Utilities;
namespace Microsoft.EntityFrameworkCore.ApplicationInsights
{
/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class DiagnosticEventForwarder : IObserver<DiagnosticListener>, IDisposable
{
private const string DependencyTypeName = "SQL";
private readonly object _sync = new object();
private readonly TelemetryClient _telemetryClient;
private IDisposable _efSubscription;
private IDisposable _listenerSubscription;
/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public DiagnosticEventForwarder([NotNull] TelemetryClient telemetryClient)
{
Check.NotNull(telemetryClient, nameof(telemetryClient));
_telemetryClient = telemetryClient;
}
/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual void Dispose()
{
lock (_sync)
{
_efSubscription?.Dispose();
_efSubscription = null;
_listenerSubscription?.Dispose();
_listenerSubscription = null;
}
}
void IObserver<DiagnosticListener>.OnNext(DiagnosticListener diagnosticListener)
{
if (diagnosticListener.Name == LoggerCategory.Root)
{
lock (_sync)
{
_efSubscription = diagnosticListener.Subscribe(new EventObserver(_telemetryClient));
}
}
}
void IObserver<DiagnosticListener>.OnCompleted()
{
}
void IObserver<DiagnosticListener>.OnError(Exception exception)
{
}
/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual void Start()
{
lock (_sync)
{
if (_listenerSubscription == null)
{
_listenerSubscription = DiagnosticListener.AllListeners.Subscribe(this);
}
}
}
private sealed class EventObserver : IObserver<KeyValuePair<string, object>>
{
private readonly TelemetryClient _telemetryClient;
public EventObserver(TelemetryClient telemetryClient) => _telemetryClient = telemetryClient;
public void OnNext(KeyValuePair<string, object> value)
{
if (!_telemetryClient.IsEnabled())
{
return;
}
var eventName = value.Key;
var eventData = value.Value;
Debug.Assert(eventName.StartsWith(LoggerCategory.Root, StringComparison.Ordinal));
Debug.Assert(eventData != null);
switch (eventData)
{
case CommandErrorData commandErrorData:
{
TrackCommandDependency(eventName, commandErrorData, false);
break;
}
case CommandExecutedData commandExecutedData:
{
TrackCommandDependency(eventName, commandExecutedData, true);
break;
}
case CommandEndData commandEndData:
{
TrackCommandDependency(eventName, commandEndData, true);
break;
}
case ConnectionErrorData connectionErrorData:
{
TrackConnectionDependency(eventName, connectionErrorData, false);
break;
}
case ConnectionEndData connectionEndData:
{
TrackConnectionDependency(eventName, connectionEndData, true);
break;
}
case TransactionErrorData transactionErrorData:
{
TrackTransactionDependency(eventName, transactionErrorData, false);
break;
}
case TransactionEndData transactionEndData:
{
TrackTransactionDependency(eventName, transactionEndData, true);
break;
}
case DataReaderDisposingData dataReaderDisposingData:
{
TrackReaderDependency(eventName, dataReaderDisposingData);
break;
}
}
}
private void TrackCommandDependency(
string eventName, CommandEndData commandEndData, bool success)
{
var command = commandEndData.Command;
var dependencyTelemetry
= new DependencyTelemetry(
DependencyTypeName,
command.Connection.Database,
eventName,
command.CommandText,
commandEndData.StartTime,
commandEndData.Duration,
success: success,
resultCode: null);
var properties = dependencyTelemetry.Properties;
properties[nameof(commandEndData.ConnectionId)] = commandEndData.ConnectionId.ToString();
properties[nameof(commandEndData.CommandId)] = commandEndData.CommandId.ToString();
properties[nameof(commandEndData.IsAsync)] = commandEndData.IsAsync.ToString();
properties[nameof(commandEndData.ExecuteMethod)] = commandEndData.ExecuteMethod.ToString();
properties[nameof(command.CommandText)] = command.CommandText;
_telemetryClient.TrackDependency(dependencyTelemetry);
}
private void TrackConnectionDependency(
string eventName, ConnectionEndData connectionEndData, bool success)
{
var connection = connectionEndData.Connection;
var dependencyTelemetry
= new DependencyTelemetry(
DependencyTypeName,
connectionEndData.Connection.Database,
eventName,
$"[{nameof(connection.Database)}='{connection.Database}',"
+ $" {nameof(connection.DataSource)}='{connection.DataSource}',"
+ $" {nameof(connection.ConnectionTimeout)}={connection.ConnectionTimeout}]",
connectionEndData.StartTime,
connectionEndData.Duration,
success: success,
resultCode: null);
var properties = dependencyTelemetry.Properties;
properties[nameof(connectionEndData.ConnectionId)] = connectionEndData.ConnectionId.ToString();
properties[nameof(connectionEndData.IsAsync)] = connectionEndData.IsAsync.ToString();
properties[nameof(connection.Database)] = connection.Database;
properties[nameof(connection.DataSource)] = connection.DataSource;
properties[nameof(connection.ConnectionTimeout)] = connection.ConnectionTimeout.ToString();
_telemetryClient.TrackDependency(dependencyTelemetry);
}
private void TrackTransactionDependency(
string eventName, TransactionEndData transactionEndData, bool success)
{
var transaction = transactionEndData.Transaction;
var dependencyTelemetry
= new DependencyTelemetry(
DependencyTypeName,
transaction.Connection.Database,
eventName,
$"{nameof(transactionEndData.Transaction.IsolationLevel)}={transaction.IsolationLevel}",
transactionEndData.StartTime,
transactionEndData.Duration,
success: success,
resultCode: null);
var properties = dependencyTelemetry.Properties;
properties[nameof(transactionEndData.ConnectionId)] = transactionEndData.ConnectionId.ToString();
properties[nameof(transactionEndData.TransactionId)] = transactionEndData.TransactionId.ToString();
properties[nameof(transactionEndData.Transaction.IsolationLevel)] = transactionEndData.Transaction.IsolationLevel.ToString();
_telemetryClient.TrackDependency(dependencyTelemetry);
}
private void TrackReaderDependency(string eventName, DataReaderDisposingData dataReaderDisposingData)
{
var command = dataReaderDisposingData.Command;
var dependencyTelemetry
= new DependencyTelemetry(
DependencyTypeName,
command.Connection.Database,
eventName,
command.CommandText,
dataReaderDisposingData.StartTime,
dataReaderDisposingData.Duration,
success: true,
resultCode: null);
var properties = dependencyTelemetry.Properties;
properties[nameof(dataReaderDisposingData.ConnectionId)] = dataReaderDisposingData.ConnectionId.ToString();
properties[nameof(dataReaderDisposingData.CommandId)] = dataReaderDisposingData.CommandId.ToString();
properties[nameof(dataReaderDisposingData.RecordsAffected)] = dataReaderDisposingData.RecordsAffected.ToString();
properties[nameof(command.CommandText)] = command.CommandText;
_telemetryClient.TrackDependency(dependencyTelemetry);
}
public void OnCompleted()
{
}
public void OnError(Exception error)
{
}
}
}
}

Просмотреть файл

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\build\common.props" />
<PropertyGroup>
<Description>Entity Framework Core Application Insights integration.</Description>
<TargetFramework>netstandard1.3</TargetFramework>
<AssemblyName>Microsoft.EntityFrameworkCore.ApplicationInsights</AssemblyName>
<RootNamespace>Microsoft.EntityFrameworkCore.ApplicationInsights</RootNamespace>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<CodeAnalysisRuleSet>..\EFCore.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Shared\*.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EFCore.Relational\EFCore.Relational.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Data.Common" Version="$(CoreFxVersion)" />
<PackageReference Include="Microsoft.ApplicationInsights" Version="$(AppInsightsVersion)" />
</ItemGroup>
<ItemGroup>
<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
</ItemGroup>
</Project>

Просмотреть файл

@ -48,7 +48,7 @@ namespace Microsoft.EntityFrameworkCore.Diagnostics
CommandId = commandId;
ConnectionId = connectionId;
ExecuteMethod = executeMethod;
Async = async;
IsAsync = async;
StartTime = startTime;
}
@ -75,7 +75,7 @@ namespace Microsoft.EntityFrameworkCore.Diagnostics
/// <summary>
/// Indicates whether or not the operation is being executed asyncronously.
/// </summary>
public virtual bool Async { get; }
public virtual bool IsAsync { get; }
/// <summary>
/// The start time of this event.

Просмотреть файл

@ -38,7 +38,7 @@ namespace Microsoft.EntityFrameworkCore.Diagnostics
{
Connection = connection;
ConnectionId = connectionId;
Async = async;
IsAsync = async;
StartTime = startTime;
}
@ -55,7 +55,7 @@ namespace Microsoft.EntityFrameworkCore.Diagnostics
/// <summary>
/// Indicates whether or not the operation is happening asyncronously.
/// </summary>
public virtual bool Async { get; }
public virtual bool IsAsync { get; }
/// <summary>
/// The start time of this event.

Просмотреть файл

@ -111,7 +111,7 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal
{
private readonly ExceptionInterceptor<T> _exceptionInterceptor;
private readonly IEnumerator<T> _innerEnumerator;
public EnumeratorExceptionInterceptor(ExceptionInterceptor<T> exceptionInterceptor)
{
_exceptionInterceptor = exceptionInterceptor;
@ -140,6 +140,7 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal
}
public void Reset() => _innerEnumerator?.Reset();
public void Dispose()
{
_innerEnumerator?.Dispose();

Просмотреть файл

@ -0,0 +1,13 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Reflection;
using Microsoft.EntityFrameworkCore.Tests;
namespace Microsoft.EntityFrameworkCore.ApplicationInsights.Tests
{
public class ApiConsistencyTest : ApiConsistencyTestBase
{
protected override Assembly TargetAssembly => typeof(DiagnosticEventForwarder).GetTypeInfo().Assembly;
}
}

Просмотреть файл

@ -0,0 +1,122 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.ApplicationInsights.Extensibility.Implementation;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests;
using Xunit;
using Xunit.Abstractions;
// ReSharper disable NotAccessedField.Local
// ReSharper disable InconsistentNaming
// ReSharper disable ReturnValueOfPureMethodIsNotUsed
// ReSharper disable PrivateFieldCanBeConvertedToLocalVariable
namespace Microsoft.EntityFrameworkCore.ApplicationInsights.Tests
{
public class DiagnosticEventForwarderTest : IClassFixture<NorthwindQuerySqlServerFixture>
{
private readonly NorthwindQuerySqlServerFixture _fixture;
private readonly TestTelemetryChannel _testTelemetryChannel = new TestTelemetryChannel();
private readonly TelemetryClient _telemetryClient;
public DiagnosticEventForwarderTest(NorthwindQuerySqlServerFixture fixture, ITestOutputHelper testOutputHelper)
{
_fixture = fixture;
//_testTelemetryChannel.SetTestOutputHelper(testOutputHelper);
_telemetryClient = new TelemetryClient(
new TelemetryConfiguration
{
InstrumentationKey = Guid.NewGuid().ToString(),
TelemetryChannel = _testTelemetryChannel
});
new DiagnosticEventForwarder(_telemetryClient).Start();
Assert.True(_telemetryClient.IsEnabled());
}
[Fact]
public void Forwards_events_as_dependencies()
{
using (var context = _fixture.CreateContext())
{
context.Customers.ToList();
}
Assert.Equal(4, _testTelemetryChannel.Items.Count);
Assert.True(_testTelemetryChannel.Items.All(it => it is DependencyTelemetry));
Assert.True(_testTelemetryChannel.Items.All(it => it.Context.Properties.ContainsKey("ConnectionId")));
var dependencyTelemetry = (DependencyTelemetry)_testTelemetryChannel.Items[0];
Assert.Equal("SQL", dependencyTelemetry.Type);
Assert.Equal("Northwind", dependencyTelemetry.Target);
Assert.Equal(RelationalEventId.ConnectionOpened.Name, dependencyTelemetry.Name);
}
#region TestTelemetryChannel
private sealed class TestTelemetryChannel : ITelemetryChannel
{
private ITestOutputHelper _testOutputHelper;
public void SetTestOutputHelper(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
}
public List<ITelemetry> Items { get; } = new List<ITelemetry>();
void ITelemetryChannel.Send(ITelemetry item)
{
Items.Add(item);
_testOutputHelper?.WriteLine(PrintTelemetry((dynamic)item));
}
void IDisposable.Dispose()
{
}
private static string PrintTelemetry(OperationTelemetry operationTelemetry)
{
return $"{operationTelemetry.GetType().ShortDisplayName()}: [Id='{operationTelemetry.Id}', Name='{operationTelemetry.Name}']";
}
private static string PrintTelemetry(DependencyTelemetry dependencyTelemetry)
{
return $"{dependencyTelemetry.GetType().ShortDisplayName()}: "
+ $"[Type='{dependencyTelemetry.Type}', "
+ $"Target='{dependencyTelemetry.Target}', "
+ $"Name='{dependencyTelemetry.Name}', "
+ $"Data='{dependencyTelemetry.Data}', "
+ $"Duration='{dependencyTelemetry.Duration.Milliseconds}ms']";
}
private static string PrintTelemetry(ITelemetry item)
{
return item.ToString();
}
void ITelemetryChannel.Flush()
{
}
bool? ITelemetryChannel.DeveloperMode { get; set; }
string ITelemetryChannel.EndpointAddress { get; set; }
}
#endregion
}
}

Просмотреть файл

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\build\common.props" />
<PropertyGroup>
<TargetFrameworks>net46;netcoreapp2.0</TargetFrameworks>
<TargetFrameworks Condition="'$(OS)' != 'Windows_NT' OR '$(CoreOnly)' == 'True'">netcoreapp2.0</TargetFrameworks>
<AssemblyName>Microsoft.EntityFrameworkCore.ApplicationInsights.Tests</AssemblyName>
<RootNamespace>Microsoft.EntityFrameworkCore.ApplicationInsights.Tests</RootNamespace>
<!-- TODO: Remove when Microsoft/vstest#428 is fixed. -->
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\EFCore.ApplicationInsights\EFCore.ApplicationInsights.csproj" />
<ProjectReference Include="..\EFCore.Tests\EFCore.Tests.csproj" />
<ProjectReference Include="..\EFCore.SqlServer.FunctionalTests\EFCore.SqlServer.FunctionalTests.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />
<PackageReference Include="xunit" Version="$(XunitVersion)" />
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitVersion)" />
</ItemGroup>
<ItemGroup>
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
</Project>

Просмотреть файл

@ -1047,8 +1047,8 @@ Logged Command",
Assert.Equal(diagnosticName, beforeData.ExecuteMethod);
Assert.Equal(diagnosticName, afterData.ExecuteMethod);
Assert.Equal(async, beforeData.Async);
Assert.Equal(async, afterData.Async);
Assert.Equal(async, beforeData.IsAsync);
Assert.Equal(async, afterData.IsAsync);
}
[Theory]
@ -1119,8 +1119,8 @@ Logged Command",
Assert.Equal(diagnosticName, beforeData.ExecuteMethod);
Assert.Equal(diagnosticName, afterData.ExecuteMethod);
Assert.Equal(async, beforeData.Async);
Assert.Equal(async, afterData.Async);
Assert.Equal(async, beforeData.IsAsync);
Assert.Equal(async, afterData.IsAsync);
Assert.Equal(exception, afterData.Exception);
}