Skip to content

Commit 5dce969

Browse files
committed
Enhance AppDomain config and assembly loading strategies
Introduce OTEL_APP_DOMAIN_STRATEGY to control AppDomain config update strategies for .NET Framework, supporting LoaderOptimization, assembly redirects, or no action. Add dynamic app.config modification for assembly binding redirects via AppConfigUpdater and new AssemblyCatalog. Expand IIS tests to cover all strategies.
1 parent 31a7277 commit 5dce969

File tree

9 files changed

+408
-95
lines changed

9 files changed

+408
-95
lines changed

src/OpenTelemetry.AutoInstrumentation.Loader/AppConfigUpdater.cs

Lines changed: 175 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,193 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
#if NETFRAMEWORK
5+
using System.Text;
6+
using System.Xml;
7+
using System.Xml.Linq;
8+
using OpenTelemetry.AutoInstrumentation.Logging;
9+
510
namespace OpenTelemetry.AutoInstrumentation.Loader;
611

712
/// <summary>
813
/// Handles update of config files for non-default AppDomain
914
/// </summary>
1015
internal static class AppConfigUpdater
1116
{
17+
private static readonly IOtelLogger Logger = EnvironmentHelper.Logger;
18+
19+
private enum PatchMode
20+
{
21+
LoaderOptimizationSingleDomain,
22+
AssemblyRedirect,
23+
None
24+
}
25+
1226
/// <summary>
13-
/// Modify assembly bindings in appDomainSetup.
14-
/// Will be called through reflection when new <see cref="System.AppDomain"/> created.
15-
/// Call done using bytecode modifications for <see cref="AppDomain.CreateDomain(string,System.Security.Policy.Evidence,System.AppDomainSetup)"/>
16-
/// and <see cref="AppDomainManager.CreateDomainHelper(string,System.Security.Policy.Evidence,System.AppDomainSetup)"/>.
27+
/// Modify assembly bindings in appDomainSetup
1728
/// </summary>
1829
/// <param name="appDomainSetup">appDomainSetup to be updated</param>
1930
public static void ModifyConfig(AppDomainSetup appDomainSetup)
2031
{
21-
appDomainSetup.LoaderOptimization = LoaderOptimization.SingleDomain;
32+
var patchMode = Environment.GetEnvironmentVariable("OTEL_APP_DOMAIN_STRATEGY") ?? string.Empty;
33+
if (!Enum.TryParse<PatchMode>(patchMode, ignoreCase: true, out var mode))
34+
{
35+
mode = PatchMode.LoaderOptimizationSingleDomain;
36+
}
37+
38+
Logger.Debug($"Use {mode} strategy for multiple app domains");
39+
40+
switch (mode)
41+
{
42+
case PatchMode.LoaderOptimizationSingleDomain:
43+
appDomainSetup.LoaderOptimization = LoaderOptimization.SingleDomain;
44+
break;
45+
case PatchMode.AssemblyRedirect:
46+
ModifyAssemblyRedirectConfig(appDomainSetup);
47+
break;
48+
case PatchMode.None:
49+
break;
50+
default:
51+
throw new InvalidOperationException();
52+
}
53+
}
54+
55+
/// <summary>
56+
/// Modify assembly bindings in appDomainSetup
57+
/// </summary>
58+
/// <param name="appDomainSetup">appDomainSetup to be updated</param>
59+
private static void ModifyAssemblyRedirectConfig(AppDomainSetup appDomainSetup)
60+
{
61+
var configPath = appDomainSetup.ConfigurationFile;
62+
try
63+
{
64+
Logger.Debug($"Try to modify {configPath}");
65+
var config = XDocument.Load(configPath);
66+
ModifyConfig(config);
67+
68+
var settings = new XmlWriterSettings { OmitXmlDeclaration = false, Encoding = Encoding.UTF8 };
69+
using var memoryStream = new MemoryStream();
70+
using var xmlWriter = XmlWriter.Create(memoryStream, settings);
71+
config.WriteTo(xmlWriter);
72+
xmlWriter.Flush();
73+
appDomainSetup.SetConfigurationBytes(memoryStream.ToArray());
74+
Logger.Information($"Config modified: {config}");
75+
}
76+
catch (Exception e)
77+
{
78+
Logger.Error(e, "Failed to modify app domain config ");
79+
}
80+
}
81+
82+
private static void ModifyConfig(XDocument config)
83+
{
84+
var configuration = config.Element(Names.Configuration);
85+
if (configuration == null)
86+
{
87+
throw new InvalidOperationException();
88+
}
89+
90+
var runtime = configuration.Element(Names.Runtime);
91+
if (runtime == null)
92+
{
93+
runtime = new XElement(Names.Runtime);
94+
configuration.Add(runtime);
95+
}
96+
97+
var assemblyBinding = runtime.Element(Names.AssemblyBinding);
98+
if (assemblyBinding == null)
99+
{
100+
assemblyBinding = new XElement(Names.AssemblyBinding);
101+
runtime.Add(assemblyBinding);
102+
}
103+
104+
foreach (var assemblyInfo in AssemblyCatalog.GetAssemblies())
105+
{
106+
var existingRedirects = assemblyBinding
107+
.Descendants(Names.AssemblyIdentity)
108+
.Where(identity =>
109+
string.Equals(identity.Attribute(Names.Name)?.Value, assemblyInfo.FullName.Name, StringComparison.OrdinalIgnoreCase)
110+
&& string.Equals(identity.Attribute(Names.PublicKeyToken)?.Value, assemblyInfo.Token, StringComparison.OrdinalIgnoreCase))
111+
.Select(identity => identity.Parent)
112+
.Elements(Names.BindingRedirect).ToList();
113+
if (existingRedirects.Count > 1)
114+
{
115+
Logger.Warning($"Multiple redirections for {assemblyInfo.FullName.Name} found. Skipping it.");
116+
continue;
117+
}
118+
119+
if (existingRedirects.Count == 1)
120+
{
121+
var versionString = existingRedirects.First().Attribute(Names.NewVersion)?.Value ?? string.Empty;
122+
Version existingNewVersion;
123+
try
124+
{
125+
existingNewVersion = new Version(versionString);
126+
}
127+
catch (Exception ex)
128+
{
129+
Logger.Error(ex, $"Version {versionString} not parsed for {assemblyInfo.FullName.Name}. Treat it as 0.0.");
130+
existingNewVersion = new Version(0, 0);
131+
}
132+
133+
if (existingNewVersion >= assemblyInfo.Version)
134+
{
135+
// App uses higher version, use it
136+
if (existingRedirects.First().Attribute(Names.OldVersion) is { } oldVersion)
137+
{
138+
oldVersion.Value = $"0.0.0.0-{existingNewVersion}";
139+
}
140+
141+
Logger.Debug($"Use existing version. Override version range for {assemblyInfo.FullName.Name}");
142+
existingRedirects[0].Parent!.Add(new XElement(
143+
Names.CodeBase,
144+
new XAttribute(Names.Version, assemblyInfo.Version),
145+
new XAttribute(Names.Href, new Uri(assemblyInfo.Path).AbsoluteUri)));
146+
continue;
147+
}
148+
else
149+
{
150+
// Remove existing redirect, we will add a new one to use higher version
151+
existingRedirects[0].Parent!.Remove();
152+
}
153+
}
154+
155+
var dependentAssembly = new XElement(
156+
Names.DependentAssembly,
157+
new XElement(
158+
Names.AssemblyIdentity,
159+
new XAttribute(Names.Name, assemblyInfo.FullName.Name),
160+
new XAttribute(Names.PublicKeyToken, assemblyInfo.Token),
161+
new XAttribute(Names.Culture, Names.NeutralCulture)),
162+
new XElement(
163+
Names.BindingRedirect,
164+
new XAttribute(Names.OldVersion, $"0.0.0.0-{assemblyInfo.Version}"),
165+
new XAttribute(Names.NewVersion, assemblyInfo.Version)),
166+
new XElement(
167+
Names.CodeBase,
168+
new XAttribute(Names.Version, assemblyInfo.Version),
169+
new XAttribute(Names.Href, new Uri(assemblyInfo.Path).AbsoluteUri)));
170+
assemblyBinding.Add(dependentAssembly);
171+
}
172+
}
173+
174+
private static class Names
175+
{
176+
public const string NeutralCulture = "neutral";
177+
public static readonly XName Configuration = XName.Get("configuration");
178+
public static readonly XName Runtime = XName.Get("runtime");
179+
public static readonly XName AssemblyBinding = XName.Get("assemblyBinding", AsmNs);
180+
public static readonly XName AssemblyIdentity = XName.Get("assemblyIdentity", AsmNs);
181+
public static readonly XName BindingRedirect = XName.Get("bindingRedirect", AsmNs);
182+
public static readonly XName DependentAssembly = XName.Get("dependentAssembly", AsmNs);
183+
public static readonly XName CodeBase = XName.Get("codeBase", AsmNs);
184+
public static readonly XName Name = XName.Get("name");
185+
public static readonly XName PublicKeyToken = XName.Get("publicKeyToken");
186+
public static readonly XName NewVersion = XName.Get("newVersion");
187+
public static readonly XName OldVersion = XName.Get("oldVersion");
188+
public static readonly XName Culture = XName.Get("culture");
189+
public static readonly XName Version = XName.Get("version");
190+
public static readonly XName Href = XName.Get("href");
191+
private const string AsmNs = "urn:schemas-microsoft-com:asm.v1";
22192
}
23193
}
24194
#endif
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#if NETFRAMEWORK
5+
using System.Reflection;
6+
7+
namespace OpenTelemetry.AutoInstrumentation.Loader;
8+
9+
internal class AssemblyCatalog
10+
{
11+
private static readonly Dictionary<string, AssemblyInfo> Assemblies = new(StringComparer.OrdinalIgnoreCase);
12+
13+
static AssemblyCatalog()
14+
{
15+
static void BuildFromFolder()
16+
{
17+
var sharedFrameworkPath = Path.GetDirectoryName(EnvironmentHelper.ManagedProfilerDirectory)!;
18+
19+
var files =
20+
Directory.GetFiles(sharedFrameworkPath, "*.dll")
21+
.Concat(
22+
Directory.GetFiles(EnvironmentHelper.ManagedProfilerDirectory, "*.dll"))
23+
.Concat(Directory.GetFiles(EnvironmentHelper.ManagedProfilerDirectory, "*.link").Select(link =>
24+
{
25+
var linkPath = File.ReadAllText(link).Trim();
26+
return Path.Combine(sharedFrameworkPath, linkPath, Path.GetFileNameWithoutExtension(link));
27+
}));
28+
29+
foreach (var file in files)
30+
{
31+
try
32+
{
33+
var assemblyName = AssemblyName.GetAssemblyName(file);
34+
var keyToken = assemblyName.GetPublicKeyToken();
35+
if (assemblyName.Name == null || keyToken == null || keyToken.Length == 0 ||
36+
assemblyName.Version == null)
37+
{
38+
EnvironmentHelper.Logger.Warning($"No strong name for {file} ({assemblyName}), skipping it");
39+
continue;
40+
}
41+
42+
#pragma warning disable CA1308
43+
var token = BitConverter.ToString(keyToken).ToLowerInvariant().Replace("-", string.Empty);
44+
#pragma warning restore CA1308
45+
46+
if (Assemblies.TryGetValue(assemblyName.Name, out var info))
47+
{
48+
if (!string.Equals(info.Token, token, StringComparison.OrdinalIgnoreCase))
49+
{
50+
EnvironmentHelper.Logger.Error(
51+
$"Multiple files for {assemblyName.Name} with different tokens. Using {file}");
52+
continue;
53+
}
54+
55+
if (info.Version < assemblyName.Version)
56+
{
57+
EnvironmentHelper.Logger.Warning(
58+
$"Multiple files for {assemblyName.Name}, using ${assemblyName.Version} from {file}");
59+
}
60+
else
61+
{
62+
EnvironmentHelper.Logger.Warning(
63+
$"Multiple files for {assemblyName.Name}, using ${info.Version} from {info.Path}");
64+
continue;
65+
}
66+
}
67+
68+
Assemblies[assemblyName.Name] = new AssemblyInfo(token, assemblyName.Version, assemblyName, file);
69+
}
70+
catch (Exception ex)
71+
{
72+
EnvironmentHelper.Logger.Error(ex, $"Failed to resolve assembly name for {file}, skipping it");
73+
}
74+
}
75+
}
76+
77+
BuildFromFolder();
78+
}
79+
80+
internal static AssemblyInfo? GetAssemblyInfo(string shortName)
81+
{
82+
if (Assemblies.TryGetValue(shortName, out var info))
83+
{
84+
return info;
85+
}
86+
87+
return null;
88+
}
89+
90+
internal static IEnumerable<AssemblyInfo> GetAssemblies()
91+
=> Assemblies.Values;
92+
93+
internal sealed class AssemblyInfo(string token, Version version, AssemblyName fullName, string path)
94+
{
95+
public string Token { get; } = token;
96+
97+
public Version Version { get; } = version;
98+
99+
public AssemblyName FullName { get; } = fullName;
100+
101+
public string Path { get; } = path;
102+
}
103+
}
104+
#endif

src/OpenTelemetry.AutoInstrumentation.Loader/AssemblyResolver.Net.cs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,5 @@ internal partial class AssemblyResolver
9292
return null;
9393
}
9494
}
95-
96-
private string ResolveManagedProfilerDirectory()
97-
{
98-
string tracerFrameworkDirectory = "net";
99-
string tracerHomeDirectory = ReadEnvironmentVariable("OTEL_DOTNET_AUTO_HOME") ?? string.Empty;
100-
101-
return Path.Combine(tracerHomeDirectory, tracerFrameworkDirectory);
102-
}
10395
}
10496
#endif

src/OpenTelemetry.AutoInstrumentation.Loader/AssemblyResolver.NetFramework.cs

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -95,42 +95,5 @@ internal partial class AssemblyResolver
9595

9696
return null;
9797
}
98-
99-
/// <summary>
100-
/// Return redirection table used in runtime that will match TFM folder to load assemblies.
101-
/// It may not be actual .NET Framework version.
102-
/// </summary>
103-
[DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
104-
[DllImport("OpenTelemetry.AutoInstrumentation.Native.dll")]
105-
private static extern int GetNetFrameworkRedirectionVersion();
106-
107-
private string ResolveManagedProfilerDirectory()
108-
{
109-
var tracerHomeDirectory = ReadEnvironmentVariable("OTEL_DOTNET_AUTO_HOME") ?? string.Empty;
110-
var tracerFrameworkDirectory = "netfx";
111-
112-
var basePath = Path.Combine(tracerHomeDirectory, tracerFrameworkDirectory);
113-
// fallback to net462 in case of any issues
114-
var frameworkFolderName = "net462";
115-
try
116-
{
117-
var detectedVersion = GetNetFrameworkRedirectionVersion();
118-
var candidateFolderName = detectedVersion % 10 != 0 ? $"net{detectedVersion}" : $"net{detectedVersion / 10}";
119-
if (Directory.Exists(Path.Combine(basePath, candidateFolderName)))
120-
{
121-
frameworkFolderName = candidateFolderName;
122-
}
123-
else
124-
{
125-
_logger.Warning($"Framework folder {candidateFolderName} not found. Fallback to {frameworkFolderName}.");
126-
}
127-
}
128-
catch (Exception ex)
129-
{
130-
_logger.Warning(ex, $"Error getting .NET Framework version from native profiler. Fallback to {frameworkFolderName}.");
131-
}
132-
133-
return Path.Combine(basePath, frameworkFolderName);
134-
}
13598
}
13699
#endif

src/OpenTelemetry.AutoInstrumentation.Loader/AssemblyResolver.cs

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,6 @@ internal partial class AssemblyResolver
1717
public AssemblyResolver(IOtelLogger otelLogger)
1818
{
1919
_logger = otelLogger;
20-
_managedProfilerDirectory = ResolveManagedProfilerDirectory();
21-
}
22-
23-
private string? ReadEnvironmentVariable(string key)
24-
{
25-
try
26-
{
27-
return Environment.GetEnvironmentVariable(key);
28-
}
29-
catch (Exception ex)
30-
{
31-
_logger.Error(ex, "Error while loading environment variable {0}", key);
32-
}
33-
34-
return null;
20+
_managedProfilerDirectory = EnvironmentHelper.ManagedProfilerDirectory;
3521
}
3622
}

0 commit comments

Comments
 (0)