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

Automatic netfx assembly redirection #1825

Merged
merged 11 commits into from
Dec 21, 2022
Merged
Show file tree
Hide file tree
Changes from 10 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
1 change: 1 addition & 0 deletions .cspell/dot-net.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ corelib
ASPNETCORE
HOSTINGSTARTUPASSEMBLIES
Bootstrapper
NETFX
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ jobs:
with:
name: bin-${{ matrix.machine }}
path: bin/tracer-home
- name: Delete SQL Server MSI
if: ${{ runner.os == 'Windows' }}
shell: bash
run: |
rm SqlLocalDB.msi
- name: Generated files unchanged
shell: bash
run: |
git status
git diff
[[ -z "$(git status --porcelain)" ]]

build-container:
strategy:
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ This beta release is built on top of [OpenTelemetry .NET](https://github.com/ope
`ConfigureTracesOptions(StackExchangeRedisCallsInstrumentationOptions options)`.
- Add plugin support for
`ConfigureMetricsOptions(AspNetCoreMetricsInstrumentationOptions options)`.
- Add automatic assembly redirection for .NET Framework applications. The redirection
can be enabled or disabled via the
`OTEL_DOTNET_AUTO_NETFX_ASSEMBLY_REDIRECTION_ENABLED` environment variable.
See the [additional settings](./docs/config.md#additional-settings) table for details.

### Changed

Expand Down
69 changes: 69 additions & 0 deletions build/AssemblyRedirectionSourceGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Mono.Cecil;
using Serilog;

public class AssemblyRedirectionSourceGenerator
{
public static void Generate(string assembliesFolderPath, string generatedFilePath)
{
Log.Debug("Generating assembly redirection file {0}", generatedFilePath);
var assemblies = new SortedDictionary<string, AssemblyNameDefinition>();
foreach (var fileName in Directory.EnumerateFiles(assembliesFolderPath))
{
try
{
using var moduleDef = ModuleDefinition.ReadModule(fileName);
var assemblyDef = moduleDef.Assembly.Name!;
if (assemblyDef.Name == "netstandard")
{
// Skip netstandard, since it doesn't need redirection.
continue;
}

assemblies[assemblyDef.Name] = assemblyDef;
Log.Debug("Adding {0} assembly to the redirection map. Targeted version {1}", assemblyDef.Name, assemblyDef.Version);
}
catch (BadImageFormatException)
{
Log.Debug("Skipping \"{0}\" couldn't open it as a managed assembly", fileName);
}
}

var sourceContents = GenerateSourceContents(assemblies);

File.WriteAllText(generatedFilePath, sourceContents, Encoding.UTF8);
Log.Information("Assembly redirection source generated {0}", generatedFilePath);
}

static string GenerateSourceContents(SortedDictionary<string, AssemblyNameDefinition> assemblies)
{
var sb = new StringBuilder(assemblies.Count * 256);
sb.AppendLine($"// Auto-generated file, do not change it - generated by the {nameof(AssemblyRedirectionSourceGenerator)} type");
sb.Append(@"
#include ""cor_profiler.h""

#ifdef _WIN32
namespace trace
{
void CorProfiler::InitNetFxAssemblyRedirectsMap()
{
assembly_version_redirect_map_.insert({
");
foreach (var kvp in assemblies)
{
var v = kvp.Value.Version!;
sb.AppendLine($" {{ L\"{kvp.Key}\", {{{v.Major}, {v.Minor}, {v.Build}, {v.Revision}}} }},");
}

sb.Append(@" });
}
}
#endif
");

return sb.ToString();
}
}
15 changes: 13 additions & 2 deletions build/Build.Steps.Windows.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
using System.IO;
using Nuke.Common;
using Nuke.Common.IO;
using Nuke.Common.ProjectModel;
using Nuke.Common.Tooling;
using Nuke.Common.Tools.Docker;
using Nuke.Common.Tools.DotNet;
using Nuke.Common.Tools.MSBuild;
using Serilog;
using static Nuke.Common.EnvironmentInfo;
Expand All @@ -17,6 +15,7 @@ partial class Build
Target CompileNativeSrcWindows => _ => _
.Unlisted()
.After(CompileManagedSrc)
.After(GenerateNetFxAssemblyRedirectionSource)
.OnlyWhenStatic(() => IsWin)
.Executes(() =>
{
Expand Down Expand Up @@ -118,4 +117,16 @@ partial class Build
.SetProcessWorkingDirectory(aspNetProject.Parent)
);
});

Target GenerateNetFxAssemblyRedirectionSource => _ => _
.Unlisted()
.After(PublishManagedProfiler)
.OnlyWhenStatic(() => IsWin)
.Executes(() =>
{
var netFxAssembliesFolder = TracerHomeDirectory / MapToFolderOutput(TargetFramework.NET462);
var generatedSourceFile = SourceDirectory / Projects.AutoInstrumentationNative / "netfx_assembly_redirection.h";

AssemblyRedirectionSourceGenerator.Generate(netFxAssembliesFolder, generatedSourceFile);
});
}
10 changes: 5 additions & 5 deletions build/Build.Steps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,6 @@ partial class Build
.EnableNoRestore()
.SetFramework(TargetFramework.NET6_0)
.SetOutput(TracerHomeDirectory / MapToFolderOutput(TargetFramework.NET6_0)));

string MapToFolderOutput(TargetFramework targetFramework)
{
return targetFramework.ToString().StartsWith("net4") ? "netfx" : "net";
}
});

Target PublishNativeProfiler => _ => _
Expand Down Expand Up @@ -565,4 +560,9 @@ private void RunBootstrappingTests()
}
}
}

private string MapToFolderOutput(TargetFramework targetFramework)
{
return targetFramework.ToString().StartsWith("net4") ? "netfx" : "net";
}
}
3 changes: 2 additions & 1 deletion build/Build.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ partial class Build : NukeBuild
[Parameter("Test projects filter. Optional, default matches all test projects. The project will be selected if the string is part of its name.")]
readonly string TestProject = "";

[Parameter("Test name fitler. Optional")]
[Parameter("Test name filter. Optional")]
readonly string TestName;

[Parameter("Number of times each dotnet test is run. Default is '1'")]
Expand Down Expand Up @@ -88,6 +88,7 @@ void DeleteReparsePoints(string path)
.DependsOn(Restore)
.DependsOn(CompileManagedSrc)
.DependsOn(PublishManagedProfiler)
.DependsOn(GenerateNetFxAssemblyRedirectionSource)
.DependsOn(CompileNativeSrc)
.DependsOn(PublishNativeProfiler)
.DependsOn(CopyIntegrationsJson)
Expand Down
1 change: 1 addition & 0 deletions build/_build.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Mono.Cecil" Version="0.11.4" />
<PackageReference Include="Nuke.Common" Version="6.3.0" />
<PackageReference Include="Nuget.CommandLine" Version="6.3.1" ExcludeAssets="all" />
</ItemGroup>
Expand Down
23 changes: 12 additions & 11 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,17 +237,18 @@ Important environment variables include:

## Additional settings

| Environment variable | Description | Default value |
|------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|
| `OTEL_DOTNET_AUTO_TRACES_ENABLED` | Enables traces. | `true` |
| `OTEL_DOTNET_AUTO_OPENTRACING_ENABLED` | Enables OpenTracing tracer. | `false` |
| `OTEL_DOTNET_AUTO_LOGS_ENABLED` | Enables logs. | `true` |
| `OTEL_DOTNET_AUTO_METRICS_ENABLED` | Enables metrics. | `true` |
| `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. | |
| `OTEL_DOTNET_AUTO_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. | |
| `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` |
| `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 instrumented spans. | |
| `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). | |
| Environment variable | Description | Default value |
|-------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|
| `OTEL_DOTNET_AUTO_TRACES_ENABLED` | Enables traces. | `true` |
| `OTEL_DOTNET_AUTO_OPENTRACING_ENABLED` | Enables OpenTracing tracer. | `false` |
| `OTEL_DOTNET_AUTO_LOGS_ENABLED` | Enables logs. | `true` |
| `OTEL_DOTNET_AUTO_METRICS_ENABLED` | Enables metrics. | `true` |
| `OTEL_DOTNET_AUTO_NETFX_ASSEMBLY_REDIRECTION_ENABLED` | Enables automatic redirection of the assemblies used by the automatic instrumentation on the .NET Framework. | `true` |
pjanotti marked this conversation as resolved.
Show resolved Hide resolved
| `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. | |
| `OTEL_DOTNET_AUTO_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. | |
| `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` |
| `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 instrumented spans. | |
| `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). | |

## .NET CLR Profiler

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,20 @@ private static string ResolveManagedProfilerDirectory()
return null;
}

StartupLogger.Debug("Requester [{0}] requested [{1}]", args?.RequestingAssembly?.FullName ?? "<null>", args?.Name ?? "<null>");
var path = Path.Combine(ManagedProfilerDirectory, $"{assemblyName}.dll");
if (File.Exists(path))
{
StartupLogger.Debug("Loading {0}", path);
return Assembly.LoadFrom(path);
try
{
var loadedAssembly = Assembly.LoadFrom(path);
StartupLogger.Debug("Assembly.LoadFrom(\"{0}\") succeeded={1}", path, loadedAssembly != null);
return loadedAssembly;
}
catch (Exception ex)
{
StartupLogger.Debug("Assembly.LoadFrom(\"{0}\") Exception: {1}", path, ex);
}
}

return null;
Expand Down
46 changes: 46 additions & 0 deletions src/OpenTelemetry.AutoInstrumentation.Native/clr_helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,52 @@ struct FunctionInfo
}
};

struct AssemblyVersionRedirection
{
// This struct is used to check and track version against ASSEMBLYMETADATA structs
// so it uses the same naming conventions, keeping fields with the same names in both
// structs.
USHORT usMajorVersion; // Major Version.
USHORT usMinorVersion; // Minor Version.
USHORT usBuildNumber; // Build Number.
USHORT usRevisionNumber; // Revision Number.

// Redirection related fields
ULONG ulRedirectionCount; // Tracks the number of times that the redirection was applied.

AssemblyVersionRedirection() :
usMajorVersion(0), usMinorVersion(0), usBuildNumber(0), usRevisionNumber(0), ulRedirectionCount(0)
{
}

AssemblyVersionRedirection(USHORT major, USHORT minor, USHORT build, USHORT revision) : ulRedirectionCount(0)
{
usMajorVersion = major;
usMinorVersion = minor;
usBuildNumber = build;
usRevisionNumber = revision;
}

int CompareToAssemblyVersion(const ASSEMBLYMETADATA& assembly)
{
return
usMajorVersion != assembly.usMajorVersion
? (usMajorVersion > assembly.usMajorVersion ? 1 : -1) :
usMinorVersion != assembly.usMinorVersion
? (usMinorVersion > assembly.usMinorVersion ? 1 : -1) :
usBuildNumber != assembly.usBuildNumber
? (usBuildNumber > assembly.usBuildNumber ? 1 : -1) :
usRevisionNumber != assembly.usRevisionNumber
? (usRevisionNumber > assembly.usRevisionNumber ? 1 : -1) :
0;
}

WSTRING VersionStr()
{
return trace::VersionStr(usMajorVersion, usMinorVersion, usBuildNumber, usRevisionNumber);
}
};

RuntimeInformation GetRuntimeInformation(ICorProfilerInfo7* info);

AssemblyInfo GetAssemblyInfo(ICorProfilerInfo7* info, const AssemblyID& assembly_id);
Expand Down
Loading