From 60e8cd8aa7aff446f0a0c8033fb768dcd8a826b1 Mon Sep 17 00:00:00 2001 From: Amanda Tarafa Mas Date: Mon, 21 Jun 2021 09:07:11 +0100 Subject: [PATCH] feat: Copies GoogleLogger to Common. This allows easier use of GoogleLogger in non ASP.NET Core applications. Towards #6367 - Replicate LoggerOptions in Common. - And have AspNetCore*.LoggerOptions be just a wrapper of Common.LoggerOptions. - Copies ILogEntryLabelProvider to Common. - And marks the one in AspNetCore* Obsolete. - Makes AspNetCore*.IExternalTraceProvider obsolete. - It can now be replaced by Common.ITraceContext. --- .../Logging/LoggingTest.cs | 75 ++++- .../Logging/GoogleLoggerTest.cs | 47 +-- .../Logging/GoogleTraceProviderTests.cs | 3 + .../LabelProviderExtensionsTest.cs | 6 +- .../Logging/TraceContextForLogEntryTests.cs | 2 + .../Logging/GoogleLogger.cs | 249 +------------- .../Logging/GoogleLoggerProvider.cs | 105 +++--- .../Logging/GoogleTraceProvider.cs | 1 + .../Logging/IExternalTraceProvider.cs | 1 + .../Logging/ILogEntryLabelProvider.cs | 18 +- .../EnvironmentNameLogEntryLabelProvider.cs | 4 +- .../HttpLogEntryLabelProvider.cs | 4 +- .../LabelProviders/LabelProviderExtensions.cs | 26 +- .../Logging/LoggerOptions.cs | 63 +--- .../Logging/TraceContextForLogEntry.cs | 1 + .../Trace/CloudTraceExtension.cs | 19 +- .../Logging/LogLevelExtensionsTest.cs} | 10 +- .../KeyValuePairEnumerableExtensions.cs | 4 +- .../Logging/GoogleLogger.cs | 310 ++++++++++++++++++ .../Logging/GoogleLoggerProvider.cs | 122 +++++++ .../Logging/GoogleLoggerScope.cs | 2 +- .../Logging/ILogEntryLabelProvider.cs | 34 ++ .../Logging/ILoggerExtensions.cs | 24 +- .../Logging/LabellingScope.cs | 2 +- .../Logging/LogLevelExtensions.cs} | 17 +- .../Logging/LoggerOptions.cs | 218 ++++++++++++ 26 files changed, 922 insertions(+), 445 deletions(-) rename apis/{Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/LogUtilsTest.cs => Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common.Tests/Logging/LogLevelExtensionsTest.cs} (86%) create mode 100644 apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/GoogleLogger.cs create mode 100644 apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/GoogleLoggerProvider.cs create mode 100644 apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/ILogEntryLabelProvider.cs rename apis/{Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/LogUtils.cs => Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/LogLevelExtensions.cs} (82%) create mode 100644 apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/LoggerOptions.cs diff --git a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.IntegrationTests/Logging/LoggingTest.cs b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.IntegrationTests/Logging/LoggingTest.cs index 8dd346d89cfb..015375996b86 100644 --- a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.IntegrationTests/Logging/LoggingTest.cs +++ b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.IntegrationTests/Logging/LoggingTest.cs @@ -519,6 +519,8 @@ public static object[][] ExternalTraceBuilders { return new object[][] { + new object[] { GetTestServer() }, + new object[] { GetTestServer() }, new object[] { GetTestServer() }, new object[] { GetTestServer() } }; @@ -546,10 +548,10 @@ public async Task Logging_Trace_External_OneEntry(TestServer server) // And the resource name of the trace associated to it contains the external trace id. Assert.Contains(TestEnvironment.GetTestProjectId(), entry.Trace); - Assert.Contains("external_trace_id", entry.Trace); + Assert.Contains("105445aa7843bc8bf206b12000100f00", entry.Trace); // The span associated to our entry is the external span. - Assert.Equal("external_span_number1", entry.SpanId); + Assert.Equal("0000000000000001", entry.SpanId); }); } @@ -578,12 +580,14 @@ public async Task Logging_Trace_External_MultipleEntries(TestServer server) Assert.All(results, entry => { Assert.Contains(projectId, entry.Trace); - Assert.Contains("external_trace_id", entry.Trace); + Assert.Contains("105445aa7843bc8bf206b12000100f00", entry.Trace); }); // The span associated to our entry is the external span, and is the same span number // as the log entry. - Assert.All(results, entry => Assert.Equal($"external_span_number{entry.JsonPayload.Fields["message"].StringValue.Last()}", entry.SpanId)); + Assert.All(results, entry => Assert.Equal($"{ExpectedSpanId(entry):x16}", entry.SpanId)); + static ulong ExpectedSpanId(LogEntry entry) => + Convert.ToUInt64(int.Parse($"{entry.JsonPayload.Fields["message"].StringValue.Last()}")); }); } @@ -668,9 +672,10 @@ public async Task Logging_Labels() _fixture.AddValidator(testId, results => { var entry = results.Single(); - Assert.Equal(3, entry.Labels.Count); + Assert.Equal(4, entry.Labels.Count); Assert.Equal("some-value", entry.Labels["some-key"]); Assert.Equal("Hello, World!", entry.Labels["Foo"]); + Assert.Equal("Hello, World!", entry.Labels["Bar"]); Assert.NotEmpty(entry.Labels["trace_identifier"]); }); } @@ -828,16 +833,28 @@ public override void Configure(IApplicationBuilder app, ILoggerFactory loggerFac app.ApplicationServices, _projectId, LoggerOptions.Create(logLevel: LogLevel.Warning, bufferOptions: BufferOptions.NoBuffer()))); } - public class ExternalTracingTestApplication : LoggerNoTracingActivatedTestApplication + public class ObsoleteExternalTracingTestApplication : LoggerNoTracingActivatedTestApplication { public override void ConfigureServices(IServiceCollection services) => +#pragma warning disable CS0618 // Type or member is obsolete base.ConfigureServices(services.AddSingleton()); +#pragma warning restore CS0618 // Type or member is obsolete + } + + public class ExternalTracingTestApplication : LoggerNoTracingActivatedTestApplication + { + public override void ConfigureServices(IServiceCollection services) => + base.ConfigureServices(services + // We start counting at -1 here because of the call we make on the middleware to + // try and obtain the context. We don't care about that one. + .AddSingleton(new SpanCountingITraceContextFactory(0)) + .AddTransient(sp => sp.GetRequiredService().GetCurrentTraceContext())); } public class GoogleTraceAsExternalTracingTestApplication : LoggerNoTracingActivatedTestApplication { public override void ConfigureServices(IServiceCollection services) => - base.ConfigureServices(services.AddSingleton()); + base.ConfigureServices(services.TryAddGoogleTraceContextProvider()); } /// @@ -894,10 +911,22 @@ public NoBufferWarningLoggerTracesAllTestApplication() { } } - public class NoBufferWarningLoggerExternalTraceTestApplication : NoBufferWarningLoggerTestApplication + public class ObsoleteNoBufferWarningLoggerExternalTraceTestApplication : NoBufferWarningLoggerTestApplication { public override void ConfigureServices(IServiceCollection services) => +#pragma warning disable CS0618 // Type or member is obsolete base.ConfigureServices(services.AddSingleton(new SpanCountingExternalTraceProvider())); +#pragma warning restore CS0618 // Type or member is obsolete + } + + public class NoBufferWarningLoggerExternalTraceTestApplication : NoBufferWarningLoggerTestApplication + { + public override void ConfigureServices(IServiceCollection services) => + base.ConfigureServices(services + // We start counting at -1 here because of the call we make on the middleware to + // try and obtain the context. We don't care about that one. + .AddSingleton(new SpanCountingITraceContextFactory(-1)) + .AddTransient(sp => sp.GetRequiredService().GetCurrentTraceContext())); } /// @@ -924,7 +953,7 @@ public override void Configure(IApplicationBuilder app, ILoggerFactory loggerFac } /// - /// An application that has a and a , + /// An application that has a and a , /// that accept all logs of level warning or above. /// public class WarningWithLabelsLoggerTestApplication : LoggerTestApplication @@ -932,7 +961,10 @@ public class WarningWithLabelsLoggerTestApplication : LoggerTestApplication public override void ConfigureServices(IServiceCollection services) => base.ConfigureServices(services .AddLogEntryLabelProvider() - .AddLogEntryLabelProvider()); + .AddLogEntryLabelProvider() +#pragma warning disable CS0618 // Type or member is obsolete + .AddSingleton()); +#pragma warning restore CS0618 // Type or member is obsolete public override void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) => base.Configure(app, loggerFactory.AddGoogle( @@ -1141,7 +1173,7 @@ public string LogsThreeEntries(string id) } } - internal class FooLogEntryLabelProvider : ILogEntryLabelProvider + internal class FooLogEntryLabelProvider : Common.ILogEntryLabelProvider { public void Invoke(Dictionary labels) { @@ -1149,10 +1181,29 @@ public void Invoke(Dictionary labels) } } + [Obsolete("Added to test that we still support the obsolete ILogEntryLabelProvider.")] + internal class BarLogEntryLabelProvider : ILogEntryLabelProvider + { + public void Invoke(Dictionary labels) + { + labels["Bar"] = "Hello, World!"; + } + } + + internal class SpanCountingITraceContextFactory + { + public SpanCountingITraceContextFactory(int startAt) => _callCount = startAt; + private int _callCount = 0; + internal ITraceContext GetCurrentTraceContext() => + new SimpleTraceContext("105445aa7843bc8bf206b12000100f00", Convert.ToUInt64(Interlocked.Increment(ref _callCount)), false); + } + +#pragma warning disable CS0618 // Type or member is obsolete internal class SpanCountingExternalTraceProvider : IExternalTraceProvider { internal int _callCount = 0; public TraceContextForLogEntry GetCurrentTraceContext(IServiceProvider serviceProvider) => - new TraceContextForLogEntry("external_trace_id", $"external_span_number{Interlocked.Increment(ref _callCount)}"); + new TraceContextForLogEntry("105445aa7843bc8bf206b12000100f00", $"{Convert.ToUInt64(Interlocked.Increment(ref _callCount)):x16}"); } +#pragma warning restore CS0618 // Type or member is obsolete } diff --git a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/GoogleLoggerTest.cs b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/GoogleLoggerTest.cs index 4ac5779fad7d..15af0c2726f1 100644 --- a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/GoogleLoggerTest.cs +++ b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/GoogleLoggerTest.cs @@ -64,9 +64,15 @@ private GoogleLogger GetLogger( consumer ??= new Mock>(MockBehavior.Strict).Object; monitoredResource ??= MonitoredResourceBuilder.GlobalResource; logTarget ??= s_defaultLogTarget; - LoggerOptions options = LoggerOptions.CreateWithServiceContext( + Common.LoggerOptions options = Common.LoggerOptions.CreateWithServiceContext( logLevel, logName, labels, monitoredResource, retryOptions: retryOptions, serviceName: serviceName, version: version); - return new GoogleLogger(consumer, logTarget, options, LogName, s_clock, serviceProvider); +#pragma warning disable CS0618 // Type or member is obsolete + Common.GoogleLogger _innerLogger = new Common.GoogleLogger( + consumer, logTarget, options, LogName, + GoogleLoggerProvider.ObsoleteLabelsGetter, GoogleLoggerProvider.ObsoleteTraceContextGetter, + s_clock, serviceProvider); +#pragma warning restore CS0618 // Type or member is obsolete + return new GoogleLogger(_innerLogger); } [Fact] @@ -439,7 +445,7 @@ public void Log_NoFormatParams() public void Log_Trace() { string traceId = "external_trace_id"; - string spanId = "external_span_id"; + ulong spanId = 1; string fullTraceName = TraceTarget.ForProject(ProjectId).GetFullTraceName(traceId); Predicate> matcher = logEntries => @@ -447,15 +453,12 @@ public void Log_Trace() LogEntry entry = logEntries.Single(); return entry.LogName == new LogName(ProjectId, BaseLogName).ToString() && entry.Trace == fullTraceName && - entry.SpanId == spanId; + entry.SpanId == $"{spanId:x16}"; }; var mockServiceProvider = new Mock(); - var mockExternalTraceProvider = new Mock(); - mockServiceProvider.Setup(sp => sp.GetService(typeof(IExternalTraceProvider))).Returns(mockExternalTraceProvider.Object); - mockExternalTraceProvider.Setup( - sp => sp.GetCurrentTraceContext(It.IsAny())) - .Returns(new TraceContextForLogEntry(traceId, spanId)); + var traceContext = new SimpleTraceContext(traceId, spanId, true); + mockServiceProvider.Setup(sp => sp.GetService(typeof(ITraceContext))).Returns(traceContext); var mockConsumer = new Mock>(); mockConsumer.Setup(c => c.Receive(Match.Create(matcher))); @@ -481,8 +484,8 @@ public void Log_Labels() }; var mockServiceProvider = new Mock(); - mockServiceProvider.Setup(sp => sp.GetService(typeof(IEnumerable))) - .Returns(new ILogEntryLabelProvider[] { new FooLogEntryLabelProvider(), new BarLogEntryLabelProvider() }); + mockServiceProvider.Setup(sp => sp.GetService(typeof(IEnumerable))) + .Returns(new Common.ILogEntryLabelProvider[] { new FooLogEntryLabelProvider(), new BarLogEntryLabelProvider() }); var mockConsumer = new Mock>(); mockConsumer.Setup(c => c.Receive(Match.Create(matcher))); @@ -505,8 +508,8 @@ public void Log_Exception() }; var mockServiceProvider = new Mock(); - mockServiceProvider.Setup(sp => sp.GetService(typeof(IEnumerable))) - .Returns(new ILogEntryLabelProvider[] { new FooLogEntryLabelProvider(), new BarLogEntryLabelProvider() }); + mockServiceProvider.Setup(sp => sp.GetService(typeof(IEnumerable))) + .Returns(new Common.ILogEntryLabelProvider[] { new FooLogEntryLabelProvider(), new BarLogEntryLabelProvider() }); var mockConsumer = new Mock>(); mockConsumer.Setup(c => c.Receive(Match.Create(matcher))); @@ -519,8 +522,8 @@ public void Log_Exception() public void Log_DoesNotLogIfNullLabels() { var mockServiceProvider = new Mock(); - mockServiceProvider.Setup(sp => sp.GetService(typeof(IEnumerable))) - .Returns(new ILogEntryLabelProvider[] { new EmptyLogEntryLabelProvider() }); + mockServiceProvider.Setup(sp => sp.GetService(typeof(IEnumerable))) + .Returns(new Common.ILogEntryLabelProvider[] { new EmptyLogEntryLabelProvider() }); var mockConsumer = new Mock>(); var logger = GetLogger(mockConsumer.Object, LogLevel.Information, serviceProvider: mockServiceProvider.Object, logName: BaseLogName); @@ -532,8 +535,8 @@ public void Log_DoesNotLogIfNullLabels() public void Log_ThrowsIfNullLabels_RetryOptionsPropagateExceptions() { var mockServiceProvider = new Mock(); - mockServiceProvider.Setup(sp => sp.GetService(typeof(IEnumerable))) - .Returns(new ILogEntryLabelProvider[] { new EmptyLogEntryLabelProvider() }); + mockServiceProvider.Setup(sp => sp.GetService(typeof(IEnumerable))) + .Returns(new Common.ILogEntryLabelProvider[] { new EmptyLogEntryLabelProvider() }); var mockConsumer = new Mock>(); var logger = GetLogger(mockConsumer.Object, LogLevel.Information, serviceProvider: mockServiceProvider.Object, logName: BaseLogName, retryOptions: RetryOptions.NoRetry(ExceptionHandling.Propagate)); @@ -564,8 +567,8 @@ public void Log_Labels_DefaultLabelsFirst() }; var mockServiceProvider = new Mock(); - mockServiceProvider.Setup(sp => sp.GetService(typeof(IEnumerable))) - .Returns(new ILogEntryLabelProvider[] { new FooLogEntryLabelProvider(), new BarLogEntryLabelProvider() }); + mockServiceProvider.Setup(sp => sp.GetService(typeof(IEnumerable))) + .Returns(new Common.ILogEntryLabelProvider[] { new FooLogEntryLabelProvider(), new BarLogEntryLabelProvider() }); var mockConsumer = new Mock>(); mockConsumer.Setup(c => c.Receive(Match.Create(matcher))); @@ -666,7 +669,7 @@ public void Log_NoServiceContext() } } - internal class FooLogEntryLabelProvider : ILogEntryLabelProvider + internal class FooLogEntryLabelProvider : Common.ILogEntryLabelProvider { public void Invoke(Dictionary labels) { @@ -674,7 +677,7 @@ public void Invoke(Dictionary labels) } } - internal class BarLogEntryLabelProvider : ILogEntryLabelProvider + internal class BarLogEntryLabelProvider : Common.ILogEntryLabelProvider { public void Invoke(Dictionary labels) { @@ -682,7 +685,7 @@ public void Invoke(Dictionary labels) } } - internal class EmptyLogEntryLabelProvider : ILogEntryLabelProvider + internal class EmptyLogEntryLabelProvider : Common.ILogEntryLabelProvider { public void Invoke(Dictionary labels) { diff --git a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/GoogleTraceProviderTests.cs b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/GoogleTraceProviderTests.cs index c877f2100673..b5902ae861ac 100644 --- a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/GoogleTraceProviderTests.cs +++ b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/GoogleTraceProviderTests.cs @@ -27,6 +27,7 @@ namespace Google.Cloud.Diagnostics.AspNetCore.Tests #error unknown target framework #endif { +#pragma warning disable CS0618 // Type or member is obsolete public class GoogleTraceProviderTests { [Fact] @@ -41,6 +42,7 @@ public void GetCurrentTraceContext() IServiceProvider serviceProvider = MockServiceProvider(traceId, spanId, true); + GoogleTraceProvider traceProvider = new GoogleTraceProvider(); TraceContextForLogEntry traceContext = traceProvider.GetCurrentTraceContext(serviceProvider); @@ -83,4 +85,5 @@ private IServiceProvider MockServiceProvider(string traceId, ulong spanId, bool return serviceProviderMock.Object; } } +#pragma warning restore CS0618 // Type or member is obsolete } diff --git a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/LabelProviders/LabelProviderExtensionsTest.cs b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/LabelProviders/LabelProviderExtensionsTest.cs index 8f853576f523..924edea4190c 100644 --- a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/LabelProviders/LabelProviderExtensionsTest.cs +++ b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/LabelProviders/LabelProviderExtensionsTest.cs @@ -40,7 +40,7 @@ public void AddsServiceFromType() Assert.Single(services); var descriptor = services.Single(); - Assert.Equal(typeof(ILogEntryLabelProvider), descriptor.ServiceType); + Assert.Equal(typeof(Common.ILogEntryLabelProvider), descriptor.ServiceType); Assert.Equal(typeof(TraceIdLogEntryLabelProvider), descriptor.ImplementationType); Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime); } @@ -57,7 +57,7 @@ public void AddsServiceFromFactory() Assert.Single(services); var descriptor = services.Single(); - Assert.Equal(typeof(ILogEntryLabelProvider), descriptor.ServiceType); + Assert.Equal(typeof(Common.ILogEntryLabelProvider), descriptor.ServiceType); Assert.NotNull(descriptor.ImplementationFactory); Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime); } @@ -74,7 +74,7 @@ public void AddsServiceFromInstance() Assert.Single(services); var descriptor = services.Single(); - Assert.Equal(typeof(ILogEntryLabelProvider), descriptor.ServiceType); + Assert.Equal(typeof(Common.ILogEntryLabelProvider), descriptor.ServiceType); Assert.IsType(descriptor.ImplementationInstance); Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime); } diff --git a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/TraceContextForLogEntryTests.cs b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/TraceContextForLogEntryTests.cs index 07f09baf418d..d2cbcc8f6da3 100644 --- a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/TraceContextForLogEntryTests.cs +++ b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/TraceContextForLogEntryTests.cs @@ -24,6 +24,7 @@ namespace Google.Cloud.Diagnostics.AspNetCore.Tests #error unknown target framework #endif { +#pragma warning disable CS0618 // Type or member is obsolete public class TraceContextForLogEntryTests { [Fact] @@ -98,4 +99,5 @@ private IManagedTracer MockTracer(string traceId = null, ulong? spanId = null) return tracerMock.Object; } } +#pragma warning restore CS0618 // Type or member is obsolete } diff --git a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/GoogleLogger.cs b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/GoogleLogger.cs index a98ce10e8ea7..7ab936c81a90 100644 --- a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/GoogleLogger.cs +++ b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/GoogleLogger.cs @@ -13,15 +13,8 @@ // limitations under the License. using Google.Api.Gax; -using Google.Cloud.Diagnostics.Common; -using Google.Cloud.Logging.V2; -using Google.Protobuf.WellKnownTypes; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; -using System.IO; -using static System.FormattableString; #if NETCOREAPP3_1 namespace Google.Cloud.Diagnostics.AspNetCore3 @@ -36,252 +29,24 @@ namespace Google.Cloud.Diagnostics.AspNetCore /// public sealed class GoogleLogger : ILogger { - private const string GcpConsoleLogsBaseUrl = "https://console.cloud.google.com/logs/viewer"; + private readonly Common.GoogleLogger _logger; - /// The log name given when creating the logger. - private readonly string _logName; - - /// The consumer to push logs to. - private readonly IConsumer _consumer; - - /// The trace target or null if non exists. - private readonly TraceTarget _traceTarget; - - /// - /// The log target, indicating mainly if the target is a project or an organization. - /// - private readonly LogTarget _logTarget; - - /// The logger options. - private readonly LoggerOptions _loggerOptions; - - /// The formatted log name. - private readonly string _fullLogName; - - /// A clock for getting the current timestamp. - private readonly IClock _clock; - - /// The service provider to resolve additional services from. - private readonly IServiceProvider _serviceProvider; - - private readonly AmbientScopeManager _ambientScopeManager; - - internal GoogleLogger(IConsumer consumer, LogTarget logTarget, LoggerOptions loggerOptions, - string logName, IClock clock = null, IServiceProvider serviceProvider = null) - { - _logTarget = GaxPreconditions.CheckNotNull(logTarget, nameof(logTarget)); - _traceTarget = logTarget.Kind == LogTargetKind.Project ? - TraceTarget.ForProject(logTarget.ProjectId) : null; - _consumer = GaxPreconditions.CheckNotNull(consumer, nameof(consumer)); - _loggerOptions = GaxPreconditions.CheckNotNull(loggerOptions, nameof(loggerOptions)); - _logName = GaxPreconditions.CheckNotNullOrEmpty(logName, nameof(logName)); - _fullLogName = logTarget.GetFullLogName(_loggerOptions.LogName); - _serviceProvider = serviceProvider; - _clock = clock ?? SystemClock.Instance; - _ambientScopeManager = new AmbientScopeManager(_loggerOptions, _serviceProvider); - } + internal GoogleLogger(Common.GoogleLogger logger) => _logger = GaxPreconditions.CheckNotNull(logger, nameof(logger)); /// - public IDisposable BeginScope(TState state) => GoogleLoggerScope.BeginScope(state); + public IDisposable BeginScope(TState state) => _logger.BeginScope(state); /// - public bool IsEnabled(LogLevel logLevel) => logLevel >= _loggerOptions.LogLevel; + public bool IsEnabled(LogLevel logLevel) => _logger.IsEnabled(logLevel); /// - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) - { - try - { - GaxPreconditions.CheckNotNull(formatter, nameof(formatter)); - - if (!IsEnabled(logLevel)) - { - return; - } - - string message = formatter(state, exception); - if (string.IsNullOrEmpty(message)) - { - return; - } - - LogEntry entry = new LogEntry - { - Resource = _loggerOptions.MonitoredResource, - LogName = _fullLogName, - Severity = logLevel.ToLogSeverity(), - Timestamp = Timestamp.FromDateTime(_clock.GetCurrentDateTimeUtc()), - JsonPayload = CreateJsonPayload(eventId, state, exception, message), - }; - - _ambientScopeManager.GetCurrentScope()?.ApplyTo(entry); - GoogleLoggerScope.Current?.ApplyTo(entry); - SetTraceAndSpanIfAny(entry); - - _consumer.Receive(new[] { entry }); - } - catch (Exception) when (_loggerOptions.RetryOptions.ExceptionHandling == ExceptionHandling.Ignore) { } - } - - private Struct CreateJsonPayload(EventId eventId, TState state, Exception exception, string message) - { - var jsonStruct = new Struct(); - jsonStruct.Fields.Add("message", Value.ForString(message)); - jsonStruct.Fields.Add("log_name", Value.ForString(_logName)); - - if (_loggerOptions.ServiceContext != null) - { - jsonStruct.Fields.Add("serviceContext", Value.ForStruct(_loggerOptions.ServiceContext)); - } - if (exception != null) - { - jsonStruct.Fields.Add("exception", Value.ForString(exception.ToString())); - } - - if (eventId.Id != 0 || eventId.Name != null) - { - var eventStruct = new Struct(); - if (eventId.Id != 0) - { - eventStruct.Fields.Add("id", Value.ForNumber(eventId.Id)); - } - if (!string.IsNullOrWhiteSpace(eventId.Name)) - { - eventStruct.Fields.Add("name", Value.ForString(eventId.Name)); - } - jsonStruct.Fields.Add("event_id", Value.ForStruct(eventStruct)); - } - - // If we have format params and its more than just the original message add them. - if (state is IEnumerable> formatParams && - ContainsFormatParameters(formatParams)) - { - jsonStruct.Fields.Add("format_parameters", formatParams.ToStructValue()); - } - - return jsonStruct; - - // Checks that fields is: - // - Non-empty - // - Not just a single entry with a key of "{OriginalFormat}" - // so we can decide whether or not to populate a struct with it. - bool ContainsFormatParameters(IEnumerable> fields) - { - using (var iterator = fields.GetEnumerator()) - { - // No fields? Nothing to format. - if (!iterator.MoveNext()) - { - return false; - } - // If the first entry isn't the original format, we definitely want to create a struct - if (iterator.Current.Key != "{OriginalFormat}") - { - return true; - } - // If the first entry *is* the original format, we want to create a struct - // if and only if there's at least one more entry. - return iterator.MoveNext(); - } - } - } - - private void SetTraceAndSpanIfAny(LogEntry entry) - { - if (_traceTarget is null) - { - return; - } - - // If there's currently a Google trace and span use that one. - // This means that the Google Trace component of the diagnostics library - // has been initialized. - // Else attempt to use an external trace context. - if ((TraceContextForLogEntry.FromGoogleTrace() ?? TraceContextForLogEntry.FromExternalTrace(_serviceProvider)) is TraceContextForLogEntry trace) - { - entry.Trace = _traceTarget.GetFullTraceName(trace.TraceId); - entry.TraceSampled = true; - entry.SpanId = trace.SpanId; - } - } + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) => + _logger.Log(logLevel, eventId, state, exception, formatter); /// /// For diagnostic purposes. Builds and returns the URL where the entries logged by /// this can be seen on the Google Cloud Logging Console. /// - public Uri GetGcpConsoleLogsUrl() - { - string target = - _logTarget.Kind == LogTargetKind.Project ? $"project={_logTarget.ProjectId}" : - _logTarget.Kind == LogTargetKind.Organization ? $"organizationId={_logTarget.OrganizationId}" : - throw new InvalidOperationException($"Unrecognized LogTargetKind: {_logTarget.Kind}"); - - string resourceType = _loggerOptions.MonitoredResource.Type; - // Log ingestion converts "gke_container" into "container", but we really do need to search for "container", - // as the UI doesn't support "gke_container". (Whereas the Monitoring API *only* supports "gke_container".) - if (resourceType == "gke_container") - { - resourceType = "container"; - } - IList parameters = new List - { - $"resource={resourceType}", - $"minLogLevel={(int)_loggerOptions.LogLevel.ToLogSeverity()}", - $"logName={_fullLogName}", - target - }; - - return new UriBuilder(GcpConsoleLogsBaseUrl) - { - Query = string.Join("&", parameters) - }.Uri; - } - - internal void WriteDiagnostics(TextWriter writer) - { - // Explicitly not catching exceptions. - // This should only be activated for diagnostics purposes so in that case - // we shouldn't try to handle exceptions. - - writer.WriteLine(Invariant($"{DateTime.UtcNow:yyyy-MM-dd'T'HH:mm:ss} - GoogleLogger will write logs to: {GetGcpConsoleLogsUrl()}")); - writer.Flush(); - } - - /// - /// Obtains the current ambient scope, which is not set by user code - /// but instead it is calculated based on the Logger configuration. - /// The ambient scope will be applied to all log entries. - /// It will be applied before the user specified scopes so that the - /// user code is able to override ambient scope values on a per - /// log entry basis. - /// - internal class AmbientScopeManager - { - private readonly GoogleLoggerScope _permanentParent; - private readonly IServiceProvider _serviceProvider; - - internal AmbientScopeManager(LoggerOptions options, IServiceProvider serviceProvider) - { - _permanentParent = options?.Labels is null ? null : GoogleLoggerScope.CreateScope(new LabellingScopeState(options.Labels), null); - _serviceProvider = serviceProvider; - } - - public GoogleLoggerScope GetCurrentScope() - { - var current = _permanentParent; - - if (_serviceProvider?.GetService>() is IEnumerable providers) - { - var labels = new Dictionary(); - foreach (var provider in providers) - { - provider.Invoke(labels); - } - current = GoogleLoggerScope.CreateScope(new LabellingScopeState(labels), current); - } - - return current; - } - } + public Uri GetGcpConsoleLogsUrl() => _logger.GetGcpConsoleLogsUrl(); } } diff --git a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/GoogleLoggerProvider.cs b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/GoogleLoggerProvider.cs index a1c517eda5ef..7a9ff4a46d9d 100644 --- a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/GoogleLoggerProvider.cs +++ b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/GoogleLoggerProvider.cs @@ -12,11 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; using Google.Api.Gax; using Google.Cloud.Diagnostics.Common; using Google.Cloud.Logging.V2; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; #if NETCOREAPP3_1 namespace Google.Cloud.Diagnostics.AspNetCore3 @@ -31,41 +33,13 @@ namespace Google.Cloud.Diagnostics.AspNetCore /// public sealed class GoogleLoggerProvider : ILoggerProvider { - /// The consumer to push logs to. - private readonly IConsumer _consumer; - - /// The logger options. - private readonly LoggerOptions _loggerOptions; - - /// Where to log to. - private readonly LogTarget _logTarget; - - /// The service provider to resolve additional services from. - private readonly IServiceProvider _serviceProvider; + private readonly Common.GoogleLoggerProvider _loggerProvider; /// /// for Google Cloud Logging. /// - /// The consumer to push logs to. Must not be null. - /// Where to log to. Must not be null. - /// The logger options. Must not be null. - /// The service provider to resolve additional services from. - internal GoogleLoggerProvider(IConsumer consumer, LogTarget logTarget, LoggerOptions loggerOptions, IServiceProvider serviceProvider) - { - _consumer = GaxPreconditions.CheckNotNull(consumer, nameof(consumer)); - _logTarget = GaxPreconditions.CheckNotNull(logTarget, nameof(logTarget)); - _loggerOptions = GaxPreconditions.CheckNotNull(loggerOptions, nameof(loggerOptions)); - _serviceProvider = serviceProvider; - - var writer = loggerOptions.LoggerDiagnosticsOutput; - if (writer != null) - { - // The log name is the ASP.NET Core log name, not the "/projects/xyz/logs/abc" log name in the resource. - // We don't currently use this in the diagnostics, but if we ever start to do so, SampleLogName seems - // like a reasonably clear example. - ((GoogleLogger) CreateLogger("SampleLogName")).WriteDiagnostics(writer); - } - } + internal GoogleLoggerProvider(Common.GoogleLoggerProvider loggerProvider) => + _loggerProvider = GaxPreconditions.CheckNotNull(loggerProvider, nameof(loggerProvider)); /// /// Create an for Google Cloud Logging. @@ -73,17 +47,13 @@ internal GoogleLoggerProvider(IConsumer consumer, LogTarget logTarget, /// The service provider to resolve additional services from. /// May be null, in which case additional services (such as custom labels) will not be used. /// Optional if running on Google App Engine or Google Compute Engine. - /// The Google Cloud Platform project ID. If unspecified and running on GAE or GCE the project ID will be - /// detected from the platform. + /// The Google Cloud Platform project ID. If unspecified and running on GAE or GCE the project ID will be + /// detected from the platform. /// Optional, options for the logger. /// Optional, logging client. - public static GoogleLoggerProvider Create(IServiceProvider serviceProvider, string projectId = null, - LoggerOptions options = null, LoggingServiceV2Client client = null) - { - options = options ?? LoggerOptions.Create(); - projectId = Project.GetAndCheckProjectId(projectId, options.MonitoredResource); - return Create(LogTarget.ForProject(projectId), serviceProvider, options, client); - } + public static GoogleLoggerProvider Create( + IServiceProvider serviceProvider, string projectId = null, LoggerOptions options = null, LoggingServiceV2Client client = null) => + new GoogleLoggerProvider(Common.GoogleLoggerProvider.Create(serviceProvider, projectId, options?.CommonLoggerOptions, client)); /// /// Create an for Google Cloud Logging. @@ -93,28 +63,53 @@ public static GoogleLoggerProvider Create(IServiceProvider serviceProvider, stri /// in which case additional services (such as custom labels) will not be used. /// Optional, options for the logger. /// Optional, logging client. - public static GoogleLoggerProvider Create(LogTarget logTarget, IServiceProvider serviceProvider, - LoggerOptions options = null, LoggingServiceV2Client client = null) - { - // Check params and set defaults if unset. - GaxPreconditions.CheckNotNull(logTarget, nameof(logTarget)); - client = client ?? LoggingServiceV2Client.Create(); - options = options ?? LoggerOptions.Create(); + public static GoogleLoggerProvider Create( + LogTarget logTarget, IServiceProvider serviceProvider, LoggerOptions options = null, LoggingServiceV2Client client = null) => + new GoogleLoggerProvider(Common.GoogleLoggerProvider.Create(logTarget, serviceProvider, options?.CommonLoggerOptions, client)); - // Get the proper consumer from the options and add a logger provider. - IConsumer consumer = LogConsumer.Create(client, options.BufferOptions, options.RetryOptions); - return new GoogleLoggerProvider(consumer, logTarget, options, serviceProvider); - } + // Adapter function to be used to get labels set with the obsolete AspNetCore*.ILogEntryLabelProvider. + // Will be used to pass these labels from AspNetCore*.GoogleLogger to Common.GoogleLogger because + // Common.GoogleLogger cannot have a dependecy on the obsolote interface. + [Obsolete("Has been added temporarily until we remove AspNetCore.ILogEntryLabelProvider")] + internal static Action> ObsoleteLabelsGetter => + (serviceProvider, labels) => + { + if (serviceProvider?.GetService>() is IEnumerable providers) + { + foreach (var provider in providers) + { + provider.Invoke(labels); + } + } + }; + + // Adapter function to be used to get trace context from the obsolete AspNetCore*.IExternalTraceProvider. + // Will be used to "pass" this context from AspNetCore*.GoogleLogger to Common.GoogleLogger because + // Common.GoogleLogger cannot have a dependecy on the obsolote interface. + [Obsolete("Has been added temporarily until we remove AspNetCore.ILogEntryLabelProvider")] + internal static Action ObsoleteTraceContextGetter => + (serviceProvider, logEntry, traceTarget) => + { + if (TraceContextForLogEntry.FromExternalTrace(serviceProvider) is TraceContextForLogEntry trace + && !(trace.TraceId is null)) + { + logEntry.Trace = traceTarget.GetFullTraceName(trace.TraceId); + logEntry.TraceSampled = true; + logEntry.SpanId = trace.SpanId; + } + }; /// /// Creates a with the given log name. /// /// The name of the log. This will be combined with the log location /// () to generate the resource name for the log. - public ILogger CreateLogger(string logName) => new GoogleLogger( - _consumer, _logTarget, _loggerOptions, logName, serviceProvider: _serviceProvider); + public ILogger CreateLogger(string logName) => +#pragma warning disable CS0618 // Type or member is obsolete + new GoogleLogger(_loggerProvider.CreateLogger(logName, ObsoleteLabelsGetter, ObsoleteTraceContextGetter)); +#pragma warning restore CS0618 // Type or member is obsolete /// - public void Dispose() => _consumer.Dispose(); + public void Dispose() => _loggerProvider.Dispose(); } } diff --git a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/GoogleTraceProvider.cs b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/GoogleTraceProvider.cs index 3ad9c42aae59..d2b536d56d0a 100644 --- a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/GoogleTraceProvider.cs +++ b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/GoogleTraceProvider.cs @@ -33,6 +33,7 @@ namespace Google.Cloud.Diagnostics.AspNetCore /// If the Tracing component is configured, log entries are automatically associated /// to Google traces and spans. /// + [Obsolete("Use CloudTraceExtensions.TryAddGoogleTraceContextProvider instead.")] public class GoogleTraceProvider : IExternalTraceProvider { /// diff --git a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/IExternalTraceProvider.cs b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/IExternalTraceProvider.cs index 6fde6c88115a..223d720f6b64 100644 --- a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/IExternalTraceProvider.cs +++ b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/IExternalTraceProvider.cs @@ -28,6 +28,7 @@ namespace Google.Cloud.Diagnostics.AspNetCore /// Implement this interface when traces are being generated by your environment /// and sent to Google Cloud Tracing not via the Google.Cloud.Diagnostics library. /// + [Obsolete("Inject an Common.ITraceContextInstead.")] public interface IExternalTraceProvider { /// diff --git a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/ILogEntryLabelProvider.cs b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/ILogEntryLabelProvider.cs index 75f1eaadfb76..977548a451b1 100644 --- a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/ILogEntryLabelProvider.cs +++ b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/ILogEntryLabelProvider.cs @@ -12,8 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Google.Cloud.Diagnostics.Common; -using System.Collections.Generic; +using System; #if NETCOREAPP3_1 namespace Google.Cloud.Diagnostics.AspNetCore3 @@ -26,16 +25,7 @@ namespace Google.Cloud.Diagnostics.AspNetCore /// /// Provides a hook to augment labels when a log entry is being logged. /// - public interface ILogEntryLabelProvider - { - /// - /// Invokes the provider to augment log entry labels. - /// - /// A dictionary of log entry labels. - /// Keys and values added to should not be null. - /// If they are, an exception will be throw when attempting to log an entry. - /// The entry won't be logged and the exception will be propagated depending - /// on the value of . - void Invoke(Dictionary labels); - } + [Obsolete("Use Google.Cloud.Diagnostics.Common.ILogEntryLabelProvider instead.")] + public interface ILogEntryLabelProvider : Common.ILogEntryLabelProvider + { } } diff --git a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/LabelProviders/EnvironmentNameLogEntryLabelProvider.cs b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/LabelProviders/EnvironmentNameLogEntryLabelProvider.cs index f85790aa34bc..7e3533a2d297 100644 --- a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/LabelProviders/EnvironmentNameLogEntryLabelProvider.cs +++ b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/LabelProviders/EnvironmentNameLogEntryLabelProvider.cs @@ -28,7 +28,9 @@ namespace Google.Cloud.Diagnostics.AspNetCore /// /// A implementation which adds the to the log entry labels. /// - public class EnvironmentNameLogEntryLabelProvider : ILogEntryLabelProvider +#pragma warning disable CS0618 // Type or member is obsolete + public class EnvironmentNameLogEntryLabelProvider : ILogEntryLabelProvider, Common.ILogEntryLabelProvider +#pragma warning restore CS0618 // Type or member is obsolete { #if NETCOREAPP3_1 private readonly IWebHostEnvironment _hostingEnvironment; diff --git a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/LabelProviders/HttpLogEntryLabelProvider.cs b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/LabelProviders/HttpLogEntryLabelProvider.cs index 1129f5187869..4505d6d56405 100644 --- a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/LabelProviders/HttpLogEntryLabelProvider.cs +++ b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/LabelProviders/HttpLogEntryLabelProvider.cs @@ -28,7 +28,9 @@ namespace Google.Cloud.Diagnostics.AspNetCore /// /// Base class for implementations which needs an instance. /// - public abstract class HttpLogEntryLabelProvider : ILogEntryLabelProvider +#pragma warning disable CS0618 // Type or member is obsolete + public abstract class HttpLogEntryLabelProvider : ILogEntryLabelProvider, Common.ILogEntryLabelProvider +#pragma warning restore CS0618 // Type or member is obsolete { private readonly IHttpContextAccessor _httpContextAccessor; diff --git a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/LabelProviders/LabelProviderExtensions.cs b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/LabelProviders/LabelProviderExtensions.cs index cf247b02b93e..9db3fe7322f5 100644 --- a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/LabelProviders/LabelProviderExtensions.cs +++ b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/LabelProviders/LabelProviderExtensions.cs @@ -24,40 +24,40 @@ namespace Google.Cloud.Diagnostics.AspNetCore #endif { /// - /// Provides extension methods to register implementations. + /// Provides extension methods to register implementations. /// public static class LabelProviderExtensions { /// - /// Adds a of type to the service collection instance. + /// Adds a of type to the service collection instance. /// - /// The type of the implementation. + /// The type of the implementation. /// The instance. /// The instance. public static IServiceCollection AddLogEntryLabelProvider(this IServiceCollection serivces) - where T : class, ILogEntryLabelProvider - => serivces.AddSingleton(); + where T : class, Common.ILogEntryLabelProvider + => serivces.AddSingleton(); /// - /// Adds a of type to the service collection instance. + /// Adds a of type to the service collection instance. /// - /// The type of the implementation. + /// The type of the implementation. /// The instance. /// The factory that creates the service. /// The instance. public static IServiceCollection AddLogEntryLabelProvider(this IServiceCollection serivces, Func implementationFactory) - where T : class, ILogEntryLabelProvider - => serivces.AddSingleton(implementationFactory); + where T : class, Common.ILogEntryLabelProvider + => serivces.AddSingleton(implementationFactory); /// - /// Adds a of type to the service collection instance. + /// Adds a of type to the service collection instance. /// - /// The type of the implementation. + /// The type of the implementation. /// The instance. /// The instance of . /// The instance. public static IServiceCollection AddLogEntryLabelProvider(this IServiceCollection serivces, T instance) - where T : class, ILogEntryLabelProvider - => serivces.AddSingleton(instance); + where T : class, Common.ILogEntryLabelProvider + => serivces.AddSingleton(instance); } } diff --git a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/LoggerOptions.cs b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/LoggerOptions.cs index fd81dcf45736..e83d339c0d96 100644 --- a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/LoggerOptions.cs +++ b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/LoggerOptions.cs @@ -14,13 +14,10 @@ using Google.Api; using Google.Api.Gax; -using Google.Api.Gax.Grpc; using Google.Cloud.Diagnostics.Common; -using Google.Protobuf.WellKnownTypes; using Microsoft.Extensions.Logging; using System.Collections.Generic; using System.IO; -using static Google.Cloud.Diagnostics.Common.ServiceContextUtils; #if NETCOREAPP3_1 namespace Google.Cloud.Diagnostics.AspNetCore3 @@ -35,30 +32,29 @@ namespace Google.Cloud.Diagnostics.AspNetCore /// public sealed class LoggerOptions { - /// The base log name for all logs. - private const string _baseLogName = "aspnetcore"; + internal Common.LoggerOptions CommonLoggerOptions { get; } /// The minimum log level. - public LogLevel LogLevel { get; } + public LogLevel LogLevel => CommonLoggerOptions.LogLevel; /// The name for the all logs. - public string LogName { get; } + public string LogName => CommonLoggerOptions.LogName; /// The monitored resource. See: https://cloud.google.com/logging/docs/api/v2/resource-list - public MonitoredResource MonitoredResource { get; } + public MonitoredResource MonitoredResource => CommonLoggerOptions.MonitoredResource; /// The buffer options for the logger. - public BufferOptions BufferOptions { get; } + public BufferOptions BufferOptions => CommonLoggerOptions.BufferOptions; /// The retry options for the logger. - public RetryOptions RetryOptions { get; } + public RetryOptions RetryOptions => CommonLoggerOptions.RetryOptions; /// Custom labels for log entries. /// Keys and values added to should not be null. /// If they are, an exception will be throw when attempting to log an entry. /// The entry won't be logged and the exception will be propagated depending /// on the value of . - public Dictionary Labels { get; } + public Dictionary Labels => CommonLoggerOptions.Labels; /// /// A to write diagnostics info about loggers created @@ -66,46 +62,22 @@ public sealed class LoggerOptions /// Currently the only diagnostics info we provide is the URL where the logs written /// with these options can be found. /// - public TextWriter LoggerDiagnosticsOutput { get; } + public TextWriter LoggerDiagnosticsOutput => CommonLoggerOptions.LoggerDiagnosticsOutput; /// /// An identifier of the service, such as the name of the executable or job. May be null. /// When set, it will be included in the serviceContext field of the log entry JSON payload. /// - public string ServiceName => - ServiceContext == null || !ServiceContext.Fields.TryGetValue(ServiceContextServiceKey, out Value v) ? null : v.StringValue; + public string ServiceName => CommonLoggerOptions.ServiceName; /// /// A string that represents the version of the service or the source code that logs are written for. /// When set, it will be included in the serviceContext field of the log entry JSON payload. /// - public string Version => - ServiceContext == null || !ServiceContext.Fields.TryGetValue(ServiceContextVersionKey, out Value v) ? null : v.StringValue; + public string Version => CommonLoggerOptions.Version; - internal Struct ServiceContext { get; } - - private LoggerOptions( - LogLevel logLevel, - string logName, - Dictionary labels, - MonitoredResource monitoredResource, - BufferOptions bufferOptions, - RetryOptions retryOptions, - TextWriter loggerDiagnosticsOutput, - string serviceName, - string version) - { - LogName = logName; - LogLevel = GaxPreconditions.CheckEnumValue(logLevel, nameof(logLevel)); - Labels = labels; - MonitoredResource = monitoredResource; - BufferOptions = bufferOptions; - RetryOptions = retryOptions; - LoggerDiagnosticsOutput = loggerDiagnosticsOutput; - - // Create the service context here, this class is inmutable. - ServiceContext = CreateServiceContext(serviceName, version); - } + private LoggerOptions(Common.LoggerOptions loggerOptions) => + CommonLoggerOptions = GaxPreconditions.CheckNotNull(loggerOptions, nameof(loggerOptions)); /// /// Create a new instance of . @@ -205,14 +177,7 @@ public static LoggerOptions CreateWithServiceContext( RetryOptions retryOptions = null, TextWriter loggerDiagnosticsOutput = null, string serviceName = null, - string version = null) - { - logName ??= _baseLogName; - labels ??= new Dictionary(); - monitoredResource ??= MonitoredResourceBuilder.FromPlatform(); - bufferOptions ??= BufferOptions.TimedBuffer(); - retryOptions ??= RetryOptions.NoRetry(); - return new LoggerOptions(logLevel, logName, labels, monitoredResource, bufferOptions, retryOptions, loggerDiagnosticsOutput, serviceName, version); - } + string version = null) => new LoggerOptions(Common.LoggerOptions.CreateWithServiceContext( + logLevel, logName, labels, monitoredResource, bufferOptions, retryOptions, loggerDiagnosticsOutput, serviceName, version)); } } diff --git a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/TraceContextForLogEntry.cs b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/TraceContextForLogEntry.cs index f4827ceca78d..15a22892a1ff 100644 --- a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/TraceContextForLogEntry.cs +++ b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/TraceContextForLogEntry.cs @@ -33,6 +33,7 @@ namespace Google.Cloud.Diagnostics.AspNetCore /// These values can be attached to a log entry to establish the /// relation of it and a trace. /// + [Obsolete("Use Google.Cloud.Diagnostics.Common.ITraceContext instead.")] public sealed class TraceContextForLogEntry { /// diff --git a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Trace/CloudTraceExtension.cs b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Trace/CloudTraceExtension.cs index 2af177d826b0..bb97cf97ebf0 100644 --- a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Trace/CloudTraceExtension.cs +++ b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Trace/CloudTraceExtension.cs @@ -121,7 +121,7 @@ public static IServiceCollection AddGoogleTrace( // We use TryAdd... here to allow user code to inject their own trace context provider // and matching trace context response propagator. We use Google trace header otherwise. - services.TryAddScoped(ProvideGoogleTraceHeaderContext); + services.TryAddGoogleTraceContextProvider(); services.TryAddSingleton>(PropagateGoogleTraceHeaders); // Obsolete: Adding this for backwards compatibility in case someone is using the old factory type. @@ -145,6 +145,23 @@ public static IServiceCollection AddGoogleTrace( return services.AddSingleton(traceFallbackPredicate); } + /// + /// Adds the services needed for obtaining the trace context from Google's own trace header, + /// but only if no other trace context provider is registered. + /// If you are using + /// you don't need to call this method. Only use this method if you want to extract the trace context + /// information from Google's own header for your own code to use, or if you are not using the tracing + /// component of this library but are using the logging component and want the trace context information + /// to be associated with the log entries. + /// + public static IServiceCollection TryAddGoogleTraceContextProvider(this IServiceCollection services) + { + // We use TryAdd... here to allow user code to inject their own trace context provider + // and matching trace context response propagator. We use Google trace header otherwise. + services.TryAddScoped(ProvideGoogleTraceHeaderContext); + return services; + } + /// /// Creates an based on the current /// and a . diff --git a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/LogUtilsTest.cs b/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common.Tests/Logging/LogLevelExtensionsTest.cs similarity index 86% rename from apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/LogUtilsTest.cs rename to apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common.Tests/Logging/LogLevelExtensionsTest.cs index d06c91cd21ac..2957e0a495d7 100644 --- a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/LogUtilsTest.cs +++ b/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common.Tests/Logging/LogLevelExtensionsTest.cs @@ -16,15 +16,9 @@ using Microsoft.Extensions.Logging; using Xunit; -#if NETCOREAPP3_1 -namespace Google.Cloud.Diagnostics.AspNetCore3.Tests -#elif NETCOREAPP2_1 || NET461 -namespace Google.Cloud.Diagnostics.AspNetCore.Tests -#else -#error unknown target framework -#endif +namespace Google.Cloud.Diagnostics.Common.Tests { - public class LogUtilsTest + public class LogLevelExtensionsTest { [Fact] public void ToLogSeverity() diff --git a/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/KeyValuePairEnumerableExtensions.cs b/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/KeyValuePairEnumerableExtensions.cs index c910b0893676..6eb7405362b9 100644 --- a/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/KeyValuePairEnumerableExtensions.cs +++ b/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/KeyValuePairEnumerableExtensions.cs @@ -23,7 +23,7 @@ namespace Google.Cloud.Diagnostics.Common /// Extension methods for converting KeyValuePair Enumerables to several types, /// including Protobuf well known types. /// - public static class KeyValuePairEnumerableExtensions + internal static class KeyValuePairEnumerableExtensions { /// /// Returns a for a that will contain @@ -33,7 +33,7 @@ public static class KeyValuePairEnumerableExtensions /// /// The fields to convert to a Strcut. May be null or empty in /// which case this method will return null. - public static Value ToStructValue(this IEnumerable> fields) + internal static Value ToStructValue(this IEnumerable> fields) { if (fields is null) { diff --git a/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/GoogleLogger.cs b/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/GoogleLogger.cs new file mode 100644 index 000000000000..ef7ce63bec18 --- /dev/null +++ b/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/GoogleLogger.cs @@ -0,0 +1,310 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Google.Api.Gax; +using Google.Cloud.Logging.V2; +using Google.Protobuf.WellKnownTypes; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using static System.FormattableString; + +namespace Google.Cloud.Diagnostics.Common +{ + /// + /// for Google Cloud Logging. + /// + public sealed class GoogleLogger : ILogger + { + private const string GcpConsoleLogsBaseUrl = "https://console.cloud.google.com/logs/viewer"; + + /// The log name given when creating the logger. + private readonly string _logName; + + /// The consumer to push logs to. + private readonly IConsumer _consumer; + + /// The trace target or null if non exists. + private readonly TraceTarget _traceTarget; + + /// + /// The log target, indicating mainly if the target is a project or an organization. + /// + private readonly LogTarget _logTarget; + + /// The logger options. + private readonly LoggerOptions _loggerOptions; + + /// The formatted log name. + private readonly string _fullLogName; + + /// A clock for getting the current timestamp. + private readonly IClock _clock; + + /// The service provider to resolve additional services from. + private readonly IServiceProvider _serviceProvider; + + private readonly Action _obsoleteTraceContextGetter; + + private readonly AmbientScopeManager _ambientScopeManager; + + internal GoogleLogger( + IConsumer consumer, LogTarget logTarget, LoggerOptions loggerOptions, + string logName, IClock clock = null, IServiceProvider serviceProvider = null) +#pragma warning disable CS0618 // Type or member is obsolete + : this (consumer, logTarget, loggerOptions, logName, null, null, clock, serviceProvider) +#pragma warning restore CS0618 // Type or member is obsolete + { + } + + [Obsolete("Added for backward compatibility only when moving GoogleLogger to Common.")] + internal GoogleLogger(IConsumer consumer, LogTarget logTarget, LoggerOptions loggerOptions, string logName, + Action> obsoleteLabelsGetter, + Action obsoleteTraceContextGetter, + IClock clock = null, IServiceProvider serviceProvider = null) + { + _logTarget = GaxPreconditions.CheckNotNull(logTarget, nameof(logTarget)); + _traceTarget = logTarget.Kind == LogTargetKind.Project ? + TraceTarget.ForProject(logTarget.ProjectId) : null; + _consumer = GaxPreconditions.CheckNotNull(consumer, nameof(consumer)); + _loggerOptions = GaxPreconditions.CheckNotNull(loggerOptions, nameof(loggerOptions)); + _logName = GaxPreconditions.CheckNotNullOrEmpty(logName, nameof(logName)); + _fullLogName = logTarget.GetFullLogName(_loggerOptions.LogName); + _serviceProvider = serviceProvider; + _clock = clock ?? SystemClock.Instance; + _obsoleteTraceContextGetter = obsoleteTraceContextGetter; + _ambientScopeManager = new AmbientScopeManager(_loggerOptions, _serviceProvider, obsoleteLabelsGetter); + } + + /// + public IDisposable BeginScope(TState state) => GoogleLoggerScope.BeginScope(state); + + /// + public bool IsEnabled(LogLevel logLevel) => logLevel >= _loggerOptions.LogLevel; + + /// + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + try + { + GaxPreconditions.CheckNotNull(formatter, nameof(formatter)); + + if (!IsEnabled(logLevel)) + { + return; + } + + string message = formatter(state, exception); + if (string.IsNullOrEmpty(message)) + { + return; + } + + LogEntry entry = new LogEntry + { + Resource = _loggerOptions.MonitoredResource, + LogName = _fullLogName, + Severity = logLevel.ToLogSeverity(), + Timestamp = Timestamp.FromDateTime(_clock.GetCurrentDateTimeUtc()), + JsonPayload = CreateJsonPayload(eventId, state, exception, message), + }; + + _ambientScopeManager.GetCurrentScope()?.ApplyTo(entry); + GoogleLoggerScope.Current?.ApplyTo(entry); + SetTraceAndSpanIfAny(entry); + + _consumer.Receive(new[] { entry }); + } + catch (Exception) when (_loggerOptions.RetryOptions.ExceptionHandling == ExceptionHandling.Ignore) { } + } + + private Struct CreateJsonPayload(EventId eventId, TState state, Exception exception, string message) + { + var jsonStruct = new Struct(); + jsonStruct.Fields.Add("message", Value.ForString(message)); + jsonStruct.Fields.Add("log_name", Value.ForString(_logName)); + + if (_loggerOptions.ServiceContext != null) + { + jsonStruct.Fields.Add("serviceContext", Value.ForStruct(_loggerOptions.ServiceContext)); + } + if (exception != null) + { + jsonStruct.Fields.Add("exception", Value.ForString(exception.ToString())); + } + + if (eventId.Id != 0 || eventId.Name != null) + { + var eventStruct = new Struct(); + if (eventId.Id != 0) + { + eventStruct.Fields.Add("id", Value.ForNumber(eventId.Id)); + } + if (!string.IsNullOrWhiteSpace(eventId.Name)) + { + eventStruct.Fields.Add("name", Value.ForString(eventId.Name)); + } + jsonStruct.Fields.Add("event_id", Value.ForStruct(eventStruct)); + } + + // If we have format params and its more than just the original message add them. + if (state is IEnumerable> formatParams && + ContainsFormatParameters(formatParams)) + { + jsonStruct.Fields.Add("format_parameters", formatParams.ToStructValue()); + } + + return jsonStruct; + + // Checks that fields is: + // - Non-empty + // - Not just a single entry with a key of "{OriginalFormat}" + // so we can decide whether or not to populate a struct with it. + bool ContainsFormatParameters(IEnumerable> fields) + { + using (var iterator = fields.GetEnumerator()) + { + // No fields? Nothing to format. + if (!iterator.MoveNext()) + { + return false; + } + // If the first entry isn't the original format, we definitely want to create a struct + if (iterator.Current.Key != "{OriginalFormat}") + { + return true; + } + // If the first entry *is* the original format, we want to create a struct + // if and only if there's at least one more entry. + return iterator.MoveNext(); + } + } + } + + private void SetTraceAndSpanIfAny(LogEntry entry) + { + if (_traceTarget is null) + { + return; + } + + // If there's currently a Google trace and span use that one. + // This means that the Google Trace component of the diagnostics library + // has been initialized. + // Else attempt to use an external trace context. + if ((ContextTracerManager.GetCurrentTraceContext() ?? _serviceProvider?.GetService()) is ITraceContext context + && context.TraceId is string) + { + + entry.Trace = _traceTarget.GetFullTraceName(context.TraceId); + entry.TraceSampled = context.ShouldTrace ?? false; + entry.SpanId = SpanIdToHex(context.SpanId); + } + else + { + _obsoleteTraceContextGetter?.Invoke(_serviceProvider, entry, _traceTarget); + } + + static string SpanIdToHex(ulong? spanId) => spanId is null ? null : $"{spanId:x16}"; + } + + /// + /// For diagnostic purposes. Builds and returns the URL where the entries logged by + /// this can be seen on the Google Cloud Logging Console. + /// + public Uri GetGcpConsoleLogsUrl() + { + string target = + _logTarget.Kind == LogTargetKind.Project ? $"project={_logTarget.ProjectId}" : + _logTarget.Kind == LogTargetKind.Organization ? $"organizationId={_logTarget.OrganizationId}" : + throw new InvalidOperationException($"Unrecognized LogTargetKind: {_logTarget.Kind}"); + + string resourceType = _loggerOptions.MonitoredResource.Type; + // Log ingestion converts "gke_container" into "container", but we really do need to search for "container", + // as the UI doesn't support "gke_container". (Whereas the Monitoring API *only* supports "gke_container".) + if (resourceType == "gke_container") + { + resourceType = "container"; + } + IList parameters = new List + { + $"resource={resourceType}", + $"minLogLevel={(int)_loggerOptions.LogLevel.ToLogSeverity()}", + $"logName={_fullLogName}", + target + }; + + return new UriBuilder(GcpConsoleLogsBaseUrl) + { + Query = string.Join("&", parameters) + }.Uri; + } + + internal void WriteDiagnostics(TextWriter writer) + { + // Explicitly not catching exceptions. + // This should only be activated for diagnostics purposes so in that case + // we shouldn't try to handle exceptions. + + writer.WriteLine(Invariant($"{DateTime.UtcNow:yyyy-MM-dd'T'HH:mm:ss} - GoogleLogger will write logs to: {GetGcpConsoleLogsUrl()}")); + writer.Flush(); + } + + /// + /// Obtains the current ambient scope, which is not set by user code + /// but instead it is calculated based on the Logger configuration. + /// The ambient scope will be applied to all log entries. + /// It will be applied before the user specified scopes so that the + /// user code is able to override ambient scope values on a per + /// log entry basis. + /// + internal class AmbientScopeManager + { + private readonly GoogleLoggerScope _permanentParent; + private readonly IServiceProvider _serviceProvider; + private readonly Action> _obsoleteLabelsGetter; + + internal AmbientScopeManager(LoggerOptions options, IServiceProvider serviceProvider, Action> obsoleteLabelsGetter) + { + _permanentParent = options?.Labels is null ? null : GoogleLoggerScope.CreateScope(new LabellingScopeState(options.Labels), null); + _serviceProvider = serviceProvider; + _obsoleteLabelsGetter = obsoleteLabelsGetter; + } + + public GoogleLoggerScope GetCurrentScope() + { + var current = _permanentParent; + var labels = new Dictionary(); + + if (_serviceProvider?.GetService>() is IEnumerable providers) + { + foreach (var provider in providers) + { + provider.Invoke(labels); + } + } + _obsoleteLabelsGetter?.Invoke(_serviceProvider, labels); + + if (labels.Count > 0) + { + current = GoogleLoggerScope.CreateScope(new LabellingScopeState(labels), current); + } + + return current; + } + } + } +} diff --git a/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/GoogleLoggerProvider.cs b/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/GoogleLoggerProvider.cs new file mode 100644 index 000000000000..e2238db59ba2 --- /dev/null +++ b/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/GoogleLoggerProvider.cs @@ -0,0 +1,122 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Google.Api.Gax; +using Google.Cloud.Logging.V2; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; + +namespace Google.Cloud.Diagnostics.Common +{ + /// + /// for Google Cloud Logging. + /// + public sealed class GoogleLoggerProvider : ILoggerProvider + { + /// The consumer to push logs to. + private readonly IConsumer _consumer; + + /// The logger options. + private readonly LoggerOptions _loggerOptions; + + /// Where to log to. + private readonly LogTarget _logTarget; + + /// The service provider to resolve additional services from. + private readonly IServiceProvider _serviceProvider; + + /// + /// for Google Cloud Logging. + /// + /// The consumer to push logs to. Must not be null. + /// Where to log to. Must not be null. + /// The logger options. Must not be null. + /// The service provider to resolve additional services from. + internal GoogleLoggerProvider(IConsumer consumer, LogTarget logTarget, LoggerOptions loggerOptions, IServiceProvider serviceProvider) + { + _consumer = GaxPreconditions.CheckNotNull(consumer, nameof(consumer)); + _logTarget = GaxPreconditions.CheckNotNull(logTarget, nameof(logTarget)); + _loggerOptions = GaxPreconditions.CheckNotNull(loggerOptions, nameof(loggerOptions)); + _serviceProvider = serviceProvider; + + var writer = loggerOptions.LoggerDiagnosticsOutput; + if (writer != null) + { + // The log name is the ASP.NET Core log name, not the "/projects/xyz/logs/abc" log name in the resource. + // We don't currently use this in the diagnostics, but if we ever start to do so, SampleLogName seems + // like a reasonably clear example. + ((GoogleLogger) CreateLogger("SampleLogName")).WriteDiagnostics(writer); + } + } + + /// + /// Create an for Google Cloud Logging. + /// + /// The service provider to resolve additional services from. + /// May be null, in which case additional services (such as custom labels) will not be used. + /// Optional if running on Google App Engine or Google Compute Engine. + /// The Google Cloud Platform project ID. If unspecified and running on GAE or GCE the project ID will be + /// detected from the platform. + /// Optional, options for the logger. + /// Optional, logging client. + public static GoogleLoggerProvider Create(IServiceProvider serviceProvider, string projectId = null, + LoggerOptions options = null, LoggingServiceV2Client client = null) + { + options = options ?? LoggerOptions.Create(); + projectId = Project.GetAndCheckProjectId(projectId, options.MonitoredResource); + return Create(LogTarget.ForProject(projectId), serviceProvider, options, client); + } + + /// + /// Create an for Google Cloud Logging. + /// + /// Where to log to. Must not be null. + /// Optional, the service provider to resolve additional services from. May be null, + /// in which case additional services (such as custom labels) will not be used. + /// Optional, options for the logger. + /// Optional, logging client. + public static GoogleLoggerProvider Create(LogTarget logTarget, IServiceProvider serviceProvider, + LoggerOptions options = null, LoggingServiceV2Client client = null) + { + // Check params and set defaults if unset. + GaxPreconditions.CheckNotNull(logTarget, nameof(logTarget)); + client = client ?? LoggingServiceV2Client.Create(); + options = options ?? LoggerOptions.Create(); + + // Get the proper consumer from the options and add a logger provider. + IConsumer consumer = LogConsumer.Create(client, options.BufferOptions, options.RetryOptions); + return new GoogleLoggerProvider(consumer, logTarget, options, serviceProvider); + } + + /// + /// Creates a with the given log name. + /// + [Obsolete("Added for backward compatibility only when moving GoogleLogger to Common.")] + public GoogleLogger CreateLogger( + string logName, Action> obsoleteLabelsGetter, Action obsoleteTraceContextGetter) => + new GoogleLogger(_consumer, _logTarget, _loggerOptions, logName, obsoleteLabelsGetter, obsoleteTraceContextGetter, serviceProvider: _serviceProvider); + + /// + /// Creates a with the given log name. + /// + /// The name of the log. This will be combined with the log location + /// () to generate the resource name for the log. + public ILogger CreateLogger(string logName) => new GoogleLogger(_consumer, _logTarget, _loggerOptions, logName, serviceProvider: _serviceProvider); + + /// + public void Dispose() => _consumer.Dispose(); + } +} diff --git a/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/GoogleLoggerScope.cs b/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/GoogleLoggerScope.cs index 920938d6a058..99d803e385b7 100644 --- a/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/GoogleLoggerScope.cs +++ b/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/GoogleLoggerScope.cs @@ -24,7 +24,7 @@ namespace Google.Cloud.Diagnostics.Common /// /// Represents a scope for a Google Logger. /// - public abstract class GoogleLoggerScope : IDisposable + internal abstract class GoogleLoggerScope : IDisposable { private static readonly AsyncLocal _current = new AsyncLocal(); diff --git a/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/ILogEntryLabelProvider.cs b/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/ILogEntryLabelProvider.cs new file mode 100644 index 000000000000..3b2be882b3a6 --- /dev/null +++ b/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/ILogEntryLabelProvider.cs @@ -0,0 +1,34 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Generic; + +namespace Google.Cloud.Diagnostics.Common +{ + /// + /// Provides a hook to augment labels when a log entry is being logged. + /// + public interface ILogEntryLabelProvider + { + /// + /// Invokes the provider to augment log entry labels. + /// + /// A dictionary of log entry labels. + /// Keys and values added to should not be null. + /// If they are, an exception will be throw when attempting to log an entry. + /// The entry won't be logged and the exception will be propagated depending + /// on the value of . + void Invoke(Dictionary labels); + } +} diff --git a/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/ILoggerExtensions.cs b/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/ILoggerExtensions.cs index 9aa3bb7d2aa8..a834563d762a 100644 --- a/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/ILoggerExtensions.cs +++ b/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/ILoggerExtensions.cs @@ -23,8 +23,8 @@ namespace Google.Cloud.Diagnostics.Common { /// /// Extensions for to allow augmenting the logged information with contextual data. - /// GoogleLogger(TODO: change this to a cref when I've moved it to this package) - /// will add the information to the that is sent to Google Cloud Logging. + /// " will add the information to the that is sent to + /// Google Cloud Logging. /// For the consumption of other loggers, the information will be included in newly created scopes /// at the moment of logging, in a format that all loggers should understand. /// @@ -34,8 +34,9 @@ public static class ILoggerExtensions /// /// Adds labels to the returned logger that may be included with every subsequent log. /// How this information is included will depend on the actual . - /// GoogleLogger(TODO: change this to a cref when I've moved it to this package) will include this - /// information in . + /// " will include this information in . + /// For the consumption of other loggers, the information will be included in newly created scopes + /// at the moment of logging, in a format that all loggers should understand. /// /// /// If had already been augmented with labels, the old labels will be replaced @@ -49,8 +50,9 @@ public static ILogger WithLabels(this ILogger logger, params KeyValuePair /// Adds labels to the returned logger that may be included with every subsequent log. /// How this information is included will depend on the actual . - /// GoogleLogger(TODO: change this to a cref when I've moved it to this package) will include this - /// information in . + /// " will include this information in . + /// For the consumption of other loggers, the information will be included in newly created scopes + /// at the moment of logging, in a format that all loggers should understand. /// /// /// If had already been augmented with labels, the old labels will be replaced @@ -68,8 +70,9 @@ public static ILogger WithLabels(this ILogger logger, IEnumerable /// Adds labels to the returned logger that may be included with every subsequent log. /// How this information is included will depend on the actual . - /// GoogleLogger(TODO: change this to a cref when I've moved it to this package) will include this - /// information in . + /// " will include this information in . + /// For the consumption of other loggers, the information will be included in newly created scopes + /// at the moment of logging, in a format that all loggers should understand. /// /// /// If had already been augmented with labels, @@ -83,8 +86,9 @@ public static ILogger WithAddedLabels(this ILogger logger, params KeyValuePair /// Adds labels to the returned logger that may be included with every subsequent log. /// How this information is included will depend on the actual . - /// GoogleLogger(TODO: change this to a cref when I've moved it to this package) will include this - /// information in . + /// " will include this information in . + /// For the consumption of other loggers, the information will be included in newly created scopes + /// at the moment of logging, in a format that all loggers should understand. /// /// /// If had already been augmented with labels, diff --git a/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/LabellingScope.cs b/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/LabellingScope.cs index 27f50e25a1b4..217f087c7de2 100644 --- a/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/LabellingScope.cs +++ b/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/LabellingScope.cs @@ -25,7 +25,7 @@ namespace Google.Cloud.Diagnostics.Common /// // Note: We implement IEnumerable for the benefit of other ILogger implementations, // so they can represent this information as they see fit. - public class LabellingScopeState : IEnumerable> + internal class LabellingScopeState : IEnumerable> { /// /// Creates a new for the given labels. diff --git a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/LogUtils.cs b/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/LogLevelExtensions.cs similarity index 82% rename from apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/LogUtils.cs rename to apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/LogLevelExtensions.cs index 200034d78f7f..801e35f33c08 100644 --- a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/LogUtils.cs +++ b/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/LogLevelExtensions.cs @@ -1,10 +1,10 @@ -// Copyright 2016 Google Inc. All Rights Reserved. +// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -15,15 +15,12 @@ using Google.Cloud.Logging.Type; using Microsoft.Extensions.Logging; -#if NETCOREAPP3_1 -namespace Google.Cloud.Diagnostics.AspNetCore3 -#elif NETSTANDARD2_0 -namespace Google.Cloud.Diagnostics.AspNetCore -#else -#error unknown target framework -#endif +namespace Google.Cloud.Diagnostics.Common { - internal static class LogUtils + /// + /// Extension methods for . + /// + internal static class LogLevelExtensions { /// /// Extensions to get a for a . diff --git a/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/LoggerOptions.cs b/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/LoggerOptions.cs new file mode 100644 index 000000000000..f35118270deb --- /dev/null +++ b/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/LoggerOptions.cs @@ -0,0 +1,218 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Google.Api; +using Google.Api.Gax; +using Google.Api.Gax.Grpc; +using Google.Protobuf.WellKnownTypes; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.IO; +using static Google.Cloud.Diagnostics.Common.ServiceContextUtils; + +namespace Google.Cloud.Diagnostics.Common +{ + /// + /// Options for a ". + /// + public sealed class LoggerOptions + { + /// The base log name for all logs. + // We keep this as aspnetcore for backwards compatibility. + // It might be a little awkward for someone writing a console application + // to see their logs end up in something named "aspnetcore" but this is + // just a name, and if they are really interested, they can specify their own. + private const string _baseLogName = "aspnetcore"; + + /// The minimum log level. + public LogLevel LogLevel { get; } + + /// The name for the logs. + public string LogName { get; } + + /// The monitored resource. See: https://cloud.google.com/logging/docs/api/v2/resource-list + public MonitoredResource MonitoredResource { get; } + + /// The buffer options for the logger. + public BufferOptions BufferOptions { get; } + + /// The retry options for the logger. + public RetryOptions RetryOptions { get; } + + /// Custom labels for log entries. + /// Keys and values added to should not be null. + /// If they are, an exception will be throw when attempting to log an entry. + /// The entry won't be logged and the exception will be propagated depending + /// on the value of . + public Dictionary Labels { get; } + + /// + /// A to write diagnostics info about loggers created + /// with these . + /// Currently the only diagnostics info we provide is the URL where the logs written + /// with these options can be found. + /// + public TextWriter LoggerDiagnosticsOutput { get; } + + /// + /// An identifier of the service, such as the name of the executable or job. May be null. + /// When set, it will be included in the serviceContext field of the log entry JSON payload. + /// + public string ServiceName => + ServiceContext == null || !ServiceContext.Fields.TryGetValue(ServiceContextServiceKey, out Value v) ? null : v.StringValue; + + /// + /// A string that represents the version of the service or the source code that logs are written for. + /// When set, it will be included in the serviceContext field of the log entry JSON payload. + /// + public string Version => + ServiceContext == null || !ServiceContext.Fields.TryGetValue(ServiceContextVersionKey, out Value v) ? null : v.StringValue; + + /// + /// The service context to associate to the Logger. + /// + internal Struct ServiceContext { get; } + + private LoggerOptions( + LogLevel logLevel, + string logName, + Dictionary labels, + MonitoredResource monitoredResource, + BufferOptions bufferOptions, + RetryOptions retryOptions, + TextWriter loggerDiagnosticsOutput, + string serviceName, + string version) + { + LogName = logName; + LogLevel = GaxPreconditions.CheckEnumValue(logLevel, nameof(logLevel)); + Labels = labels; + MonitoredResource = monitoredResource; + BufferOptions = bufferOptions; + RetryOptions = retryOptions; + LoggerDiagnosticsOutput = loggerDiagnosticsOutput; + + // Create the service context here, this class is inmutable. + ServiceContext = CreateServiceContext(serviceName, version); + } + + /// + /// Create a new instance of . + /// + /// Optional, the minimum log level. Defaults to + /// Optional, the name of the log. Defaults to 'aspnetcore'. + /// Optional, custom labels to be added to log entries. + /// Keys and values added to should not be null. + /// If they are, an exception will be throw when attempting to log an entry. + /// The entry won't be logged and the exception will be propagated depending + /// on the value of . + /// Optional, the monitored resource. The monitored resource will + /// be automatically detected if it is not set and will default to the global resource if the detection fails. + /// See: https://cloud.google.com/logging/docs/api/v2/resource-list + /// Optional, the buffer options. Defaults to a + /// Optional, the retry options. Defaults to a + /// Optional. If set some logger diagnostics info will be written + /// to the given . Currently the only diagnostics info we provide is the URL where + /// the logs written with these options can be found. + public static LoggerOptions Create( + LogLevel logLevel = LogLevel.Information, + string logName = null, + Dictionary labels = null, + MonitoredResource monitoredResource = null, + BufferOptions bufferOptions = null, + RetryOptions retryOptions = null, + TextWriter loggerDiagnosticsOutput = null) => + CreateWithServiceContext(logLevel, logName, labels, monitoredResource, bufferOptions, retryOptions, loggerDiagnosticsOutput); + + /// + /// Create a new instance of with default service context. + /// If is provided, the service context will be obtained from it. + /// Else, if running on GAE, the service context will be obtained from the platform. + /// + /// Optional, the minimum log level. Defaults to + /// Optional, the name of the log. Defaults to 'aspnetcore'. + /// Optional, custom labels to be added to log entries. + /// Keys and values added to should not be null. + /// If they are, an exception will be throw when attempting to log an entry. + /// The entry won't be logged and the exception will be propagated depending + /// on the value of . + /// Optional, the monitored resource. The monitored resource will + /// be automatically detected if it is not set and will default to the global resource if the detection fails. + /// See: https://cloud.google.com/logging/docs/api/v2/resource-list + /// Optional, the buffer options. Defaults to a + /// Optional, the retry options. Defaults to a + /// Optional. If set some logger diagnostics info will be written + /// to the given . Currently the only diagnostics info we provide is the URL where + /// the logs written with these options can be found. + public static LoggerOptions CreateWithDetectedServiceContext( + LogLevel logLevel = LogLevel.Information, + string logName = null, + Dictionary labels = null, + MonitoredResource monitoredResource = null, + BufferOptions bufferOptions = null, + RetryOptions retryOptions = null, + TextWriter loggerDiagnosticsOutput = null) => + CreateWithServiceContext( + logLevel, + logName, + labels, + monitoredResource, + bufferOptions, + retryOptions, + loggerDiagnosticsOutput, + Project.GetServiceName(null, monitoredResource), + Project.GetServiceVersion(null, monitoredResource)); + + /// + /// Create a new instance of . + /// + /// Optional, the minimum log level. Defaults to + /// Optional, the name of the log. Defaults to 'aspnetcore'. + /// Optional, custom labels to be added to log entries. + /// Keys and values added to should not be null. + /// If they are, an exception will be throw when attempting to log an entry. + /// The entry won't be logged and the exception will be propagated depending + /// on the value of . + /// Optional, the monitored resource. The monitored resource will + /// be automatically detected if it is not set and will default to the global resource if the detection fails. + /// See: https://cloud.google.com/logging/docs/api/v2/resource-list + /// Optional, the buffer options. Defaults to a + /// Optional, the retry options. Defaults to a + /// Optional. If set some logger diagnostics info will be written + /// to the given . Currently the only diagnostics info we provide is the URL where + /// the logs written with these options can be found. + /// A string that represents the version of the service or the source code that logs are written for. + /// When set, it will be included in the serviceContext field of the log entry JSON payload. + /// A string that represents the version of the service or the source code that logs are written for. + /// When set, it will be included in the serviceContext field of the log entry JSON payload. + public static LoggerOptions CreateWithServiceContext( + LogLevel logLevel = LogLevel.Information, + string logName = null, + Dictionary labels = null, + MonitoredResource monitoredResource = null, + BufferOptions bufferOptions = null, + RetryOptions retryOptions = null, + TextWriter loggerDiagnosticsOutput = null, + string serviceName = null, + string version = null) + { + logName ??= _baseLogName; + labels ??= new Dictionary(); + monitoredResource ??= MonitoredResourceBuilder.FromPlatform(); + bufferOptions ??= BufferOptions.TimedBuffer(); + retryOptions ??= RetryOptions.NoRetry(); + return new LoggerOptions(logLevel, logName, labels, monitoredResource, bufferOptions, retryOptions, loggerDiagnosticsOutput, serviceName, version); + } + } +}