Skip to content
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

[Instrumentation.EntityFrameworkCore] Fix db.statement tag for Devart Oracle #2466

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

vladajankovic
Copy link
Contributor

Fixes #2462

Changes

Added code to fix exception thrown when Devart Oracle is used (this.commandTypeFetcher.Fetch(command)).
If Devart Oracle is used, the SQL statement will be extracted from the payload.

Merge requirement checklist

  • CONTRIBUTING guidelines followed (license requirements, nullable enabled, static analysis, etc.)
  • Unit tests added/updated
  • Appropriate CHANGELOG.md files updated for non-trivial changes
  • Changes in public API reviewed (if applicable)

@vladajankovic vladajankovic requested a review from a team as a code owner January 15, 2025 16:44
@github-actions github-actions bot added the comp:instrumentation.entityframeworkcore Things related to OpenTelemetry.Instrumentation.EntityFrameworkCore label Jan 15, 2025
Comment on lines 203 to 216
if (command.GetType().FullName?.Contains("Devart.Data.Oracle") == true)
{
string payloadString = payload?.ToString() ?? string.Empty;
if ((payloadString.Contains("CommandType='Text'") && this.options.SetDbStatementForText) ||
(payloadString.Contains("CommandType='StoredProcedure'") && this.options.SetDbStatementForStoredProcedure))
{
string[] result = payloadString.Split(['\n'], 2);
if (result.Length > 1)
{
activity.AddTag(AttributeDbStatement, result[1]);
}
}
}
else if (this.commandTypeFetcher.Fetch(command) is CommandType commandType)
Copy link
Contributor

Choose a reason for hiding this comment

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

There is couple things you should consider here:

  1. Support both for emitting old and new attributes. See lese if statement for details:
                                    if (this.options.SetDbStatementForStoredProcedure)
                                    {
                                        if (this.options.EmitOldAttributes)
                                        {
                                            activity.AddTag(AttributeDbStatement, commandText);
                                        }
                                        if (this.options.EmitNewAttributes)
                                        {
                                            activity.AddTag(AttributeDbQueryText, commandText);
                                        }
                                    }
  1. payloadString split, is it always '\n' or it can be vary based on the environment settings?
  2. Consider adding stub/mocked tests for this case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. I was writing this code based on an older version (1.0.0-beta.10) that didn't have this.options.EmitOldAttributes and this.options.EmitNewAttributes. So, I will update the code structure to follow the convention of the switch statement.

  2. Updated the line as follows: string[] result = payloadString.Split([Environment.NewLine], 2, StringSplitOptions.None);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. I am unable to write mock tests because Devart requires a license in order to connect to Oracle database. 😞

@vladajankovic
Copy link
Contributor Author

Hi @Kielek, can you chack on these changes I made?

@@ -200,7 +200,50 @@ public override void OnEventWritten(string name, object? payload)
return;
}

if (this.commandTypeFetcher.Fetch(command) is CommandType commandType)
if (command.GetType().FullName?.Contains("Devart.Data.Oracle") == true)
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider code below. It is doing couple of things

  1. It is always trying to fetch method in the standard way (probably most common)
  2. If it fails it goes to fallback mechanism for Devart.
  3. Changes some Linq comparison to standard way.
  4. Optimize payloadString.Contains( usages in new code, it is better to check bools first (cheaper).
From a06207733b5917f57328bd281d6d586948719e79 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= <[email protected]>
Date: Wed, 22 Jan 2025 09:31:15 +0100
Subject: [PATCH] proposal

---
 .../EntityFrameworkDiagnosticListener.cs      | 116 ++++++++----------
 1 file changed, 53 insertions(+), 63 deletions(-)

diff --git a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/Implementation/EntityFrameworkDiagnosticListener.cs b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/Implementation/EntityFrameworkDiagnosticListener.cs
index 26eb87b9..0cdb07fe 100644
--- a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/Implementation/EntityFrameworkDiagnosticListener.cs
+++ b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/Implementation/EntityFrameworkDiagnosticListener.cs
@@ -200,90 +200,80 @@ internal sealed class EntityFrameworkDiagnosticListener : ListenerHandler
                             return;
                         }
 
-                        if (command.GetType().FullName?.Contains("Devart.Data.Oracle") == true)
+                        if (this.commandTypeFetcher.Fetch(command) is CommandType commandType)
                         {
-                            string payloadString = payload?.ToString() ?? string.Empty;
-                            string[] result = payloadString.Split([Environment.NewLine], 2, StringSplitOptions.None);
-                            string commandText = result.Length > 1 ? result[1] : payloadString;
-
-                            if (payloadString.Contains("CommandType='Text'"))
+                            if (this.commandTextFetcher.TryFetch(command, out var commandText)) // consider adding here && !string.IsNullOrEmpty(commandText)
                             {
-                                if (this.options.SetDbStatementForText)
+                                switch (commandType)
                                 {
-                                    if (this.options.EmitOldAttributes)
-                                    {
-                                        activity.AddTag(AttributeDbStatement, commandText);
-                                    }
+                                    case CommandType.StoredProcedure:
+                                        if (this.options.SetDbStatementForStoredProcedure)
+                                        {
+                                            if (this.options.EmitOldAttributes)
+                                            {
+                                                activity.AddTag(AttributeDbStatement, commandText);
+                                            }
+
+                                            if (this.options.EmitNewAttributes)
+                                            {
+                                                activity.AddTag(AttributeDbQueryText, commandText);
+                                            }
+                                        }
 
-                                    if (this.options.EmitNewAttributes)
-                                    {
-                                        activity.AddTag(AttributeDbQueryText, commandText);
-                                    }
+                                        break;
+
+                                    case CommandType.Text:
+                                        if (this.options.SetDbStatementForText)
+                                        {
+                                            if (this.options.EmitOldAttributes)
+                                            {
+                                                activity.AddTag(AttributeDbStatement, commandText);
+                                            }
+
+                                            if (this.options.EmitNewAttributes)
+                                            {
+                                                activity.AddTag(AttributeDbQueryText, commandText);
+                                            }
+                                        }
+
+                                        break;
+
+                                    case CommandType.TableDirect:
+                                        break;
+                                    default:
+                                        break;
                                 }
                             }
-                            else if (payloadString.Contains("CommandType='StoredProcedure'"))
+                            else if (command.GetType().FullName?.StartsWith("Devart.Data.Oracle", StringComparison.Ordinal) == true)
                             {
-                                if (this.options.SetDbStatementForText)
+                                string payloadString = payload?.ToString() ?? string.Empty;
+                                string[] result = payloadString.Split([Environment.NewLine], 2, StringSplitOptions.None);
+                                string devartCommandText = result.Length > 1 ? result[1] : payloadString;
+
+                                if (this.options.SetDbStatementForText && payloadString.Contains("CommandType='Text'"))
                                 {
                                     if (this.options.EmitOldAttributes)
                                     {
-                                        activity.AddTag(AttributeDbStatement, commandText);
+                                        activity.AddTag(AttributeDbStatement, devartCommandText);
                                     }
 
                                     if (this.options.EmitNewAttributes)
                                     {
-                                        activity.AddTag(AttributeDbQueryText, commandText);
+                                        activity.AddTag(AttributeDbQueryText, devartCommandText);
                                     }
                                 }
-                            }
-                            else if (payloadString.Contains("CommandType='TableDirect'"))
-                            {
-                            }
-                            else
-                            {
-                            }
-                        }
-                        else if (this.commandTypeFetcher.Fetch(command) is CommandType commandType)
-                        {
-                            var commandText = this.commandTextFetcher.Fetch(command);
-                            switch (commandType)
-                            {
-                                case CommandType.StoredProcedure:
-                                    if (this.options.SetDbStatementForStoredProcedure)
+                                else if (this.options.SetDbStatementForText && payloadString.Contains("CommandType='StoredProcedure'"))
+                                {
+                                    if (this.options.EmitOldAttributes)
                                     {
-                                        if (this.options.EmitOldAttributes)
-                                        {
-                                            activity.AddTag(AttributeDbStatement, commandText);
-                                        }
-
-                                        if (this.options.EmitNewAttributes)
-                                        {
-                                            activity.AddTag(AttributeDbQueryText, commandText);
-                                        }
+                                        activity.AddTag(AttributeDbStatement, devartCommandText);
                                     }
 
-                                    break;
-
-                                case CommandType.Text:
-                                    if (this.options.SetDbStatementForText)
+                                    if (this.options.EmitNewAttributes)
                                     {
-                                        if (this.options.EmitOldAttributes)
-                                        {
-                                            activity.AddTag(AttributeDbStatement, commandText);
-                                        }
-
-                                        if (this.options.EmitNewAttributes)
-                                        {
-                                            activity.AddTag(AttributeDbQueryText, commandText);
-                                        }
+                                        activity.AddTag(AttributeDbQueryText, devartCommandText);
                                     }
-
-                                    break;
-
-                                case CommandType.TableDirect:
-                                    break;
-                                default:
-                                    break;
+                                }
                             }
                         }
 
-- 
5.45.2.windows.1

Sorry for waiting long for a feedback, it is not trivial change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Kielek Thank you for the response and suggestions.
When this code is executed this.commandTypeFetcher.Fetch(command) in the IF statement, the exception will leave the EntityFrameworkCoreCommandExecuting case (It will not step into the ELSE IF statement to check if it's Devart). This can be fixed with a try-catch statement, where the TRY part contains the original code, and the CATCH statement handles fallback:

try
{
    if (this.commandTypeFetcher.Fetch(command) is CommandType commandType)
    {
        if (this.commandTextFetcher.TryFetch(command, out var commandText) && !string.IsNullOrEmpty(commandText))
        {
            switch (commandType)
            {
                case CommandType.StoredProcedure:
                    if (this.options.SetDbStatementForStoredProcedure)
                    {
                        if (this.options.EmitOldAttributes)
                        {
                            activity.AddTag(AttributeDbStatement, commandText);
                        }

                        if (this.options.EmitNewAttributes)
                        {
                            activity.AddTag(AttributeDbQueryText, commandText);
                        }
                    }

                    break;

                case CommandType.Text:
                    if (this.options.SetDbStatementForText)
                    {
                        if (this.options.EmitOldAttributes)
                        {
                            activity.AddTag(AttributeDbStatement, commandText);
                        }

                        if (this.options.EmitNewAttributes)
                        {
                            activity.AddTag(AttributeDbQueryText, commandText);
                        }
                    }

                    break;

                case CommandType.TableDirect:
                    break;
                default:
                    break;
            }
        }
    }
}
catch (Exception)
{
    string payloadString = payload?.ToString() ?? string.Empty;
    string[] result = payloadString.Split([Environment.NewLine], 2, StringSplitOptions.None);
    string commandText = result.Length > 1 ? result[1] : payloadString;

    if (payloadString.Contains("CommandType='Text'"))
    {
        if (this.options.SetDbStatementForText)
        {
            if (this.options.EmitOldAttributes)
            {
                activity.AddTag(AttributeDbStatement, commandText);
            }

            if (this.options.EmitNewAttributes)
            {
                activity.AddTag(AttributeDbQueryText, commandText);
            }
        }
    }
    else if (payloadString.Contains("CommandType='StoredProcedure'"))
    {
        if (this.options.SetDbStatementForText)
        {
            if (this.options.EmitOldAttributes)
            {
                activity.AddTag(AttributeDbStatement, commandText);
            }

            if (this.options.EmitNewAttributes)
            {
                activity.AddTag(AttributeDbQueryText, commandText);
            }
        }
    }
}

With this change we don't need to check if Devart is causing this problem or another provider for database connection.
Is this change acceptable?

Copy link
Contributor

Choose a reason for hiding this comment

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

What's about converting it to if (this.commandTypeFetcher.TryFetch(command, out var commandType), without try/catch?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tried it but still throws exception unfortunately 😞

Copy link
Contributor Author

@vladajankovic vladajankovic Jan 22, 2025

Choose a reason for hiding this comment

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

Here is what I've dug up while analyzing this exception. Firstly, the Fetch function calls the TryFetch function in its implementation. The TryFetch function is implemented as:

public bool TryFetch(object obj, out T value)
{
    if (obj == null)
    {
        value = default;
        return false;
    }

    if (this.innerFetcher == null)
    {
        var type = obj.GetType().GetTypeInfo();
        var property = type.DeclaredProperties.FirstOrDefault(p => string.Equals(p.Name, this.propertyName, StringComparison.OrdinalIgnoreCase)) ?? type.GetProperty(this.propertyName);
        this.innerFetcher = PropertyFetch.FetcherForProperty(property);
    }

    return this.innerFetcher.TryFetch(obj, out value);
}

Now if we follow the code execution, PropertyFetch.FetcherForProperty(property); is called with the following implementation:

public static PropertyFetch FetcherForProperty(PropertyInfo propertyInfo)
{
    if (propertyInfo == null || !typeof(T).IsAssignableFrom(propertyInfo.PropertyType))
    {
        // returns null on any fetch.
        return new PropertyFetch();
    }

    var typedPropertyFetcher = typeof(TypedPropertyFetch<,>);
    var instantiatedTypedPropertyFetcher = typedPropertyFetcher.MakeGenericType(
        typeof(T), propertyInfo.DeclaringType, propertyInfo.PropertyType);
    return (PropertyFetch)Activator.CreateInstance(instantiatedTypedPropertyFetcher, propertyInfo);
}

The return statement calls (PropertyFetch)Activator.CreateInstance(instantiatedTypedPropertyFetcher, propertyInfo); which calls the constructor of the TypedPropertyFetch:

#pragma warning disable CA1812
        private sealed class TypedPropertyFetch<TDeclaredObject, TDeclaredProperty> : PropertyFetch
#pragma warning restore CA1812
            where TDeclaredProperty : T
        {
            private readonly Func<TDeclaredObject, TDeclaredProperty> propertyFetch;

            public TypedPropertyFetch(PropertyInfo property)
            {
#if NET
                this.propertyFetch = property.GetMethod.CreateDelegate<Func<TDeclaredObject, TDeclaredProperty>>();
#else
                this.propertyFetch = (Func<TDeclaredObject, TDeclaredProperty>)property.GetMethod.CreateDelegate(typeof(Func<TDeclaredObject, TDeclaredProperty>));
#endif
            }

When CreateDelegate() is called, the exception is thrown System.ArgumentException: 'Cannot bind to the target method because its signature is not compatible with that of the delegate type.'.

This is the part I can't solve, which is why I made this workaround.

Copy link
Contributor

This PR was marked stale due to lack of activity. It will be closed in 7 days.

@github-actions github-actions bot added the Stale label Jan 30, 2025
Copy link

codecov bot commented Jan 30, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 0.00%. Comparing base (71655ce) to head (175c984).
Report is 734 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main   #2466       +/-   ##
==========================================
- Coverage   73.91%       0   -73.92%     
==========================================
  Files         267       0      -267     
  Lines        9615       0     -9615     
==========================================
- Hits         7107       0     -7107     
+ Misses       2508       0     -2508     

see 263 files with indirect coverage changes

@github-actions github-actions bot removed the Stale label Jan 31, 2025
Copy link
Contributor

github-actions bot commented Feb 7, 2025

This PR was marked stale due to lack of activity. It will be closed in 7 days.

@github-actions github-actions bot added the Stale label Feb 7, 2025
@Kielek Kielek removed the Stale label Feb 7, 2025
Copy link
Contributor

This PR was marked stale due to lack of activity. It will be closed in 7 days.

@github-actions github-actions bot added Stale and removed Stale labels Feb 15, 2025
@Kielek
Copy link
Contributor

Kielek commented Feb 18, 2025

@vladajankovic , again, sorry for long response. I have more other non-maintainer related activities currently, but finally I have find some time to check this problem once more time.

And the results looks good to me. Could you please provide some testing code which is failing?
There is a chance that I fixed the problem by #2558, but it will be great if you can confirm this.

If it is still failing, please provide code snipped which could help with debugging.

Tested on:

Docker: docker run -p 5432:5432 -e POSTGRES_HOST_AUTH_METHOD='trust' postgres:17.3
Test code:

From 817ac1e0828273a9b8ec51d10656efadf3390a4c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= <[email protected]>
Date: Tue, 18 Feb 2025 11:43:38 +0100
Subject: [PATCH] test

---
 opentelemetry-dotnet-contrib.sln              |   7 ++
 .../Properties/AssemblyInfo.cs                |   2 +
 .../EntityFrameworkDevartPostgreSqlTests.cs   | 114 ++++++++++++++++++
 ...ameworkCore.Devart.PostgreSql.Tests.csproj |  23 ++++
 4 files changed, 146 insertions(+)
 create mode 100644 test/OpenTelemetry.Instrumentation.EntityFrameworkCore.Devart.PostgreSql.Tests/EntityFrameworkDevartPostgreSqlTests.cs
 create mode 100644 test/OpenTelemetry.Instrumentation.EntityFrameworkCore.Devart.PostgreSql.Tests/OpenTelemetry.Instrumentation.EntityFrameworkCore.Devart.PostgreSql.Tests.csproj

diff --git a/opentelemetry-dotnet-contrib.sln b/opentelemetry-dotnet-contrib.sln
index 03d61a66..8fad7807 100644
--- a/opentelemetry-dotnet-contrib.sln
+++ b/opentelemetry-dotnet-contrib.sln
@@ -403,6 +403,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentati
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentation.ServiceFabricRemoting.Tests", "test\OpenTelemetry.Instrumentation.ServiceFabricRemoting.Tests\OpenTelemetry.Instrumentation.ServiceFabricRemoting.Tests.csproj", "{00C9F0B8-3D51-483C-821A-5889B38A144C}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenTelemetry.Instrumentation.EntityFrameworkCore.Devart.PostgreSql.Tests", "test\OpenTelemetry.Instrumentation.EntityFrameworkCore.Devart.PostgreSql.Tests\OpenTelemetry.Instrumentation.EntityFrameworkCore.Devart.PostgreSql.Tests.csproj", "{900B96E1-4298-944D-3444-F33E17BDC508}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -833,6 +835,10 @@ Global
 		{00C9F0B8-3D51-483C-821A-5889B38A144C}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{00C9F0B8-3D51-483C-821A-5889B38A144C}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{00C9F0B8-3D51-483C-821A-5889B38A144C}.Release|Any CPU.Build.0 = Release|Any CPU
+		{900B96E1-4298-944D-3444-F33E17BDC508}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{900B96E1-4298-944D-3444-F33E17BDC508}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{900B96E1-4298-944D-3444-F33E17BDC508}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{900B96E1-4298-944D-3444-F33E17BDC508}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -961,6 +967,7 @@ Global
 		{9B994669-E839-4C42-A0F1-DF9DD058C1DC} = {3A464E7A-42F3-44B0-B8D7-80521A7704A6}
 		{91BA4FF0-50BC-49D0-976B-CBC9E5FE8337} = {22DF5DC0-1290-4E83-A9D8-6BB7DE3B3E63}
 		{00C9F0B8-3D51-483C-821A-5889B38A144C} = {2097345F-4DD3-477D-BC54-A922F9B2B402}
+		{900B96E1-4298-944D-3444-F33E17BDC508} = {2097345F-4DD3-477D-BC54-A922F9B2B402}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {B0816796-CDB3-47D7-8C3C-946434DE3B66}
diff --git a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/Properties/AssemblyInfo.cs b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/Properties/AssemblyInfo.cs
index 7e447d9e..e4aa97c1 100644
--- a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/Properties/AssemblyInfo.cs
+++ b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/Properties/AssemblyInfo.cs
@@ -5,6 +5,8 @@ using System.Runtime.CompilerServices;
 
 #if SIGNED
 [assembly: InternalsVisibleTo("OpenTelemetry.Instrumentation.EntityFrameworkCore.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")]
+[assembly: InternalsVisibleTo("OpenTelemetry.Instrumentation.EntityFrameworkCore.Devart.PostgreSql.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")]
 #else
 [assembly: InternalsVisibleTo("OpenTelemetry.Instrumentation.EntityFrameworkCore.Tests")]
+[assembly: InternalsVisibleTo("OpenTelemetry.Instrumentation.EntityFrameworkCore.Devart.PostgreSql.Tests")]
 #endif
diff --git a/test/OpenTelemetry.Instrumentation.EntityFrameworkCore.Devart.PostgreSql.Tests/EntityFrameworkDevartPostgreSqlTests.cs b/test/OpenTelemetry.Instrumentation.EntityFrameworkCore.Devart.PostgreSql.Tests/EntityFrameworkDevartPostgreSqlTests.cs
new file mode 100644
index 00000000..1ac0a143
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.EntityFrameworkCore.Devart.PostgreSql.Tests/EntityFrameworkDevartPostgreSqlTests.cs
@@ -0,0 +1,114 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Data.Common;
+using System.Diagnostics;
+using Devart.Data.PostgreSql;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using OpenTelemetry.Instrumentation.EntityFrameworkCore.Implementation;
+using OpenTelemetry.Trace;
+using Xunit;
+
+namespace OpenTelemetry.Instrumentation.EntityFrameworkCore.Devart.PostgreSql.Tests;
+
+public class EntityFrameworkDevartPostgreSqlTests : IDisposable
+{
+    private readonly DbContextOptions<ItemsContext> contextOptions;
+    private readonly DbConnection connection;
+
+    public EntityFrameworkDevartPostgreSqlTests()
+    {
+        var dbContextOptionsBuilder = new DbContextOptionsBuilder<ItemsContext>();
+        var connection2 = CreatePgSqlConnection();
+        dbContextOptionsBuilder.UsePostgreSql(connection2);
+        this.contextOptions = dbContextOptionsBuilder
+            .Options;
+
+        this.connection = RelationalOptionsExtension.Extract(this.contextOptions).Connection!;
+
+        this.Seed();
+    }
+
+    [Fact]
+    public void EntityFrameworkContextEventsInstrumentedTest()
+    {
+        var exportedItems = new List<Activity>();
+        using var shutdownSignal = Sdk.CreateTracerProviderBuilder()
+            .AddInMemoryExporter(exportedItems)
+            .AddEntityFrameworkCoreInstrumentation(x => x.SetDbStatementForText = true).Build();
+
+        using (var context = new ItemsContext(this.contextOptions))
+        {
+            var items = context.Set<Item>().OrderBy(e => e.Name).ToList();
+
+            Assert.Equal(3, items.Count);
+            Assert.Equal("ItemOne", items[0].Name);
+            Assert.Equal("ItemThree", items[1].Name);
+            Assert.Equal("ItemTwo", items[2].Name);
+        }
+
+        var activity = Assert.Single(exportedItems);
+        Assert.Equal(ActivityKind.Client, activity.Kind);
+        Assert.Equal("postgresql", activity.Tags.FirstOrDefault(t => t.Key == EntityFrameworkDiagnosticListener.AttributeDbSystem).Value);
+
+        Assert.Equal("SELECT i.\"Name\"\r\nFROM \"Item\" AS i\r\nORDER BY i.\"Name\"", activity.Tags.FirstOrDefault(t => t.Key == EntityFrameworkDiagnosticListener.AttributeDbStatement).Value);
+    }
+
+    public void Dispose() => this.connection.Dispose();
+
+    private static PgSqlConnection CreatePgSqlConnection()
+    {
+        var connection = new PgSqlConnection("Server=127.0.0.1;Port=5432;User ID=postgres;");
+
+        connection.Open();
+
+        return connection;
+    }
+
+    private void Seed()
+    {
+        using var context = new ItemsContext(this.contextOptions);
+
+        context.Database.EnsureDeleted();
+        context.Database.EnsureCreated();
+
+        var one = new Item("ItemOne");
+
+        var two = new Item("ItemTwo");
+
+        var three = new Item("ItemThree");
+
+        context.AddRange(one, two, three);
+
+        context.SaveChanges();
+    }
+
+    private class Item
+    {
+        public Item(string name)
+        {
+            this.Name = name;
+        }
+
+        public string Name { get; }
+    }
+
+    private class ItemsContext : DbContext
+    {
+        public ItemsContext(DbContextOptions options)
+            : base(options)
+        {
+        }
+
+        protected override void OnModelCreating(ModelBuilder modelBuilder)
+        {
+            modelBuilder.Entity<Item>(
+                b =>
+                {
+                    b.Property(e => e.Name);
+                    b.HasKey("Name");
+                });
+        }
+    }
+}
diff --git a/test/OpenTelemetry.Instrumentation.EntityFrameworkCore.Devart.PostgreSql.Tests/OpenTelemetry.Instrumentation.EntityFrameworkCore.Devart.PostgreSql.Tests.csproj b/test/OpenTelemetry.Instrumentation.EntityFrameworkCore.Devart.PostgreSql.Tests/OpenTelemetry.Instrumentation.EntityFrameworkCore.Devart.PostgreSql.Tests.csproj
new file mode 100644
index 00000000..402b4314
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.EntityFrameworkCore.Devart.PostgreSql.Tests/OpenTelemetry.Instrumentation.EntityFrameworkCore.Devart.PostgreSql.Tests.csproj
@@ -0,0 +1,23 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <!-- OmniSharp/VS Code requires TargetFrameworks to be in descending order for IntelliSense and analysis. -->
+    <TargetFrameworks>$(SupportedNetTargets)</TargetFrameworks>
+    <Description>Unit test project for OpenTelemetry Microsoft.EntityFrameworkCore instrumentation.</Description>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Devart.Data.PostgreSQL.EFCore" Version="8.4.193.9" />
+    <PackageReference Include="OpenTelemetry.Exporter.InMemory" Version="$(OpenTelemetryExporterInMemoryPkgVer)" />
+    <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="$(OpenTelemetryCoreLatestVersion)" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Instrumentation.EntityFrameworkCore\OpenTelemetry.Instrumentation.EntityFrameworkCore.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Compile Include="$(RepoRoot)\src\Shared\SpanAttributeConstants.cs" Link="Includes\SpanAttributeConstants.cs" />
+  </ItemGroup>
+
+</Project>
-- 
2.47.1.windows.2

@vladajankovic
Copy link
Contributor Author

Hi @Kielek I will test the changes in #2558 to check if it solves the problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
comp:instrumentation.entityframeworkcore Things related to OpenTelemetry.Instrumentation.EntityFrameworkCore
Projects
None yet
2 participants