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 e3a127d23727..d5f2130417f1 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 @@ -160,7 +160,7 @@ public async Task Logging_Scope() { var message = EntryData.GetMessage(nameof(MainController.Scope), testId); Assert.Equal(message, results.Single().JsonPayload.Fields["message"].StringValue); - Assert.Contains("Scope => ", results.Single().JsonPayload.Fields["scope"].StringValue); + Assert.Contains("Scope", results.Single().JsonPayload.Fields["scope"].StringValue); }); } diff --git a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/GoogleLoggerScopeTest.cs b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/GoogleLoggerScopeTest.cs deleted file mode 100644 index d59f6824cf65..000000000000 --- a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/GoogleLoggerScopeTest.cs +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright 2017 Google Inc. All Rights Reserved. -// -// 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 -// -// 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; -using System.Threading; -using System.Threading.Tasks; -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 -{ - public class GoogleLoggerScopeTest - { - [Fact] - public void Current_Unset() - { - Assert.Null(GoogleLoggerScope.Current); - } - - [Fact] - public void Scope() - { - Assert.Null(GoogleLoggerScope.Current); - using (new GoogleLoggerScope("message")) - { - Assert.Equal("message => ", GoogleLoggerScope.Current.ToString()); - } - Assert.Null(GoogleLoggerScope.Current); - } - - [Fact] - public void MultipleScopes_Nested() - { - Assert.Null(GoogleLoggerScope.Current); - using (new GoogleLoggerScope("grandparent")) - { - Assert.Equal("grandparent => ", GoogleLoggerScope.Current.ToString()); - using (new GoogleLoggerScope("parent")) - { - Assert.Equal("grandparent => parent => ", GoogleLoggerScope.Current.ToString()); - using (new GoogleLoggerScope("child")) - { - Assert.Equal("grandparent => parent => child => ", GoogleLoggerScope.Current.ToString()); - } - } - Assert.Equal("grandparent => ", GoogleLoggerScope.Current.ToString()); - using (new GoogleLoggerScope("other_parent")) - { - Assert.Equal("grandparent => other_parent => ", GoogleLoggerScope.Current.ToString()); - } - Assert.Equal("grandparent => ", GoogleLoggerScope.Current.ToString()); - } - Assert.Null(GoogleLoggerScope.Current); - } - - [Fact] - public async Task MultipleScopes_Threads_Running_During_Scope() - { - string rootScope = "root"; - Func func = async (grandparent, parent, child) => - { - Assert.Equal($"{rootScope} => ", GoogleLoggerScope.Current.ToString()); - using (new GoogleLoggerScope(grandparent)) - { - Assert.Equal($"{rootScope} => {grandparent} => ", GoogleLoggerScope.Current.ToString()); - await Task.Yield(); - using (new GoogleLoggerScope(parent)) - { - Assert.Equal($"{rootScope} => {grandparent} => {parent} => ", GoogleLoggerScope.Current.ToString()); - await Task.Yield(); - using (new GoogleLoggerScope(child)) - { - Assert.Equal($"{rootScope} => {grandparent} => {parent} => {child} => ", GoogleLoggerScope.Current.ToString()); - await Task.Yield(); - } - await Task.Yield(); - Assert.Equal($"{rootScope} => {grandparent} => {parent} => ", GoogleLoggerScope.Current.ToString()); - } - await Task.Yield(); - Assert.Equal($"{rootScope} => {grandparent} => ", GoogleLoggerScope.Current.ToString()); - } - Assert.Equal($"{rootScope} => ", GoogleLoggerScope.Current.ToString()); - }; - - using (new GoogleLoggerScope(rootScope)) - { - await Task.WhenAll( - Task.Run(() => func("grandparent-one", "parent-one", "child-one")), - Task.Run(() => func("grandparent-two", "parent-two", "child-two")), - Task.Run(() => func("grandparent-three", "parent-three", "child-three")), - Task.Run(() => func("grandparent-four", "parent-four", "child-four")), - Task.Run(() => func("grandparent-five", "parent-five", "child-five"))); - } - } - - [Fact] - public async Task MultipleScopes_Threads_Started_Before_Scope() - { - var childThreadsReleased = new ManualResetEventSlim(initialState: false); - - Func op = async (parent, child) => - { - childThreadsReleased.Wait(); - - Assert.Null(GoogleLoggerScope.Current); - using (new GoogleLoggerScope(parent)) - { - Assert.Equal($"{parent} => ", GoogleLoggerScope.Current.ToString()); - await Task.Yield(); - using (new GoogleLoggerScope(child)) - { - Assert.Equal($"{parent} => {child} => ", GoogleLoggerScope.Current.ToString()); - await Task.Yield(); - } - await Task.Yield(); - Assert.Equal($"{parent} => ", GoogleLoggerScope.Current.ToString()); - } - await Task.Yield(); - Assert.Null(GoogleLoggerScope.Current); - }; - - var t1 = Task.Run(() => op("parent-one", "child-one").Wait()); - var t2 = Task.Run(() => op("parent-two", "child-two").Wait()); - - using (new GoogleLoggerScope("root")) - { - childThreadsReleased.Set(); - await Task.WhenAll(t1, t2); - } - } - - [Fact] - public async Task MultipleScopes_Threads_Started_During_Scope() - { - string rootScope = "root"; - var childThreadsReleased = new ManualResetEventSlim(initialState: false); - - Func op = async (parent, child) => - { - childThreadsReleased.Wait(); - - Assert.Equal($"{rootScope} => ", GoogleLoggerScope.Current.ToString()); - using (new GoogleLoggerScope(parent)) - { - Assert.Equal($"{rootScope} => {parent} => ", GoogleLoggerScope.Current.ToString()); - await Task.Yield(); - using (new GoogleLoggerScope(child)) - { - Assert.Equal($"{rootScope} => {parent} => {child} => ", GoogleLoggerScope.Current.ToString()); - await Task.Yield(); - } - await Task.Yield(); - Assert.Equal($"{rootScope} => {parent} => ", GoogleLoggerScope.Current.ToString()); - } - await Task.Yield(); - Assert.Equal($"{rootScope} => ", GoogleLoggerScope.Current.ToString()); - }; - - Task t1; - Task t2; - using (new GoogleLoggerScope(rootScope)) - { - t1 = Task.Run(() => op("parent-one", "child-one").Wait()); - t2 = Task.Run(() => op("parent-two", "child-two").Wait()); - } - - childThreadsReleased.Set(); - await Task.WhenAll(t1, t2); - } - } -} 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 848483352741..4ac5779fad7d 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 @@ -19,7 +19,6 @@ using Google.Cloud.Diagnostics.Common; using Google.Cloud.Logging.V2; using Google.Protobuf.WellKnownTypes; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Moq; using System; @@ -141,7 +140,7 @@ public void BeginScope() { Predicate> matcher = (l) => l.Single().JsonPayload.Fields["message"].StringValue == LogMessage && - l.Single().JsonPayload.Fields["scope"].StringValue == "scope => "; + l.Single().JsonPayload.Fields["scope"].StringValue == "scope"; var mockConsumer = new Mock>(); mockConsumer.Setup(c => c.Receive(Match.Create(matcher))); var logger = GetLogger(mockConsumer.Object, logLevel: LogLevel.Information); @@ -161,7 +160,7 @@ public void BeginScope_WithFormattedScope() var parentScopes = json["parent_scopes"].ListValue.Values; var parentScope0 = parentScopes[0].StructValue.Fields; return json["message"].StringValue == LogMessage && - json["scope"].StringValue == "scope 42, Baz => " && + json["scope"].StringValue == "scope 42, Baz" && parentScopes.Count == 1 && parentScope0.Count == 3 && parentScope0["Foo"].StringValue == "42" && @@ -189,7 +188,7 @@ public void BeginScope_DigitOnlyFormatParametersHaveUnderscorePrefix() var parentScopes = json["parent_scopes"].ListValue.Values; var parentScope0 = parentScopes[0].StructValue.Fields; return json["message"].StringValue == LogMessage && - json["scope"].StringValue == "scope 42, Baz => " && + json["scope"].StringValue == "scope 42, Baz" && parentScopes.Count == 1 && parentScope0.Count == 3 && parentScope0["_0"].StringValue == "42" && @@ -219,7 +218,7 @@ public void BeginScope_WithNestedFormattedScope() var scope1 = parentScopes[1].StructValue.Fields; return json["message"].StringValue == LogMessage && - json["scope"].StringValue == "first 42 => second Baz => " && + json["scope"].StringValue == "first 42 => second Baz" && parentScopes.Count == 2 && scope0.Count == 2 && scope0["{OriginalFormat}"].StringValue == "second {Bar}" && @@ -256,7 +255,7 @@ public void BeginScope_WithFormattedMessageAndScope() var parentScopes = json["parent_scopes"].ListValue.Values; var parentScope0 = parentScopes[0].StructValue.Fields; return json["message"].StringValue == "a log message with stuff" && - json["scope"].StringValue == "scope 42 => " && + json["scope"].StringValue == "scope 42" && formatParams.Count == 2 && formatParams["things"].StringValue == logParam && formatParams["{OriginalFormat}"].StringValue == message && @@ -282,7 +281,7 @@ public void BeginScope_Nested() { Predicate> matcher = (l) => l.Single().JsonPayload.Fields["message"].StringValue == LogMessage && - l.Single().JsonPayload.Fields["scope"].StringValue == "parent => child => "; + l.Single().JsonPayload.Fields["scope"].StringValue == "parent => child"; var mockConsumer = new Mock>(); mockConsumer.Setup(c => c.Receive(Match.Create(matcher))); var logger = GetLogger(mockConsumer.Object, LogLevel.Information); 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 dd18cd544a0d..b9d978476845 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 @@ -35,23 +35,6 @@ namespace Google.Cloud.Diagnostics.AspNetCore /// /// for Google Cloud Logging. /// - /// - /// - /// - /// public void Configure(ILoggerFactory loggerFactory) - /// { - /// string projectId = "[Google Cloud Platform project ID]"; - /// loggerFactory.AddGoogle(projectId); - /// ... - /// } - /// - /// - /// - /// - /// Logs to Google Cloud Logging. - /// Docs: https://cloud.google.com/logging/docs/ - /// - /// public sealed class GoogleLogger : ILogger { private const string GcpConsoleLogsBaseUrl = "https://console.cloud.google.com/logs/viewer"; @@ -97,7 +80,7 @@ internal GoogleLogger(IConsumer consumer, LogTarget logTarget, LoggerO } /// - public IDisposable BeginScope(TState state) => new GoogleLoggerScope(state); + public IDisposable BeginScope(TState state) => GoogleLoggerScope.BeginScope(state); /// public bool IsEnabled(LogLevel logLevel) => logLevel >= _loggerOptions.LogLevel; @@ -130,6 +113,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except Labels = { CreateLabels() }, }; + GoogleLoggerScope.Current?.ApplyFullScopeStack(entry); SetTraceAndSpanIfAny(entry); _consumer.Receive(new[] { entry }); @@ -194,32 +178,7 @@ private Struct CreateJsonPayload(EventId eventId, TState state, Exceptio if (state is IEnumerable> formatParams && ContainsFormatParameters(formatParams)) { - jsonStruct.Fields.Add("format_parameters", CreateStructValue(formatParams)); - } - - var currentLogScope = GoogleLoggerScope.Current; - if (currentLogScope != null) - { - jsonStruct.Fields.Add("scope", Value.ForString(currentLogScope.ToString())); - } - - // Create a map of format parameters of all the parent scopes, - // starting from the most inner scope to the top-level scope. - var scopeParamsList = new List(); - while (currentLogScope != null) - { - // Determine if the state of the scope are format params - if (currentLogScope.State is IEnumerable> scopeFormatParams) - { - scopeParamsList.Add(CreateStructValue(scopeFormatParams)); - } - - currentLogScope = currentLogScope.Parent; - } - - if (scopeParamsList.Count > 0) - { - jsonStruct.Fields.Add("parent_scopes", Value.ForList(scopeParamsList.ToArray())); + jsonStruct.Fields.Add("format_parameters", formatParams.ToStructValue()); } return jsonStruct; @@ -247,25 +206,6 @@ bool ContainsFormatParameters(IEnumerable> fields) return iterator.MoveNext(); } } - - Value CreateStructValue(IEnumerable> fields) - { - Struct fieldsStruct = new Struct(); - foreach (var pair in fields) - { - string key = pair.Key; - if (string.IsNullOrEmpty(key)) - { - continue; - } - if (char.IsDigit(key[0])) - { - key = "_" + key; - } - fieldsStruct.Fields[key] = Value.ForString(pair.Value?.ToString() ?? ""); - } - return Value.ForStruct(fieldsStruct); - } } private void SetTraceAndSpanIfAny(LogEntry entry) diff --git a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/GoogleLoggerScope.cs b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/GoogleLoggerScope.cs deleted file mode 100644 index 26248b3315ac..000000000000 --- a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/GoogleLoggerScope.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2017 Google Inc. All Rights Reserved. -// -// 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 -// -// 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 System; -using System.Threading; - -#if NETCOREAPP3_1 -namespace Google.Cloud.Diagnostics.AspNetCore3 -#elif NETSTANDARD2_0 -namespace Google.Cloud.Diagnostics.AspNetCore -#else -#error unknown target framework -#endif -{ - /// Scope for the . - internal class GoogleLoggerScope : IDisposable - { - private static AsyncLocal _current = new AsyncLocal(); - - /// - /// The current scope, can be null. - /// - public static GoogleLoggerScope Current - { - get - { - return _current.Value; - } - set - { - _current.Value = value; - } - } - - /// The state associated with the this scope. - public object State { get; } - - /// The parent scope, can be null. - public GoogleLoggerScope Parent { get; } - - public GoogleLoggerScope(object state) - { - State = GaxPreconditions.CheckNotNull(state, nameof(state)); - Parent = Current; - Current = this; - } - - /// Disposes of the current scope. - public void Dispose() => Current = Current?.Parent; - - /// - /// Gets the scope (and parents') as a string. It is in the format: - /// "grandparent => parent => child => " where the scope - /// value is "child", its parent is "parent" and its grandparent is "grandparent". - /// - public override string ToString() => $"{Parent}{State} => "; - } -} diff --git a/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common.Tests/Logging/GoogleLoggerScopeTest.cs b/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common.Tests/Logging/GoogleLoggerScopeTest.cs new file mode 100644 index 000000000000..121852f29637 --- /dev/null +++ b/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common.Tests/Logging/GoogleLoggerScopeTest.cs @@ -0,0 +1,303 @@ +// 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.Cloud.Logging.V2; +using Google.Protobuf.WellKnownTypes; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Google.Cloud.Diagnostics.Common.Tests +{ + public class GoogleLoggerScopeTest + { + private LogEntry GetEmptyLogEntry() => new LogEntry { JsonPayload = new Struct() }; + + private void AssertScope(string scopeValue) + { + var logEntry = GetEmptyLogEntry(); + GoogleLoggerScope.Current.ApplyFullScopeStack(logEntry); + Assert.Equal(scopeValue, logEntry.JsonPayload.Fields["scope"].StringValue); + } + + private void AssertParentScopes(params Struct[] values) + { + var logEntry = GetEmptyLogEntry(); + GoogleLoggerScope.Current.ApplyFullScopeStack(logEntry); + Assert.Collection( + logEntry.JsonPayload.Fields["parent_scopes"].ListValue.Values.Select(v => v.StructValue), + values.Select>(expected => actual => Assert.Equal(expected, actual)).ToArray()); + } + + [Fact] + public void Current_Unset() + { + Assert.Null(GoogleLoggerScope.Current); + } + + [Fact] + public void Scope() + { + Assert.Null(GoogleLoggerScope.Current); + using (GoogleLoggerScope.BeginScope("message")) + { + AssertScope("message"); + } + Assert.Null(GoogleLoggerScope.Current); + } + + [Fact] + public void MultipleScopes_Nested() + { + Assert.Null(GoogleLoggerScope.Current); + using (GoogleLoggerScope.BeginScope("grandparent")) + { + AssertScope("grandparent"); + + using (GoogleLoggerScope.BeginScope("parent")) + { + AssertScope("grandparent => parent"); + + using (GoogleLoggerScope.BeginScope("child")) + { + AssertScope("grandparent => parent => child"); + } + + AssertScope("grandparent => parent"); + } + + AssertScope("grandparent"); + + using (GoogleLoggerScope.BeginScope("other_parent")) + { + AssertScope("grandparent => other_parent"); + } + + AssertScope("grandparent"); + } + Assert.Null(GoogleLoggerScope.Current); + } + + [Fact] + public void MultipleScopes_Nested_KeyValuePairs() + { + Assert.Null(GoogleLoggerScope.Current); + using (GoogleLoggerScope.BeginScope(new DummyKeyValuePairs( + new KeyValuePair("grandfather", "Joey"), + new KeyValuePair("grandmother", "Janey")))) + { + AssertScope("['grandfather'='Joey']['grandmother'='Janey']"); + AssertParentScopes( + new Struct { Fields = { { "grandfather", Value.ForString("Joey") }, { "grandmother", Value.ForString("Janey") } } }); + + using (GoogleLoggerScope.BeginScope(new DummyKeyValuePairs( + new KeyValuePair("father", "Joe"), + new KeyValuePair("mother", "Jane")))) + { + AssertScope("['grandfather'='Joey']['grandmother'='Janey'] => ['father'='Joe']['mother'='Jane']"); + AssertParentScopes( + new Struct { Fields = { { "father", Value.ForString("Joe") }, { "mother", Value.ForString("Jane") } } }, + new Struct { Fields = { { "grandfather", Value.ForString("Joey") }, { "grandmother", Value.ForString("Janey") } } }); + } + + AssertScope("['grandfather'='Joey']['grandmother'='Janey']"); + AssertParentScopes( + new Struct { Fields = { { "grandfather", Value.ForString("Joey") }, { "grandmother", Value.ForString("Janey") } } }); + } + Assert.Null(GoogleLoggerScope.Current); + } + + [Fact] + public void MultipleScopes_Nested_Mixed() + { + Assert.Null(GoogleLoggerScope.Current); + using (GoogleLoggerScope.BeginScope(new DummyKeyValuePairs( + new KeyValuePair("father", "Joe"), + new KeyValuePair("mother", "Jane")))) + { + AssertScope("['father'='Joe']['mother'='Jane']"); + AssertParentScopes( + new Struct { Fields = { { "father", Value.ForString("Joe") }, { "mother", Value.ForString("Jane") } } }); + + using (GoogleLoggerScope.BeginScope("myself")) + { + AssertScope("['father'='Joe']['mother'='Jane'] => myself"); + // "myself" is not added to parent_scopes because it is not of the form key=>value. + AssertParentScopes( + new Struct { Fields = { { "father", Value.ForString("Joe") }, { "mother", Value.ForString("Jane") } } }); + } + + AssertScope("['father'='Joe']['mother'='Jane']"); + AssertParentScopes( + new Struct { Fields = { { "father", Value.ForString("Joe") }, { "mother", Value.ForString("Jane") } } }); + } + Assert.Null(GoogleLoggerScope.Current); + } + + [Fact] + public async Task MultipleScopes_Threads_Running_During_Scope() + { + string rootScope = "root"; + Func func = async (grandparent, parent, child) => + { + AssertScope(rootScope); + + using (GoogleLoggerScope.BeginScope(grandparent)) + { + AssertScope($"{rootScope} => {grandparent}"); + await Task.Yield(); + + using (GoogleLoggerScope.BeginScope(parent)) + { + AssertScope($"{rootScope} => {grandparent} => {parent}"); + await Task.Yield(); + + using (GoogleLoggerScope.BeginScope(child)) + { + AssertScope($"{rootScope} => {grandparent} => {parent} => {child}"); + await Task.Yield(); + } + + await Task.Yield(); + AssertScope($"{rootScope} => {grandparent} => {parent}"); + } + + await Task.Yield(); + AssertScope($"{rootScope} => {grandparent}"); + } + + AssertScope(rootScope); + }; + + using (GoogleLoggerScope.BeginScope(rootScope)) + { + await Task.WhenAll( + Task.Run(() => func("grandparent-one", "parent-one", "child-one")), + Task.Run(() => func("grandparent-two", "parent-two", "child-two")), + Task.Run(() => func("grandparent-three", "parent-three", "child-three")), + Task.Run(() => func("grandparent-four", "parent-four", "child-four")), + Task.Run(() => func("grandparent-five", "parent-five", "child-five"))); + } + } + + [Fact] + public async Task MultipleScopes_Threads_Started_Before_Scope() + { + var childThreadsReleased = new ManualResetEventSlim(initialState: false); + + Func op = async (parent, child) => + { + childThreadsReleased.Wait(); + + Assert.Null(GoogleLoggerScope.Current); + + using (GoogleLoggerScope.BeginScope(parent)) + { + AssertScope(parent); + await Task.Yield(); + + using (GoogleLoggerScope.BeginScope(child)) + { + AssertScope($"{parent} => {child}"); + await Task.Yield(); + } + + await Task.Yield(); + AssertScope(parent); + } + + await Task.Yield(); + Assert.Null(GoogleLoggerScope.Current); + }; + + var t1 = Task.Run(() => op("parent-one", "child-one").Wait()); + var t2 = Task.Run(() => op("parent-two", "child-two").Wait()); + + using (GoogleLoggerScope.BeginScope("root")) + { + childThreadsReleased.Set(); + await Task.WhenAll(t1, t2); + } + } + + [Fact] + public async Task MultipleScopes_Threads_Started_During_Scope() + { + string rootScope = "root"; + var childThreadsReleased = new ManualResetEventSlim(initialState: false); + + Func op = async (parent, child) => + { + childThreadsReleased.Wait(); + + AssertScope(rootScope); + + using (GoogleLoggerScope.BeginScope(parent)) + { + AssertScope($"{rootScope} => {parent}"); + await Task.Yield(); + + using (GoogleLoggerScope.BeginScope(child)) + { + AssertScope($"{rootScope} => {parent} => {child}"); + await Task.Yield(); + } + + await Task.Yield(); + AssertScope($"{rootScope} => {parent}"); + } + + await Task.Yield(); + AssertScope(rootScope); + }; + + Task t1; + Task t2; + using (GoogleLoggerScope.BeginScope(rootScope)) + { + t1 = Task.Run(() => op("parent-one", "child-one").Wait()); + t2 = Task.Run(() => op("parent-two", "child-two").Wait()); + } + + childThreadsReleased.Set(); + await Task.WhenAll(t1, t2); + } + + private class DummyKeyValuePairs : IEnumerable> + { + private IEnumerable> _pairs; + public DummyKeyValuePairs(params KeyValuePair[] pairs) => _pairs = pairs; + + + public IEnumerator> GetEnumerator() => _pairs.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public override string ToString() + { + StringBuilder builder = new StringBuilder(); + foreach (var pair in _pairs) + { + builder.Append($"['{pair.Key}'='{pair.Value}']"); + } + return builder.ToString(); + } + } + } +} 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 new file mode 100644 index 000000000000..3afdd81353a9 --- /dev/null +++ b/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/KeyValuePairEnumerableExtensions.cs @@ -0,0 +1,59 @@ +// 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.Protobuf.WellKnownTypes; +using System.Collections.Generic; + +namespace Google.Cloud.Diagnostics.Common +{ + /// + /// Extension methods for converting several types to Protobub well known types. + /// + public static class KeyValuePairEnumerableExtensions + { + /// + /// Returns a for a that will contain + /// a field for each key present in . The value of each + /// field in the Struct will the the one associated to the corresponding key in + /// . + /// + /// 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) + { + if (fields == null) + { + return null; + } + + bool hasValues = false; + Struct fieldsStruct = new Struct(); + foreach (var pair in fields) + { + hasValues = true; + string key = pair.Key; + if (string.IsNullOrEmpty(key)) + { + continue; + } + if (char.IsDigit(key[0])) + { + key = "_" + key; + } + fieldsStruct.Fields[key] = Value.ForString(pair.Value?.ToString() ?? ""); + } + return hasValues ? Value.ForStruct(fieldsStruct) : null; + } + } +} 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 new file mode 100644 index 000000000000..2f84969be49a --- /dev/null +++ b/apis/Google.Cloud.Diagnostics.Common/Google.Cloud.Diagnostics.Common/Logging/GoogleLoggerScope.cs @@ -0,0 +1,186 @@ +// 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 System; +using System.Collections.Generic; +using System.Threading; + +namespace Google.Cloud.Diagnostics.Common +{ + /// + /// Represents a scope for a Google Logger. + /// + public abstract class GoogleLoggerScope : IDisposable + { + private static readonly AsyncLocal _current = new AsyncLocal(); + + /// + /// The current scope, may be null. + /// + public static GoogleLoggerScope Current + { + get + { + return _current.Value; + } + private set + { + _current.Value = value; + } + } + + /// + /// Creates a new scope with the given state and as parent. + /// Sets the newly created scope as . + /// + /// + public static GoogleLoggerScope BeginScope(object state) => + Current = state switch + { + IEnumerable> keyValues => new KeyValueLoggerScope(keyValues, Current), + _ => new GoogleLoggerScope(state, Current) + }; + + /// + /// The parent scope, may be null. + /// + protected internal GoogleLoggerScope Parent { get; } + + /// + /// Creates a new scope. + /// + protected internal GoogleLoggerScope(GoogleLoggerScope parent) + => Parent = parent; + + /// + /// Removes this scope, and all inner scopes, from the scope stack. + /// + public void Dispose() + { + var maybeMyself = Current; + while (maybeMyself != null && maybeMyself != this) + { + maybeMyself = maybeMyself.Parent; + } + // Only if we've found this instance in the Scope stack + // we pop it and all it's child scopes. + if (maybeMyself == this) + { + Current = Parent; + } + } + + /// + /// Modifies the log entry adding information for the full scope stack + /// whose top, or most recently created scope, is . + /// Information is added from least recent scope to more recent one, so that + /// information added by more recent scopes can overwrite information added + /// by less recent ones. + /// + public void ApplyFullScopeStack(LogEntry entry) + { + GaxPreconditions.CheckNotNull(entry, nameof(entry)); + GaxPreconditions.CheckNotNull(entry.JsonPayload, nameof(entry.JsonPayload)); + ApplyFullScopeStackImpl(entry); + } + + private void ApplyFullScopeStackImpl(LogEntry entry) + { + Parent?.ApplyFullScopeStackImpl(entry); + ApplyThisScope(entry); + } + + /// + /// Apply this scope's information only. + /// Implementers should decide whether to overwirte similar information from + /// previous scopes or combine it with this one. + /// + protected internal abstract void ApplyThisScope(LogEntry entry); + } + + internal class GoogleLoggerScope : GoogleLoggerScope + { + /// The state associated with the this scope. + public TState State { get; } + + public GoogleLoggerScope(TState state, GoogleLoggerScope parent) + : base(parent) + { + GaxPreconditions.CheckNotNull((object)state, nameof(state)); + State = state; + } + + /// + /// Applies the information contained in to the log entry. + /// + /// + /// is called on and it's added + /// to the in a field named scope. + /// This method will combine existing information so that the scope + /// field looks like "grandparentScope => parentScope => childScope". + /// + protected internal override void ApplyThisScope(LogEntry entry) + { + string stateText = State.ToString() ?? ""; + if (entry.JsonPayload.Fields.TryGetValue("scope", out Value value)) + { + value = Value.ForString($"{value.StringValue} => {stateText}"); + } + else + { + value = Value.ForString(stateText); + } + + entry.JsonPayload.Fields["scope"] = value; + } + } + + internal class KeyValueLoggerScope : GoogleLoggerScope>> + { + public KeyValueLoggerScope(IEnumerable> state, GoogleLoggerScope parent) + : base(state, parent) + { } + + /// + /// Applies the information contained in to the log entry. + /// + /// + /// This method first calls . + /// Then, if contains elements it will create + /// a adding each key value pair to . + /// This will be added to the on a list field + /// named parent_scopes. + /// + protected internal override void ApplyThisScope(LogEntry entry) + { + base.ApplyThisScope(entry); + + if (State.ToStructValue() is not Value newValue) + { + return; + } + + if (!entry.JsonPayload.Fields.TryGetValue("parent_scopes", out Value existingValues)) + { + existingValues = Value.ForList(); + entry.JsonPayload.Fields["parent_scopes"] = existingValues; + } + + existingValues.ListValue.Values.Insert(0, newValue); + } + } +}