Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 12 additions & 0 deletions src/Elastic.Apm/Logging/GlobalLogConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,18 @@ private GlobalLogConfiguration(bool isActive, LogLevel logLevel, GlobalLogTarget
LogFilePrefix = logFilePrefix;

AgentLogFilePath = CreateLogFileName();

if (isActive && (logTarget & GlobalLogTarget.File) == GlobalLogTarget.File)
{
try
{
Directory.CreateDirectory(logFileDirectory);
}
catch
{
/* best-effort: file logging will fail gracefully if directory can't be created */
}
}
}

internal bool IsActive { get; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Licensed to Elasticsearch B.V under
// one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using Elastic.Apm.Logging;

namespace Elastic.Apm.StartupHook.Loader;

internal static class AssemblyLoadLogger
{
// Assemblies with no independent diagnostic value — their identity is already captured by
// the .NET runtime version string, or they are infrastructure shims.
private static readonly HashSet<string> SkipByName = new(StringComparer.OrdinalIgnoreCase)
{ "mscorlib", "netstandard", "System.Private.CoreLib", "dotnet" };

private static readonly ConcurrentDictionary<string, byte> Logged = new(StringComparer.OrdinalIgnoreCase);

private static int _subscribed;

/// <summary>
/// Subscribes to <see cref="AppDomain.AssemblyLoad"/> and logs assemblies as they load.
/// Also scans assemblies already loaded at call time. Subscription is established first
/// so no assembly loaded during the initial scan is missed.
/// </summary>
internal static void Subscribe(IApmLogger logger)
{
if (Interlocked.Exchange(ref _subscribed, 1) == 1)
return;

AppDomain.CurrentDomain.AssemblyLoad += (_, args) => TryLog(logger, args.LoadedAssembly);

foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
TryLog(logger, assembly);
}

private static void TryLog(IApmLogger logger, Assembly assembly)
{
if (!logger.IsEnabled(LogLevel.Debug))
return;

try
{
var name = assembly.GetName();
if (string.IsNullOrEmpty(name.Name) || SkipByName.Contains(name.Name))
return;

// Key on name + assembly version: the same library name can appear multiple times in a
// process when different versions are loaded into separate AssemblyLoadContexts (e.g.
// plugin hosts, or the agent isolating its own dependencies). Each distinct binding
// version is worth logging — it is precisely these conflicts that cause support issues.
// Keying on assembly version (not informational version) matches the CLR binding identity.
var assemblyVersion = name.Version?.ToString() ?? "unknown";
if (!Logged.TryAdd($"{name.Name}|{assemblyVersion}", 0))
return;

// AssemblyInformationalVersion is the most precise — it carries the full semantic
// version including pre-release labels and git commit SHA for official packages.
// AssemblyVersion is logged separately because it is often locked to a major version
// for binary compatibility and may differ significantly from the actual release version.
var informationalVersion = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
var location = assembly.Location;

var tokenBytes = name.GetPublicKeyToken();
var publicKeyToken = tokenBytes is { Length: > 0 }
? BitConverter.ToString(tokenBytes).Replace("-", "").ToLowerInvariant()
: "null";

if (informationalVersion != null)
{
logger.Debug()?.Log(
"Assembly loaded: {AssemblyName}, AssemblyVersion={AssemblyVersion}, InformationalVersion={InformationalVersion}, PublicKeyToken={PublicKeyToken}, Location={Location}",
name.Name, assemblyVersion, informationalVersion, publicKeyToken, location);
}
else
{
logger.Debug()?.Log(
"Assembly loaded: {AssemblyName}, AssemblyVersion={AssemblyVersion}, PublicKeyToken={PublicKeyToken}, Location={Location}",
name.Name, assemblyVersion, publicKeyToken, location);
}
}
catch
{
logger.Error()?.Log("Failed to log loaded assembly");
}
}
}
2 changes: 2 additions & 0 deletions src/startuphook/Elastic.Apm.StartupHook.Loader/Loader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ public static void Initialize()

HostBuilderExtensions.UpdateServiceInformation(Agent.Instance.Service);

AssemblyLoadLogger.Subscribe(logger);

static void LoadDiagnosticSubscriber(IDiagnosticsSubscriber diagnosticsSubscriber, IApmLogger logger)
{
try
Expand Down
7 changes: 7 additions & 0 deletions src/startuphook/ElasticApmAgentStartupHook/StartupHook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ internal class StartupHook
/// </summary>
public static void Initialize()
{
// DOTNET_STARTUP_HOOKS applies to every .NET process in the environment, including the
// dotnet CLI itself when using 'dotnet run', 'dotnet build', 'dotnet watch', etc.
// Those CLI host processes are not user applications — bail out immediately so we don't
// waste resources initialising the agent in them or produce noise log files.
if (string.Equals(Assembly.GetEntryAssembly()?.GetName().Name, "dotnet", StringComparison.OrdinalIgnoreCase))
return;

Logger = StartupHookLogger.Create();

if (!IsNet8OrHigher())
Expand Down
Loading