diff --git a/Datadog.Trace.sln b/Datadog.Trace.sln index f2aab4956baa..09bdab4e9602 100644 --- a/Datadog.Trace.sln +++ b/Datadog.Trace.sln @@ -401,6 +401,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples.AspNetCore2", "test EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Datadog.Instrumentation", "src\Datadog.Instrumentation\Datadog.Instrumentation.csproj", "{CF364E70-F5B5-4D44-B29E-2165525D3A69}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LogsInjection.ILogger", "test\test-applications\integrations\LogsInjection.ILogger\LogsInjection.ILogger.csproj", "{463A6FB2-1ABE-4B92-A470-97134D0BBC7E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1668,6 +1670,16 @@ Global {CF364E70-F5B5-4D44-B29E-2165525D3A69}.Release|x64.Build.0 = Release|Any CPU {CF364E70-F5B5-4D44-B29E-2165525D3A69}.Release|x86.ActiveCfg = Release|Any CPU {CF364E70-F5B5-4D44-B29E-2165525D3A69}.Release|x86.Build.0 = Release|Any CPU + {463A6FB2-1ABE-4B92-A470-97134D0BBC7E}.Debug|x64.ActiveCfg = Debug|x64 + {463A6FB2-1ABE-4B92-A470-97134D0BBC7E}.Debug|x64.Build.0 = Debug|x64 + {463A6FB2-1ABE-4B92-A470-97134D0BBC7E}.Debug|x86.ActiveCfg = Debug|x86 + {463A6FB2-1ABE-4B92-A470-97134D0BBC7E}.Debug|x86.Build.0 = Debug|x86 + {463A6FB2-1ABE-4B92-A470-97134D0BBC7E}.Release|x64.ActiveCfg = Release|x64 + {463A6FB2-1ABE-4B92-A470-97134D0BBC7E}.Release|x64.Build.0 = Release|x64 + {463A6FB2-1ABE-4B92-A470-97134D0BBC7E}.Release|x86.ActiveCfg = Release|x86 + {463A6FB2-1ABE-4B92-A470-97134D0BBC7E}.Release|x86.Build.0 = Release|x86 + {463A6FB2-1ABE-4B92-A470-97134D0BBC7E}.Debug|Any CPU.ActiveCfg = Debug|x86 + {463A6FB2-1ABE-4B92-A470-97134D0BBC7E}.Release|Any CPU.ActiveCfg = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1803,6 +1815,7 @@ Global {7C66569C-1174-49AF-8DA7-8B216685C1D4} = {8CEC2042-F11C-49F5-A674-2355793B600A} {8A73100E-F2C3-44D3-A5B8-49B5BFDF1B52} = {0972AD57-B16B-494F-AE0A-091DD6F3B42B} {CF364E70-F5B5-4D44-B29E-2165525D3A69} = {9E5F0022-0A50-40BF-AC6A-C3078585ECAB} + {463A6FB2-1ABE-4B92-A470-97134D0BBC7E} = {BAF8F246-3645-42AD-B1D0-0F7EAFBAB34A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {160A1D00-1F5B-40F8-A155-621B4459D78F} diff --git a/integrations.json b/integrations.json index 281052ad6145..22eb2822a6b6 100644 --- a/integrations.json +++ b/integrations.json @@ -1401,6 +1401,59 @@ } ] }, + { + "name": "ILogger", + "method_replacements": [ + { + "caller": {}, + "target": { + "assembly": "Microsoft.Extensions.Logging.Abstractions", + "type": "Microsoft.Extensions.Logging.LoggerExternalScopeProvider", + "method": "ForEachScope", + "signature_types": [ + "System.Void", + "System.Action`2[System.Object,!!0]", + "!!0" + ], + "minimum_major": 2, + "minimum_minor": 0, + "minimum_patch": 0, + "maximum_major": 5, + "maximum_minor": 65535, + "maximum_patch": 65535 + }, + "wrapper": { + "assembly": "Datadog.Trace, Version=1.28.3.0, Culture=neutral, PublicKeyToken=def86d061d0d2eeb", + "type": "Datadog.Trace.ClrProfiler.AutoInstrumentation.ILogger.LoggerExternalScopeProviderForEachScopeIntegration", + "action": "CallTargetModification" + } + }, + { + "caller": {}, + "target": { + "assembly": "Microsoft.Extensions.Logging", + "type": "Microsoft.Extensions.Logging.LoggerFactoryScopeProvider", + "method": "ForEachScope", + "signature_types": [ + "System.Void", + "System.Action`2[System.Object,!!0]", + "!!0" + ], + "minimum_major": 2, + "minimum_minor": 0, + "minimum_patch": 0, + "maximum_major": 5, + "maximum_minor": 65535, + "maximum_patch": 65535 + }, + "wrapper": { + "assembly": "Datadog.Trace, Version=1.28.3.0, Culture=neutral, PublicKeyToken=def86d061d0d2eeb", + "type": "Datadog.Trace.ClrProfiler.AutoInstrumentation.ILogger.LoggerFactoryScopeProviderForEachScopeIntegration", + "action": "CallTargetModification" + } + } + ] + }, { "name": "Kafka", "method_replacements": [ diff --git a/src/Datadog.Trace.ClrProfiler.Native/dd_profiler_constants.h b/src/Datadog.Trace.ClrProfiler.Native/dd_profiler_constants.h index 97be57a3907d..9fe8aa65e5d7 100644 --- a/src/Datadog.Trace.ClrProfiler.Native/dd_profiler_constants.h +++ b/src/Datadog.Trace.ClrProfiler.Native/dd_profiler_constants.h @@ -40,7 +40,22 @@ const WSTRING skip_assembly_prefixes[]{ WStr("Microsoft.ApplicationInsights"), WStr("Microsoft.Build"), WStr("Microsoft.CSharp"), - WStr("Microsoft.Extensions"), + WStr("Microsoft.Extensions.Caching"), + WStr("Microsoft.Extensions.Configuration"), + WStr("Microsoft.Extensions.DependencyInjection"), + WStr("Microsoft.Extensions.DependencyModel"), + WStr("Microsoft.Extensions.Diagnostics"), + WStr("Microsoft.Extensions.FileProviders"), + WStr("Microsoft.Extensions.FileSystemGlobbing"), + WStr("Microsoft.Extensions.Hosting"), + WStr("Microsoft.Extensions.Http"), + WStr("Microsoft.Extensions.Identity"), + WStr("Microsoft.Extensions.Localization"), + WStr("Microsoft.Extensions.ObjectPool"), + WStr("Microsoft.Extensions.Options"), + WStr("Microsoft.Extensions.PlatformAbstractions"), + WStr("Microsoft.Extensions.Primitives"), + WStr("Microsoft.Extensions.WebEncoders "), WStr("Microsoft.Web.Compilation.Snapshots"), WStr("System.Core"), WStr("System.Console"), diff --git a/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ILogger/DatadogLoggingScope.cs b/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ILogger/DatadogLoggingScope.cs new file mode 100644 index 000000000000..5d7618aef4cc --- /dev/null +++ b/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ILogger/DatadogLoggingScope.cs @@ -0,0 +1,84 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; + +namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.ILogger +{ + internal class DatadogLoggingScope : IReadOnlyList> + { + private readonly string _service; + private readonly string _env; + private readonly string _version; + private readonly Tracer _tracer; + + public DatadogLoggingScope() + : this(Tracer.Instance) + { + } + + internal DatadogLoggingScope(Tracer tracer) + { + _tracer = tracer; + _service = tracer.DefaultServiceName ?? string.Empty; + _env = tracer.Settings.Environment ?? string.Empty; + _version = tracer.Settings.ServiceVersion ?? string.Empty; + } + + public int Count => 5; + + public KeyValuePair this[int index] + { + get + { + return index switch + { + 0 => new KeyValuePair("dd_service", _tracer.ActiveScope?.Span.ServiceName ?? _service), + 1 => new KeyValuePair("dd_env", _env), + 2 => new KeyValuePair("dd_version", _version), + 3 => new KeyValuePair("dd_trace_id", _tracer.ActiveScope?.Span.TraceId), + 4 => new KeyValuePair("dd_span_id", _tracer.ActiveScope?.Span.SpanId), + _ => throw new ArgumentOutOfRangeException(nameof(index)) + }; + } + } + + public override string ToString() + { + var span = _tracer.ActiveScope?.Span; + + return string.Format( + CultureInfo.InvariantCulture, + "dd_service:{0}, dd_env:{1}, dd_version:{2}, dd_trace_id:{3}, dd_span_id:{4}", + span?.ServiceName ?? _service, + _env, + _version, + _tracer.ActiveScope?.Span.TraceId, + _tracer.ActiveScope?.Span.SpanId); + } + + public IEnumerator> GetEnumerator() + { + var span = _tracer.ActiveScope?.Span; + yield return new KeyValuePair("dd_service", span?.ServiceName ?? _service); + yield return new KeyValuePair("dd_env", _env); + yield return new KeyValuePair("dd_version", _version); + + if (span is not null) + { + yield return new KeyValuePair("dd_trace_id", span.TraceId); + yield return new KeyValuePair("dd_span_id", span.SpanId); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ILogger/LoggerExternalScopeProviderForEachScopeIntegration.cs b/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ILogger/LoggerExternalScopeProviderForEachScopeIntegration.cs new file mode 100644 index 000000000000..506b2c0ccb0d --- /dev/null +++ b/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ILogger/LoggerExternalScopeProviderForEachScopeIntegration.cs @@ -0,0 +1,57 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.ComponentModel; +using Datadog.Trace.ClrProfiler.CallTarget; + +namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.ILogger +{ + /// + /// LoggerExternalScopeProvider.ForEach<TState> calltarget instrumentation + /// + [InstrumentMethod( + AssemblyName = "Microsoft.Extensions.Logging.Abstractions", + TypeName = "Microsoft.Extensions.Logging.LoggerExternalScopeProvider", + MethodName = "ForEachScope", + ReturnTypeName = ClrNames.Void, + ParameterTypeNames = new[] { "System.Action`2[System.Object,!!0]", "!!0" }, + MinimumVersion = "2.0.0", + MaximumVersion = "5.*.*", + IntegrationName = LoggerIntegrationCommon.IntegrationName)] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public class LoggerExternalScopeProviderForEachScopeIntegration + { + /// + /// OnMethodBegin callback + /// + /// Type of the target + /// The type of the action + /// The type of the state + /// Instance value, aka `this` of the instrumented method. + /// The callback to be invoked per scope + /// The state to pass to the callback + /// Calltarget state value + public static CallTargetState OnMethodBegin(TTarget instance, TAction callback, TState state) + { + LoggerIntegrationCommon.AddScope(Tracer.Instance, callback, state); + return new CallTargetState(scope: null, state: null); + } + + /// + /// OnMethodEnd callback + /// + /// Type of the target + /// Instance value, aka `this` of the instrumented method. + /// Exception instance in case the original code threw an exception. + /// Calltarget state value + /// A response value, in an async scenario will be T of Task of T + public static CallTargetReturn OnMethodEnd(TTarget instance, Exception exception, CallTargetState state) + { + return CallTargetReturn.GetDefault(); + } + } +} diff --git a/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ILogger/LoggerFactoryScopeProviderForEachScopeIntegration.cs b/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ILogger/LoggerFactoryScopeProviderForEachScopeIntegration.cs new file mode 100644 index 000000000000..e0b7f2df2dda --- /dev/null +++ b/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ILogger/LoggerFactoryScopeProviderForEachScopeIntegration.cs @@ -0,0 +1,57 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.ComponentModel; +using Datadog.Trace.ClrProfiler.CallTarget; + +namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.ILogger +{ + /// + /// LoggerFactoryScopeProvider.ForEach<TState> calltarget instrumentation + /// + [InstrumentMethod( + AssemblyName = "Microsoft.Extensions.Logging", + TypeName = "Microsoft.Extensions.Logging.LoggerFactoryScopeProvider", + MethodName = "ForEachScope", + ReturnTypeName = ClrNames.Void, + ParameterTypeNames = new[] { "System.Action`2[System.Object,!!0]", "!!0" }, + MinimumVersion = "2.0.0", + MaximumVersion = "5.*.*", + IntegrationName = LoggerIntegrationCommon.IntegrationName)] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public class LoggerFactoryScopeProviderForEachScopeIntegration + { + /// + /// OnMethodBegin callback + /// + /// Type of the target + /// The type of the action + /// The type of the state + /// Instance value, aka `this` of the instrumented method. + /// The callback to be invoked per scope + /// The state to pass to the callback + /// Calltarget state value + public static CallTargetState OnMethodBegin(TTarget instance, TAction callback, TState state) + { + LoggerIntegrationCommon.AddScope(Tracer.Instance, callback, state); + return new CallTargetState(scope: null, state: null); + } + + /// + /// OnMethodEnd callback + /// + /// Type of the target + /// Instance value, aka `this` of the instrumented method. + /// Exception instance in case the original code threw an exception. + /// Calltarget state value + /// A response value, in an async scenario will be T of Task of T + public static CallTargetReturn OnMethodEnd(TTarget instance, Exception exception, CallTargetState state) + { + return CallTargetReturn.GetDefault(); + } + } +} diff --git a/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ILogger/LoggerIntegrationCommon.cs b/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ILogger/LoggerIntegrationCommon.cs new file mode 100644 index 000000000000..d77629c2daaa --- /dev/null +++ b/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ILogger/LoggerIntegrationCommon.cs @@ -0,0 +1,28 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using Datadog.Trace.Configuration; + +namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.ILogger +{ + internal static class LoggerIntegrationCommon + { + public const string IntegrationName = nameof(IntegrationIds.ILogger); + private static readonly IntegrationInfo IntegrationId = IntegrationRegistry.GetIntegrationInfo(IntegrationName); + + private static readonly DatadogLoggingScope DatadogScope = new(); + + public static void AddScope(Tracer tracer, TAction callback, TState state) + { + if (tracer.Settings.LogsInjectionEnabled + && tracer.Settings.IsIntegrationEnabled(IntegrationId) + && callback is Action foreachCallback) + { + foreachCallback.Invoke(DatadogScope, state); + } + } + } +} diff --git a/src/Datadog.Trace/Configuration/IntegrationIds.cs b/src/Datadog.Trace/Configuration/IntegrationIds.cs index 72ac70234c3c..45fe0772b8ac 100644 --- a/src/Datadog.Trace/Configuration/IntegrationIds.cs +++ b/src/Datadog.Trace/Configuration/IntegrationIds.cs @@ -34,5 +34,6 @@ internal enum IntegrationIds CosmosDb, AwsSdk, AwsSqs, + ILogger, } } diff --git a/test/Datadog.Trace.ClrProfiler.IntegrationTests/ILoggerTests.cs b/test/Datadog.Trace.ClrProfiler.IntegrationTests/ILoggerTests.cs new file mode 100644 index 000000000000..37da41102ee3 --- /dev/null +++ b/test/Datadog.Trace.ClrProfiler.IntegrationTests/ILoggerTests.cs @@ -0,0 +1,56 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#if !NET452 +using System; +using Datadog.Trace.TestHelpers; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Datadog.Trace.ClrProfiler.IntegrationTests +{ + // ReSharper disable once InconsistentNaming + public class ILoggerTests : LogsInjectionTestBase + { + private readonly LogFileTest[] _logFiles = + { + new LogFileTest + { + FileName = "simple.log", + RegexFormat = @"""{0}"":{1}", + UnTracedLogTypes = UnTracedLogTypes.EnvServiceTracingPropertiesOnly, + PropertiesUseSerilogNaming = true + }, + }; + + public ILoggerTests(ITestOutputHelper output) + : base(output, "LogsInjection.ILogger") + { + SetServiceVersion("1.0.0"); + SetEnvironmentVariable("DD_LOGS_INJECTION", "true"); + SetCallTargetSettings(true); + } + + [Fact] + [Trait("Category", "EndToEnd")] + [Trait("RunOnWindows", "True")] + public void InjectsLogs() + { + int agentPort = TcpPortProvider.GetOpenPort(); + using (var agent = new MockTracerAgent(agentPort)) + using (var processResult = RunSampleAndWaitForExit(agent.Port, aspNetCorePort: 0)) + { + Assert.True(processResult.ExitCode >= 0, $"Process exited with code {processResult.ExitCode} and exception: {processResult.StandardError}"); + + var spans = agent.WaitForSpans(1, 2500); + spans.Should().HaveCountGreaterOrEqualTo(1); + + ValidateLogCorrelation(spans, _logFiles); + } + } + } +} +#endif diff --git a/test/Datadog.Trace.ClrProfiler.IntegrationTests/LogsInjectionTestBase.cs b/test/Datadog.Trace.ClrProfiler.IntegrationTests/LogsInjectionTestBase.cs index 3f6750d00f39..b7643d11a7d8 100644 --- a/test/Datadog.Trace.ClrProfiler.IntegrationTests/LogsInjectionTestBase.cs +++ b/test/Datadog.Trace.ClrProfiler.IntegrationTests/LogsInjectionTestBase.cs @@ -105,7 +105,7 @@ public void ValidateLogCorrelation(IReadOnlyCollection spa var logs = GetLogFileContents(logFilePath); logs.Should().NotBeNullOrEmpty(); - // using var s = new AssertionScope(test.FileName); + using var s = new AssertionScope(test.FileName); // Assumes we _only_ have logs for logs within traces + our startup log additionalInjectedLogFilter ??= (_) => true; diff --git a/test/Datadog.Trace.TestHelpers/TestHelper.cs b/test/Datadog.Trace.TestHelpers/TestHelper.cs index b0cc9bd55dbe..e1396b9ab9ad 100644 --- a/test/Datadog.Trace.TestHelpers/TestHelper.cs +++ b/test/Datadog.Trace.TestHelpers/TestHelper.cs @@ -137,9 +137,9 @@ public Process StartSample(int traceAgentPort, string arguments, string packageV processToProfile: executable); } - public ProcessResult RunSampleAndWaitForExit(int traceAgentPort, int? statsdPort = null, string arguments = null, string packageVersion = "", string framework = "") + public ProcessResult RunSampleAndWaitForExit(int traceAgentPort, int? statsdPort = null, string arguments = null, string packageVersion = "", string framework = "", int aspNetCorePort = 5000) { - var process = StartSample(traceAgentPort, arguments, packageVersion, aspNetCorePort: 5000, statsdPort: statsdPort, framework: framework); + var process = StartSample(traceAgentPort, arguments, packageVersion, aspNetCorePort: aspNetCorePort, statsdPort: statsdPort, framework: framework); using var helper = new ProcessHelper(process); diff --git a/test/test-applications/integrations/LogsInjection.ILogger/LogHelper.cs b/test/test-applications/integrations/LogsInjection.ILogger/LogHelper.cs new file mode 100644 index 000000000000..c2eb2b7f71d5 --- /dev/null +++ b/test/test-applications/integrations/LogsInjection.ILogger/LogHelper.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Logging; + +namespace LogsInjection.ILogger +{ + public static class LogHelper + { + public const string ExcludeMessagePrefix = "[ExcludeMessage]"; + + public static void UninjectedLog(this Microsoft.Extensions.Logging.ILogger logger, string message) + { + logger.LogInformation($"{ExcludeMessagePrefix}{message}"); + } + + public static void ConditionalLog(this Microsoft.Extensions.Logging.ILogger logger, string message) + { +#if NETCOREAPP + logger.LogInformation(message); +#else + // We don't instrument on .NET Framework, so we don't expect this to be log injected + logger.UninjectedLog(message); +#endif + } + + public static void ConfigureCustomLogging(WebHostBuilderContext ctx, ILoggingBuilder logging) + { + // delete existing log file + var logFile = Path.Combine(ctx.HostingEnvironment.ContentRootPath, "simple.log"); + if (File.Exists(logFile)) + { + try + { + File.Delete(logFile); + } + catch + { + // Don't throw if something's amiss + } + } + + logging.AddProvider(new SimpleLogProvider(logFile)); + } + + public class SimpleLogProvider : ILoggerProvider, ISupportExternalScope + { + private readonly string _logPath; + + public SimpleLogProvider(string logPath) + { + _logPath = logPath; + } + + internal IExternalScopeProvider ScopeProvider { get; private set; } + + public Microsoft.Extensions.Logging.ILogger CreateLogger(string categoryName) => new SimpleLogger(this, categoryName, _logPath); + + public void Dispose() + { + } + + void ISupportExternalScope.SetScopeProvider(IExternalScopeProvider scopeProvider) + { + ScopeProvider = scopeProvider; + } + + public class SimpleLogger : Microsoft.Extensions.Logging.ILogger + { + private readonly SimpleLogProvider _provider; + private readonly string _logPath; + private readonly string _category; + + public SimpleLogger(SimpleLogProvider loggerProvider, string categoryName, string logPath) + { + _provider = loggerProvider; + _category = categoryName; + _logPath = logPath; + } + + public IDisposable BeginScope(TState state) + { + return _provider.ScopeProvider?.Push(state); + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log(DateTimeOffset timestamp, LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + var scopes = new Dictionary(); + var scopeProvider = _provider.ScopeProvider; + if (scopeProvider != null) + { + scopeProvider.ForEachScope( + (scope, builder) => + { + if (scope is IEnumerable> pairs) + { + foreach (var kvp in pairs) + { + builder[kvp.Key] = kvp.Value; + } + } + else + { + var temp = builder.TryGetValue("scopes", out var rawScope) + ? (List)rawScope + : new List(); + temp.Add(scope); + builder["scopes"] = temp; + } + }, + scopes); + } +#if NETCOREAPP2_1 || !NETCOREAPP + var log = Newtonsoft.Json.JsonConvert.SerializeObject( + new + { + timestamp = timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff zzz"), + category = _category, + scopes = scopes, + message = formatter(state, exception), + exception = exception?.Message + }, Newtonsoft.Json.Formatting.None); +#else + var log = System.Text.Json.JsonSerializer.Serialize( + new + { + timestamp = timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff zzz"), + category = _category, + scopes = scopes, + message = formatter(state, exception), + exception = exception?.Message + }, new System.Text.Json.JsonSerializerOptions() + { + WriteIndented = false, + IgnoreNullValues = true, + }); +#endif + + + File.AppendAllText(_logPath, log + Environment.NewLine); + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + Log(DateTimeOffset.UtcNow, logLevel, eventId, state, exception, formatter); + } + } + } + } +} diff --git a/test/test-applications/integrations/LogsInjection.ILogger/LogsInjection.ILogger.csproj b/test/test-applications/integrations/LogsInjection.ILogger/LogsInjection.ILogger.csproj new file mode 100644 index 000000000000..eb400f6393ee --- /dev/null +++ b/test/test-applications/integrations/LogsInjection.ILogger/LogsInjection.ILogger.csproj @@ -0,0 +1,29 @@ + + + + net461;netcoreapp2.1;netcoreapp3.0;netcoreapp3.1;net5.0 + false + false + true + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/test-applications/integrations/LogsInjection.ILogger/Program.cs b/test/test-applications/integrations/LogsInjection.ILogger/Program.cs new file mode 100644 index 000000000000..fee90b9d0254 --- /dev/null +++ b/test/test-applications/integrations/LogsInjection.ILogger/Program.cs @@ -0,0 +1,22 @@ +using System; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; + +namespace LogsInjection.ILogger +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseContentRoot(AppContext.BaseDirectory) + .UseSetting(WebHostDefaults.SuppressStatusMessagesKey, "True") + .ConfigureLogging(LogHelper.ConfigureCustomLogging) + .UseStartup(); + + } +} diff --git a/test/test-applications/integrations/LogsInjection.ILogger/Properties/launchSettings.json b/test/test-applications/integrations/LogsInjection.ILogger/Properties/launchSettings.json new file mode 100644 index 000000000000..f43064c6e051 --- /dev/null +++ b/test/test-applications/integrations/LogsInjection.ILogger/Properties/launchSettings.json @@ -0,0 +1,25 @@ +{ + "profiles": { + "LogsInjection.ILogger": { + "commandName": "Project", + "dotnetRunMessages": "false", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_URLS": "http://127.0.0.1:0", + + "COR_ENABLE_PROFILING": "1", + "COR_PROFILER": "{846F5F1C-F9AE-4B07-969E-05C26BC060D8}", + "COR_PROFILER_PATH": "$(ProjectDir)$(OutputPath)profiler-lib\\Datadog.Trace.ClrProfiler.Native.dll", + + "CORECLR_ENABLE_PROFILING": "1", + "CORECLR_PROFILER": "{846F5F1C-F9AE-4B07-969E-05C26BC060D8}", + "CORECLR_PROFILER_PATH": "$(ProjectDir)$(OutputPath)profiler-lib\\Datadog.Trace.ClrProfiler.Native.dll", + + "DD_DOTNET_TRACER_HOME": "$(ProjectDir)$(OutputPath)profiler-lib", + "DD_INTEGRATIONS": "$(ProjectDir)$(OutputPath)profiler-lib\\integrations.json", + "DD_VERSION": "1.0.0", + "DD_LOGS_INJECTION": "1" + } + } + } +} \ No newline at end of file diff --git a/test/test-applications/integrations/LogsInjection.ILogger/Startup.cs b/test/test-applications/integrations/LogsInjection.ILogger/Startup.cs new file mode 100644 index 000000000000..b89aee993c05 --- /dev/null +++ b/test/test-applications/integrations/LogsInjection.ILogger/Startup.cs @@ -0,0 +1,56 @@ +using System.Linq; +using Datadog.Trace; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace LogsInjection.ILogger +{ + public class Startup + { + public static volatile bool AppListening = false; + public static volatile string ServerAddress = null; + public void ConfigureServices(IServiceCollection services) + { + services.AddHostedService(); + services.AddHttpClient(); + } + +#pragma warning disable 618 // ignore obsolete IApplicationLifetime + public void Configure(IApplicationBuilder app, IApplicationLifetime lifetime, ILogger logger) +#pragma warning restore 618 + { + // Not injected as we won't have a traceId + logger.UninjectedLog("Building pipeline"); + using (var scope = Tracer.Instance.StartActive("pipeline build")) + { + logger.LogInformation("Still building pipeline..."); + } + + // Register a callback to run after the app is fully configured + lifetime.ApplicationStarted.Register(() => + { + ServerAddress = app.ServerFeatures.Get().Addresses.First(); + AppListening = true; + }); + + app.Use((httpContext, next) => + { + logger.ConditionalLog($"Visited {httpContext.Request.Path}"); + return next(); + }); + + app.Run(context => + { + logger.ConditionalLog("Received request, echoing"); + + using var scope = Tracer.Instance.StartActive("middleware execution"); + logger.LogInformation("Sending response"); + return context.Response.WriteAsync("PONG"); + }); + } + } +} diff --git a/test/test-applications/integrations/LogsInjection.ILogger/Worker.cs b/test/test-applications/integrations/LogsInjection.ILogger/Worker.cs new file mode 100644 index 000000000000..84ebd063fd70 --- /dev/null +++ b/test/test-applications/integrations/LogsInjection.ILogger/Worker.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace LogsInjection.ILogger +{ + public class Worker : BackgroundService + { + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; +#pragma warning disable 618 // ignore obsolete IApplicationLifetime + private readonly IApplicationLifetime _lifetime; + + public Worker(ILogger logger, IApplicationLifetime lifetime, IServiceProvider serviceProvider) +#pragma warning restore 618 + { + _logger = logger; + _lifetime = lifetime; + _serviceProvider = serviceProvider; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!Startup.AppListening && !stoppingToken.IsCancellationRequested) + { + _logger.UninjectedLog("Waiting for app started handling requests"); + await Task.Delay(100, stoppingToken); + } + + if (stoppingToken.IsCancellationRequested) + { + _logger.UninjectedLog("Cancellation requested."); + return; + } + + using (var scope = Datadog.Trace.Tracer.Instance.StartActive("worker request")) + { + try + { + using var serviceScope = _serviceProvider.CreateScope(); + var httpClientFactory = serviceScope.ServiceProvider.GetRequiredService(); + var client = httpClientFactory.CreateClient(); + + _logger.LogInformation("Sending request to self"); + var response = await client.GetAsync(Startup.ServerAddress, stoppingToken); + + if (!response.IsSuccessStatusCode) + { + _logger.LogError("Error sending request, status code did not indicate success"); + response.EnsureSuccessStatusCode(); + } + + _logger.LogInformation("Request sent successfully"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending request"); + } + } + + _logger.UninjectedLog("Stopping application"); + + // trigger app shutdown + _lifetime.StopApplication(); + } + } +} diff --git a/test/test-applications/integrations/LogsInjection.ILogger/appsettings.json b/test/test-applications/integrations/LogsInjection.ILogger/appsettings.json new file mode 100644 index 000000000000..8983e0fc1c5e --- /dev/null +++ b/test/test-applications/integrations/LogsInjection.ILogger/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +}