Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@

### Added

- IL rewrite for SqlCommand on .NET Framework to ensure CommandText is available for

Check failure on line 12 in CHANGELOG.md

View workflow job for this annotation

GitHub Actions / build

Line length

CHANGELOG.md:12:81 MD013/line-length Line length [Expected: 80; Actual: 85] https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md013.md

Check failure on line 12 in CHANGELOG.md

View workflow job for this annotation

GitHub Actions / build

Trailing spaces

CHANGELOG.md:12:85 MD009/no-trailing-spaces Trailing spaces [Expected: 0 or 2; Actual: 1] https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md009.md
[SqlClient instrumentation](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/issues/4343).
This is enabled by default but can disabled via the
`OTEL_DOTNET_AUTO_SQLCLIENT_NETFX_ILREWRITE_ENABLED` environment variable.

### Changed

#### Dependency updates
Expand Down
6 changes: 4 additions & 2 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ with environment variables taking precedence over `App.config` or `Web.config` f
- `OTEL_DOTNET_AUTO_LOG_DIRECTORY`
- `OTEL_LOG_LEVEL`
- `OTEL_DOTNET_AUTO_NETFX_REDIRECT_ENABLED`
- `OTEL_DOTNET_AUTO_SQLCLIENT_NETFX_ILREWRITE_ENABLED`

Example with `OTEL_SERVICE_NAME` setting:

Expand Down Expand Up @@ -414,9 +415,10 @@ Important environment variables include:
| `OTEL_DOTNET_AUTO_NETFX_REDIRECT_ENABLED` | Enables automatic redirection of the assemblies used by the automatic instrumentation on the .NET Framework. | `true` | [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/versioning-and-stability.md) |
| `OTEL_DOTNET_AUTO_TRACES_ADDITIONAL_SOURCES` | Comma-separated list of additional `System.Diagnostics.ActivitySource` names to be added to the tracer at the startup. Use it to capture manually instrumented spans. | | [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/versioning-and-stability.md) |
| `OTEL_DOTNET_AUTO_TRACES_ADDITIONAL_LEGACY_SOURCES` | Comma-separated list of additional legacy source names to be added to the tracer at the startup. Use it to capture `System.Diagnostics.Activity` objects created without using the `System.Diagnostics.ActivitySource` API. | | [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/versioning-and-stability.md) |
| `OTEL_DOTNET_AUTO_FLUSH_ON_UNHANDLEDEXCEPTION` | Controls whether the telemetry data is flushed when an [AppDomain.UnhandledException](https://docs.microsoft.com/en-us/dotnet/api/system.appdomain.unhandledexception) event is raised. Set to `true` when you suspect that you are experiencing a problem with missing telemetry data and also experiencing unhandled exceptions. | `false` | [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/versioning-and-stability.md) |
| `OTEL_DOTNET_AUTO_FLUSH_ON_UNHANDLEDEXCEPTION` | Controls whether the telemetry data is flushed when an [AppDomain.UnhandledException](https://docs.microsoft.com/en-us/dotnet/api/system.appdomain.unhandledexception) event is raised. Set to `true` when you suspect that you are experiencing a problem with missing telemetry data and also experiencing unhandled exceptions. | `false` | [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/versioning-and-stability.md) |
| `OTEL_DOTNET_AUTO_METRICS_ADDITIONAL_SOURCES` | Comma-separated list of additional `System.Diagnostics.Metrics.Meter` names to be added to the meter at the startup. Use it to capture manually created metrics. | | [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/versioning-and-stability.md) |
| `OTEL_DOTNET_AUTO_PLUGINS` | Colon-separated list of OTel SDK instrumentation plugin types, specified with the [assembly-qualified name](https://docs.microsoft.com/en-us/dotnet/api/system.type.assemblyqualifiedname?view=net-6.0#system-type-assemblyqualifiedname). *Note: This list must be colon-separated because the type names may include commas.* See more info on how to write plugins at [plugins.md](plugins.md). | | [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/versioning-and-stability.md) |
| `OTEL_DOTNET_AUTO_PLUGINS` | Colon-separated list of OTel SDK instrumentation plugin types, specified with the [assembly-qualified name](https://docs.microsoft.com/en-us/dotnet/api/system.type.assemblyqualifiedname?view=net-6.0#system-type-assemblyqualifiedname). *Note: This list must be colon-separated because the type names may include commas.* See more info on how to write plugins at [plugins.md](plugins.md). | | [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/versioning-and-stability.md) | |
`OTEL_DOTNET_AUTO_SQLCLIENT_NETFX_ILREWRITE_ENABLED` | Enables IL rewriting of `SqlCommand` on .NET Framework to ensure `CommandText` is present for `SqlClient` instrumentation, which is required for `db.query.text` and `db.query.summary` to be populated. | `true` | [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/versioning-and-stability.md) |

## RuleEngine

Expand Down
141 changes: 141 additions & 0 deletions src/OpenTelemetry.AutoInstrumentation.Native/cor_profiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,14 @@ HRESULT STDMETHODCALLTYPE CorProfiler::ModuleLoadFinished(ModuleID module_id, HR
" | IsResource = ", module_info.IsResource(), std::noboolalpha);
}

#ifdef _WIN32
if (runtime_information_.is_desktop() && module_info.assembly.name == WStr("System.Data") &&
IsSqlClientNetFxILRewriteEnabled())
{
RewriteILSystemDataCommandText(module_id);
}
#endif

if (module_info.IsNGEN())
{
// We check if the Module contains NGEN images and added to the
Expand Down Expand Up @@ -3234,6 +3242,139 @@ HRESULT CorProfiler::GenerateLoaderType(const ModuleID module_id,
return S_OK;
}

HRESULT CorProfiler::RewriteILSystemDataCommandText(const ModuleID module_id)
{
if (Logger::IsDebugEnabled())
{
Logger::Debug("RewriteILSystemDataCommandText: Attempting IL rewrite for System.Data");
}

ComPtr<IUnknown> metadata_interfaces;
auto hr = this->info_->GetModuleMetaData(module_id, ofRead | ofWrite, IID_IMetaDataImport2,
metadata_interfaces.GetAddressOf());

if (FAILED(hr))
{
Logger::Warn("RewriteILSystemDataCommandText: Failed to get metadata interface for ", module_id);
return hr;
}

const auto& metadata_import = metadata_interfaces.As<IMetaDataImport2>(IID_IMetaDataImport);
const auto& metadata_emit = metadata_interfaces.As<IMetaDataEmit2>(IID_IMetaDataEmit);

// Find the type definition for System.Data.SqlClient.SqlCommand
mdTypeDef system_data_sqlclient_sqlcommand;
hr = metadata_import->FindTypeDefByName(WStr("System.Data.SqlClient.SqlCommand"), mdTokenNil,
&system_data_sqlclient_sqlcommand);

if (FAILED(hr))
{
Logger::Warn("RewriteILSystemDataCommandText: FindTypeDefByName System.Data.SqlClient.SqlCommand failed");
return hr;
}

SignatureBuilder::InstanceMethod write_begin_execute_event_signature{{SignatureBuilder::BuiltIn::Void}, {}};

// Find the method definition for WriteBeginExecuteEvent
mdMethodDef write_begin_execute_event_token;
hr = metadata_import->FindMethod(system_data_sqlclient_sqlcommand, WStr("WriteBeginExecuteEvent"),
write_begin_execute_event_signature.Head(),
write_begin_execute_event_signature.Size(), &write_begin_execute_event_token);

if (FAILED(hr))
{
Logger::Warn("RewriteILSystemDataCommandText: FindMethod "
"System.Data.SqlClient.SqlCommand::WriteBeginExecuteEvent failed");
return hr;
}

ILRewriter rewriter(this->info_, nullptr, module_id, write_begin_execute_event_token);
hr = rewriter.Import();

if (FAILED(hr))
{
Logger::Warn("RewriteILSystemDataCommandText: ILRewriter.Import failed");
return hr;
}

// 1. Find the last callvirt instruction (BeginExecute)
// callvirt instance void System.Data.SqlEventSource::BeginExecute(int32, string, string, string)
ILInstr* lastInstr = rewriter.GetILList()->m_pPrev;
ILInstr* targetCallvirt = nullptr;
for (ILInstr* instr = lastInstr; instr != rewriter.GetILList(); instr = instr->m_pPrev)
{
if (instr->m_opcode == CEE_CALLVIRT)
{
targetCallvirt = instr;
break;
}
}

if (!targetCallvirt)
{
Logger::Warn("RewriteILSystemDataCommandText: Could not find callvirt instruction for BeginExecute");
return E_FAIL;
}

// 2. Insert pop before the callvirt (remove last argument)
ILInstr* popInstr = rewriter.NewILInstr();
popInstr->m_opcode = CEE_POP;
rewriter.InsertBefore(targetCallvirt, popInstr);

// 3. Insert ldarg.0 after pop (load 'this')
ILInstr* ldarg0Instr = rewriter.NewILInstr();
ldarg0Instr->m_opcode = CEE_LDARG_0;
rewriter.InsertAfter(popInstr, ldarg0Instr);

// 4. Insert callvirt to get_CommandText after ldarg.0

SignatureBuilder::InstanceMethod get_command_text_signature{{SignatureBuilder::BuiltIn::String}, {}};

mdTypeRef dbCommandTypeRef;
hr = metadata_emit->DefineTypeRefByName(mdTokenNil, WStr("System.Data.Common.DbCommand"), &dbCommandTypeRef);

if (FAILED(hr))
{
Logger::Warn("RewriteILSystemDataCommandText: DefineTypeRefByName System.Data.Common.DbCommand failed");
return hr;
}

mdMemberRef getCommandTextMemberRef;
hr = metadata_emit->DefineMemberRef(dbCommandTypeRef, WStr("get_CommandText"), get_command_text_signature.Head(),
get_command_text_signature.Size(), &getCommandTextMemberRef);

if (FAILED(hr))
{
Logger::Warn("RewriteILSystemDataCommandText: DefineMemberRef get_CommandText failed");
return hr;
}

ILInstr* callvirtGetCommandTextInstr = rewriter.NewILInstr();
callvirtGetCommandTextInstr->m_opcode = CEE_CALLVIRT;
callvirtGetCommandTextInstr->m_Arg32 = getCommandTextMemberRef;
rewriter.InsertAfter(ldarg0Instr, callvirtGetCommandTextInstr);

if (IsDumpILRewriteEnabled())
{
mdToken token = 0;
TypeInfo typeInfo{};
WSTRING methodName = WStr("WriteBeginExecuteEvent");
FunctionInfo caller(token, methodName, typeInfo, MethodSignature(), FunctionMethodSignature());
Logger::Info(
GetILCodes("*** ModifyWriteBeginExecuteEvent: Modified Code: ", &rewriter, caller, metadata_import));
}

hr = rewriter.Export();

if (FAILED(hr))
{
Logger::Warn("RewriteILSystemDataCommandText: Call to ILRewriter.Export() failed for ModuleID=", module_id);
return hr;
}

return hr;
}

HRESULT CorProfiler::GenerateLoaderMethod(const ModuleID module_id, mdMethodDef* ret_method_token)
{
ComPtr<IUnknown> metadata_interfaces;
Expand Down
5 changes: 5 additions & 0 deletions src/OpenTelemetry.AutoInstrumentation.Native/cor_profiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,11 @@ class CorProfiler : public CorProfilerBase
//
void ConfigureContinuousProfiler(bool threadSamplingEnabled, unsigned int threadSamplingInterval, bool allocationSamplingEnabled, unsigned int maxMemorySamplesPerMinute, unsigned int selectedThreadsSamplingInterval);

//
// IL Rewriting methods
//
HRESULT RewriteILSystemDataCommandText(const ModuleID module_id);

friend class TracerMethodRewriter;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ const WSTRING netfx_assembly_redirection_enabled = WStr("OTEL_DOTNET_AUTO_NETFX_
// Enable the fail fast mode.
const WSTRING fail_fast_enabled = WStr("OTEL_DOTNET_AUTO_FAIL_FAST_ENABLED");

// Enable the assembly version redirection when running on the .NET Framework.
const WSTRING sqlclient_netfx_ilrewrite_enabled = WStr("OTEL_DOTNET_AUTO_SQLCLIENT_NETFX_ILREWRITE_ENABLED");

// The list of startup hooks defined for .NET Core 3.1+ applications.
// This is a .NET runtime environment variable.
// See https://github.com/dotnet/runtime/blob/main/docs/design/features/host-startup-hook.md
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,9 @@ bool IsNetFxAssemblyRedirectionEnabled()
ToBooleanWithDefault(GetEnvironmentValue(environment::netfx_assembly_redirection_enabled), true);
}

bool IsSqlClientNetFxILRewriteEnabled()
{
ToBooleanWithDefault(GetEnvironmentValue(environment::sqlclient_netfx_ilrewrite_enabled), true);
}

} // namespace trace
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ bool IsDumpILRewriteEnabled();
bool IsAzureAppServices();
bool IsFailFastEnabled();
bool IsNetFxAssemblyRedirectionEnabled();
bool IsSqlClientNetFxILRewriteEnabled();
Copy link
Member

Choose a reason for hiding this comment

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

Ideally, it should be propagated from the managed coded. We are in the middle of introducing File based configuration and it need to be reworked in the near feature. Not a blocker for this PR IMO.

Ref: #4492


} // namespace trace

Expand Down
17 changes: 12 additions & 5 deletions test/IntegrationTests/SqlClientSystemTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ public static TheoryData<string, bool> GetData()

foreach (var version in LibraryVersion.SqlClientSystem)
{
#if NET
theoryData.Add(version, true);
#endif
theoryData.Add(version, false);
}

Expand All @@ -37,23 +35,32 @@ public static TheoryData<string, bool> GetData()
[Trait("Category", "EndToEnd")]
[Trait("Containers", "Linux")]
[MemberData(nameof(GetData))]
public void SubmitTraces(string packageVersion, bool dbStatementForText)
public void SubmitTraces(string packageVersion, bool enableIlRewrite)
{
// Skip the test if fixture does not support current platform
_sqlServerFixture.SkipIfUnsupportedPlatform();

SetEnvironmentVariable("OTEL_DOTNET_AUTO_SQLCLIENT_SET_DBSTATEMENT_FOR_TEXT", dbStatementForText.ToString());
SetEnvironmentVariable("OTEL_DOTNET_AUTO_SQLCLIENT_SET_DBSTATEMENT_FOR_TEXT", "true");
#if NETFRAMEWORK
SetEnvironmentVariable("OTEL_DOTNET_AUTO_SQLCLIENT_NETFX_ILREWRITE_ENABLED", enableIlRewrite.ToString());
#else
enableIlRewrite.ToString(); // to avoid unused parameter warning
#endif
using var collector = new MockSpansCollector(Output);
SetExporter(collector);

if (dbStatementForText)
#if NETFRAMEWORK
if (enableIlRewrite)
{
collector.Expect("OpenTelemetry.Instrumentation.SqlClient", span => span.Attributes.Any(attr => attr.Key == "db.statement" && !string.IsNullOrWhiteSpace(attr.Value?.StringValue)));
}
else
{
collector.Expect("OpenTelemetry.Instrumentation.SqlClient", span => span.Attributes.All(attr => attr.Key != "db.statement"));
}
#else
collector.Expect("OpenTelemetry.Instrumentation.SqlClient", span => span.Attributes.Any(attr => attr.Key == "db.statement" && !string.IsNullOrWhiteSpace(attr.Value?.StringValue)));
#endif

RunTestApplication(new()
{
Expand Down
Loading